admin.py 43 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 337
               'extension_duration': m.get_extension_duration()
              }
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
    exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
360 361 362 363 364 365 366 367
    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),
368
        'show_ftop_button': getattr(settings, 'BDM_SHOW_FTOP_BUTTON', True),
Damien Moulard committed
369
        'has_committe_shift': committees_shift_id is not None,
370
        'committees_shift_id': committees_shift_id,
371
        'committees_shift_name': committees_shift_name,
372 373
        'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
        'exemptions_shift_id': exemptions_shift_id,
374
    }
Félicie committed
375 376
    return HttpResponse(template.render(context, request))

377 378 379 380 381
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
382 383 384 385 386
def get_attached_members(request):
    """ Récupération des membres en binôme """
    res = CagetteMembers.get_attached_members()
    return JsonResponse({ 'res' : res })

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

        res["res"] = []
395 396 397 398 399 400
        update_members_makeups_core(members_data, res)
        response = JsonResponse(res)
    else:
        res["message"] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response
401

Thibault Grandjean committed
402

403 404 405
def update_members_makeups_core(members_data, res):
    for member_data in members_data:
        cm = CagetteMember(int(member_data["member_id"]))
406

407
        res["res"].append(cm.update_member_makeups(member_data))
408

409 410
        # Update member standard points, for standard members only
        if member_data["member_shift_type"] == "standard":
411 412 413 414
            # Set points to minus the number of makeups to do + the makeups to come (limited to -2)
            cs = CagetteShift()
            shift_data = cs.get_shift_partner(int(member_data["member_id"]))
            target_points = - int(member_data["target_makeups_nb"]) - sum(1 for value in shift_data if value['is_makeup'])
415 416
            if (target_points < -2):
                target_points = -2
417

418 419
            member_points = cm.get_member_points("standard")
            points_diff = abs(member_points - target_points)
420

421 422
            # Don't update if no change
            if points_diff == 0:
423
                res["res"][-1]['standard_points'] = member_points
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
                continue

            if member_points > target_points:
                points_update = - points_diff
            else:
                points_update = points_diff

            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': points_update
            }

            cm.update_member_points(data)
440 441 442
            #There are some odoo process happening here that could change the actual number of points
            #so we want to fetch it again to send it back
            res["res"][-1]['standard_points'] = cm.get_member_points("standard")
443 444 445 446 447 448 449
    # Better to call run_process_target_status now, otherwise partner remains
    # in alert until routine is called (every 5 minutes). It is a bit weird for users and
    # allocation of rattrapages before the routine is executed will not trigger change to delay state !
    # (the parner would have to go back to espace membre and click on "j'affecte mes rattrapage"
    # even though it shows 'J'ai 0 rattrapages à effecter' for the delay state change to be eventually triggered)
    api = OdooAPI()
    api.execute('res.partner', 'run_process_target_status', [])
450

451

452 453 454 455
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)
456
    
457
    if is_connected_user is True:
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
        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)

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 502 503
        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()

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

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

# --- Gestion des créneaux

514 515 516 517 518 519 520
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"])
521 522
        shift_registration_id = int(data["shift_registration_id"])
        shift_is_makeup = data["shift_is_makeup"]
523
        cancellation_description = data["cancellation_description"]
524 525 526

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

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

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
        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"])
            shift_template_id = int(data["shift_template_id"])
            makeups_to_do = int(data["makeups_to_do"])
            permanent_unsuscribe = data["permanent_unsuscribe"]

            cm = CagetteMember(partner_id)

            # Get partner nb of future makeup shifts
            partner_makeups = cm.get_member_selected_makeups()

            target_makeup = makeups_to_do + len(partner_makeups)
            if target_makeup > 2:
                target_makeup = 2

            # Update partner makeups to do
568
            res["update_makeups"] = cm.update_member_makeups({'target_makeups_nb': target_makeup})
569

570
            # Delete all shift registrations & shift template registration
571
            res["unsubscribe_member"] = cm.unsubscribe_member()
572

573 574
            if permanent_unsuscribe is True:
                res["set_done"] = cm.set_cooperative_state("gone")
575 576 577 578 579 580 581
                if res["set_done"]:
                    """ Delete pair(s?) of partner if it is a parent to improve statistics (#4810) """
                    api = OdooAPI()
                    associated_members = api.search_read('res.partner', [['parent_id', '=', partner_id]], ['id'])
                    for am in associated_members:
                        data = {"child": {"id": am["id"]}, "gone": ["parent", "child"]}
                        delete_pair_core(data)
582

583 584
        except Exception as e:
            res["error"] = str(e)
585 586 587 588 589

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

592
def shift_subscription(request):
593 594 595 596 597
    """ 
        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.
    """
598
    res = {}
599 600 601 602 603 604 605 606
    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:
607 608
        api = OdooAPI()

609 610
        partner_id = int(data["partner_id"])
        shift_type = data["shift_type"]
611
        moving_from_std_to_ftop = False
612 613
        if shift_type == 1:
            # 1 = standard
614
            shift_template_id = int(data["shift_template_id"])
615
        elif shift_type == 2:
616
            # 2 = ftop
Damien Moulard committed
617 618

            # First try to get committees shift
619
            shift_template_id = CagetteServices.get_committees_shift_id()
Damien Moulard committed
620 621 622
            # 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()
623 624 625 626

            c = [['id', '=', partner_id]]
            f = ['shift_type']
            moving_from_std_to_ftop = api.search_read('res.partner', c, f)[0]['shift_type'] == 'standard'
627 628 629 630
        else:
            # 3 = exempté
            # Get exemptions shift
            shift_template_id = CagetteServices.get_exemptions_shift_id()
Damien Moulard committed
631

632
        m = CagetteMember(partner_id)
633 634 635 636 637 638 639 640 641 642 643 644 645 646

        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
                )
647 648
            changing_shift = not moving_from_std_to_ftop
            res["unsubscribe_member"] = m.unsubscribe_member(changing_shift)
649

650 651
        if moving_from_std_to_ftop:
            lower_makeup_count_to_zero_if_moving_from_std_to_ftop(partner_id, res)
652
        reg_id = m.create_coop_shift_subscription(shift_template_id, shift_type)
653

654
        # Return necessary data
655 656 657 658 659 660 661 662 663 664 665 666
        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)
667 668 669 670 671
    else:
        response = JsonResponse({"message": "Unauthorized"}, status=403)

    return response

672

673 674 675 676 677 678 679 680 681 682 683 684
def lower_makeup_count_to_zero_if_moving_from_std_to_ftop(partner_id, res):
    members_data = []
    members_data.append({
        'member_id': partner_id,
        'member_shift_type': 'standard',
        'target_makeups_nb': 0,
        'description': 'reset automatique du compteur rattrapages suite changement créneau standard vers non standard'
    })
    res["res"] = []
    update_members_makeups_core(members_data, res)
    if res["res"][0]["update"]:
        res["makeups_to_do"] = 0
685

686
# --- Gestion des binômes
687

688
def get_member_info(request, id):
689 690 691 692 693 694 695
    """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
696
            'name',
697 698 699 700 701 702 703 704 705 706 707
            'sex',
            'cooperative_state',
            'email',
            'street',
            'street2',
            'zip',
            'city',
            'current_template_name',
            'shift_type',
            'parent_id',
            'is_associated_people',
Félicie committed
708
            'parent_name',
709 710
            "makeups_to_do",
            "barcode_base"
711
        ]
712 713
        cond = [['id', '=', id]]
        member = api.search_read('res.partner', cond, fields)
714
        if member:
715 716 717
            member = member[0]
            parent = None
            if member['parent_id']:
718 719 720 721 722
                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']
723
            res['member'] = member
724 725 726 727 728 729 730
            response = JsonResponse(res)
        else:
            response = JsonResponse({"message": "Not found"}, status=404)
    else:
        res['message'] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response
731 732 733 734 735 736 737 738 739 740

def create_pair(request):
    """Create pair

    payload example:
    {
        "parent": {"id": 3075},
        "child": {"id": 3067}
    }
    """
741 742 743 744 745 746 747 748 749 750 751 752 753 754
    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 = [
755
                "birthdate",
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
                "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",
777
                "nb_associated_people",
Thibault Grandjean committed
778 779 780
                "current_template_name",
                "parent_id",
                "is_associated_people",
781 782 783 784
                "makeups_to_do",
                "final_standard_points",
                "final_ftop_points",
                "shift_type"
785 786
            ]
            child = api.search_read('res.partner', [['id', '=', child_id]], fields)[0]
787 788 789
            parent = api.search_read('res.partner', [['id', '=', parent_id]],
                                                    ['commercial_partner_id',
                                                     'nb_associated_people',
Thibault Grandjean committed
790
                                                     'current_template_name',
791
                                                     'makeups_to_do',
792 793 794
                                                     "final_standard_points",
                                                     "final_ftop_points",
                                                     'shift_type'
795 796 797 798 799 800 801 802
                                                     'parent_id'])[0]
            errors = []
            if child['nb_associated_people'] > 0:
                # le membre est déjà titulaire d'un binôme
                errors.append("Le membre suppléant sélectionné est titulaire d'un bînome")
            # 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é")
Thibault Grandjean committed
803 804 805 806 807 808
            # Verifier que le suppléant n'est pas déjà en binôme soit titulaire soit suppléant
            for m in api.search_read('res.partner', [['email', '=', child['email']]]):
                if m['is_associated_people']:
                    errors.append('Le membre suppléant est déjà en bînome')
                if m['child_ids']:
                    errors.append("Le membre suppléant sélectionné est titulaire d'un binôme")
809 810 811 812 813 814 815 816 817 818 819
            # le membre titulaire a déjà un/des suppléants?
            if parent['nb_associated_people'] >= 1:
                # On récupère le/s suppléant(s)
                associated_members = api.search_read('res.partner', [['parent_id', '=', parent_id]], ['id', 'age'])
                # le suppléant est un mineur?
                for m in associated_members:
                    if m['age'] > 18:
                        errors.append("Le membre titulaire sélectionné a déjà un suppléant")
            if errors:
                return JsonResponse({"errors": errors}, status=409)

820 821 822
            del child["id"]
            for field in child.keys():
                if field.endswith("_id"):
823 824 825 826
                    try:
                        child[field] = child[field][0]
                    except TypeError:
                        child[field] = False
827
            child['is_associated_people'] = True
Thibault Grandjean committed
828
            child['parent_id'] = parent['id']
829 830 831 832 833
            # 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'
834
            # fusion des rattrapages
835 836 837
            child_makeups = child['makeups_to_do']
            parent_makeups = parent['makeups_to_do']

838
            child_scheduled_makeups = api.search_read('shift.registration', [['partner_id', '=', child_id],
839
                                                                             ['is_makeup', '=', True],
840 841 842
                                                                             ['state', '=', 'open'],
                                                                             ['date_begin', '>', datetime.now().isoformat()]])
            parent_scheduled_makeups = api.search_read('shift.registration', [['partner_id', '=', parent_id],
843
                                                                             ['is_makeup', '=', True],
844 845
                                                                             ['state', '=', 'open'],
                                                                             ['date_begin', '>', datetime.now().isoformat()]])
846 847 848 849 850 851 852
            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
853

Yvon committed
854
                    # Comme on annule les services du suppléant, on ajoute en makeup_to_do du titulaire les rattrapages choisis ou non du suppléant
855
                    
856 857
                    # On annule les rattrapages du child
                    api.update('res.partner', [child_id], {"makeups_to_do": 0})
858
                    
859 860 861 862 863
                    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'],
864
                                                           "partner_id": child_id,
865
                                                           "point_qty": 1})
866
                        # on retire les points au titulaire, les rattrages sont automatiquement ajoutés
867 868 869
                        api.create('shift.counter.event', {"name": 'passage en binôme',
                                                           "shift_id": False,
                                                           "type": parent['shift_type'],
870
                                                           "partner_id": parent_id,
871 872 873 874 875 876 877 878 879
                                                           "point_qty": -1})
                elif child_makeups + parent_makeups > 2:
                    # on annule les rattrapages du suppléant et on met 2 rattrapages sur le titulaire
                    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'],
880
                                                           "partner_id": child_id,
881
                                                           "point_qty": 1})
882
                    for i in range(abs(parent_makeups - 2)):
883 884 885 886
                        # màj du compteur du titulaire
                        api.create('shift.counter.event', {"name": "passage en binôme",
                                                           "shift_id": False,
                                                           "type": parent['shift_type'],
887
                                                           "partner_id": parent_id,
888
                                                           "point_qty": -1})
889 890 891 892 893
                # No more useful since status is fully managed by lacagette_addons modules
                #try:
                #    api.execute('res.partner', 'run_process_target_status', [])
                #except:
                #    pass
894
            
895
            m = CagetteMember(child_id).unsubscribe_member()
896 897
            # update child base account state
            api.update("res.partner", [child_id], {'cooperative_state': "associated"})
898

899 900 901
            # get barcode rule id
            bbcode_rule = api.search_read("barcode.rule", [['for_associated_people', "=", True]], ['id'])[0]
            child['barcode_rule_id'] = bbcode_rule["id"]
902 903 904 905 906 907 908 909 910 911 912
            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
913 914 915 916 917 918 919
            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
920
    else:
921
        return JsonResponse({"message": "Method Not Allowed"}, status=405)
922 923 924 925


def delete_pair(request):
    """
926 927 928 929 930 931 932
    Administration des binômes membres
        Delete pair
    GET:
        Return template
    POST:
        payload example:
        {
933 934 935 936 937 938 939
          "child": {
            "id": "1620"
          },
          "gone": [
            "parent",
            "child"
          ]
940 941 942 943 944 945 946 947 948 949
        }
    """
    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())
950
            delete_pair_core(data)
951 952 953 954
            response = JsonResponse({"message": "Succesfuly unpaired members"}, status=200)
        else:
            response = JsonResponse({"message": "Unauthorized"}, status=403)
        return response
955
    else:
956
        return JsonResponse({"message": "Method Not Allowed"}, status=405)
957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987


def delete_pair_core(data):
    """
        Core of delete_pair
            argument example :
            {
              "child": {
                "id": "1620"
              },
              "gone": [
                "parent",
                "child"
              ]
            }
    """
    api = OdooAPI()
    child_id = int(data['child']['id'])
    child = api.search_read('res.partner', [['id', '=', child_id]], ['email', 'id', 'parent_id'])[0]
    child_accounts = api.search_read('res.partner', [['email', '=', child['email']]], ['id', 'email'])
    prev_child = [x['id'] for x in child_accounts if x['id'] != child_id]
    parent = api.search_read('res.partner', [['id', '=', child['parent_id'][0]]], ['cooperative_state'])[0]
    api.update('res.partner', [child_id], {"parent_id": False, "is_associated_people": False, "active": False,
                                           "is_former_associated_people": True})
    child_update_fields = {'cooperative_state': "unsubscribed", "is_former_associated_people": True}
    if 'gone' in data and 'child' in data['gone']:
        child_update_fields['cooperative_state'] = "gone"
    for id in prev_child:
        api.update("res.partner", [id], child_update_fields)
    if 'gone' in data and 'parent' in data['gone']:
        api.update("res.partner", [parent['id']], {'cooperative_state': "gone", "is_former_associated_people": True})