from django.contrib import admin
from outils.common_imports import *
from outils.for_view_imports import *
from outils.common import OdooAPI
from members.models import CagetteUser
from members.models import CagetteMembers
from members.models import CagetteMember
from shifts.models import CagetteServices
from shifts.models import CagetteShift
from members_space.models import CagetteMembersSpace
from outils.common import MConfig
from datetime import datetime, date
import shifts.fonctions
from .exceptions import MembersAppException

default_msettings = {'msg_accueil': {'title': 'Message borne accueil',
                                             'type': 'textarea',
                                             'value': '',
                                             'sort_order': 1
                                            },
                     'no_picture_member_advice': {'title': 'Message avertissement membre sans photo',
                                             'type': 'textarea',
                                             'value': '',
                                             'sort_order': 2
                                      },
                     'shop_opening_hours': {
                                                'title': 'Horaires ouverture magasin',
                                                'type': 'textarea',
                                                'value': '',
                                                'sort_order': 3
                                            },
                      'abcd_calendar_link': {
                                                'title': 'Lien vers le calendrier ABCD',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 4
                       },
                       'forms_link': {
                                                'title': 'Lien vers la page des formulaires',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 5
                       },
                       'unsuscribe_form_link': {
                                                'title': 'Lien vers le formulaire de ré-inscription',
                                                'type': 'text',
                                                'value': '',
                                                'class': 'link',
                                                'sort_order': 6
                       },
                       '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
                       },
                       '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
                       },

                    }

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
            for k,v in msettings.items():
                if 'sort_order' not in v:
                    msettings[k]['sort_order'] = 1
            result['settings'] = dict(sorted(msettings.items(), key=lambda k_v: k_v[1]['sort_order']))
            # on preprod server, dict order (through JsonResponse ??) is not respected !!
        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:
            fields = ['shift_type']
            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:
                    if m['shift_type'] == 'ftop':
                        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):
    """Only used from manage_mess, which was a tempory fonctionality"""
    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)
    return response

# # # ADMIN / BDM # # #

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

def manage_makeups(request):
    """ Administration des membres """
    template = loader.get_template('members/admin/manage_makeups.html')
    committees_shift_id = CagetteServices.get_committees_shift_id()
    m = CagetteMembersSpace()
    context = {'title': 'BDM - Rattrapages',
               'module': 'Membres',
               'has_committe_shift': committees_shift_id is not None
              }
    return HttpResponse(template.render(context, request))

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))

def manage_attached(request):
    """ Administration des binômes membres """
    template = loader.get_template('members/admin/manage_attached.html')
    context = {'title': 'BDM - Binômes',
               'module': 'Membres'}
    return HttpResponse(template.render(context, request))

def manage_regular_shifts(request):
    """ Administration des créneaux des membres """
    template = loader.get_template('members/admin/manage_regular_shifts.html')
    committees_shift_id = CagetteServices.get_committees_shift_id()
    committees_shift_name = getattr(settings, 'COMMITTEES_SHIFT_NAME', "service des Comités")
    if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
        exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
    else:
        exemptions_shift_id = 0
    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),
        'show_ftop_button': getattr(settings, 'BDM_SHOW_FTOP_BUTTON', True),
        'has_committe_shift': committees_shift_id is not None,
        'committees_shift_id': committees_shift_id,
        'committees_shift_name': committees_shift_name,
        'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
        'exemptions_shift_id': exemptions_shift_id,
    }
    return HttpResponse(template.render(context, request))

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

def get_attached_members(request):
    """ Récupération des membres en binôme """
    res = CagetteMembers.get_attached_members()
    return JsonResponse({ 'res' : res })

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

        # Perform checks and compute some data in a loop on members.
        cs = CagetteShift()
        api = OdooAPI()
        unsubscription_limit = int(api.get_system_param('lacagette_membership.points_limit_to_get_unsubscribed'))
        error = {}
        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"] = "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)
            points_target = cm.get_member_points("standard") - makeup_change_count
            if points_target <= unsubscription_limit:
                error["error"]\
                    = "L'ajout de rattrapage a été annulé car il provoquerait la désinscription d'un membre."
                return JsonResponse(error, status=400)
            res_exempted = shifts.fonctions.get_exempted_ids_from(api, [member_data["member_id"]])
            if res_exempted:
                error["error"] = "L'opération de rattrapage a été annulée car le membre ciblé est exempté."
                return JsonResponse(error, status=400)
            if cm.has_state_unsubscribed_gone_or_associated():
                error["error"] = ("L'opération de rattrapage a été annulée car le membre ciblé est désinscrit, parti ou"
                                  " associé.")
                return JsonResponse(error, status=400)
            # Save computed data in member_data. It will be useful to update makeups and points.
            member_data["points_diff"] = - makeup_change_count

        # Update makeups and points
        res["res"] = []
        update_members_makeups_core(members_data, res)
        response = JsonResponse(res)
    else:
        res["message"] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response


def update_std_member_makeups_to_do_and_points_given_points_target(member_id, points_target, description):
    cm = CagetteMember(member_id)
    cs = CagetteShift()
    points_diff = points_target - cm.get_member_points("standard")
    makeup_change_count = - points_diff
    target_makeups_nb = makeup_change_count + cs.get_member_makeups_to_do(cm.id)
    members_data = {
        'member_id': member_id,
        'points_diff': points_diff,
        'description': description,
        'member_shift_type': 'standard',
        'target_makeups_nb': target_makeups_nb
    }
    update_members_makeups_core([members_data])

def update_members_makeups_core(members_data, res = None):
    """Met à jour makeups_to_do et le nombre de points.
    members_data est une liste dont chaque élément doit contenir :
    - member_id
    - points_diff (positif : ajout de points)
    - description
    - member_shift_type
    - target_makeups_nb
    Si res est fourni, écrit dedans des données de retour des opérations ainsi que le nombre de points standard
    selon un format (à préciser).
    - """
    for member_data in members_data:
        cm = CagetteMember(int(member_data["member_id"]))

        # 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:
            update_makeups_res = cm.update_member_makeups(member_data)
            # No need to manually call status recompute after that because we update member point
            # (and this triggers status update in odoo).
            if res:
                res["res"].append(update_makeups_res)
        else:
            if res:
                res["res"].append({'mid': int(member_data["member_id"])})

        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)

        if res:
            res["res"][-1]['standard_points'] = cm.get_member_points("standard")

# --- Gestion des créneaux

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())
        shift_registration_id = int(data["shift_registration_id"])
        cancellation_description = data["cancellation_description"]

        # Note: 'upcoming_registration_count' in res.partner won't change because the _compute method
        #       in odoo counts canceled shift registrations.
        m = CagetteShift()
        res["cancel_shift"] = m.cancel_shift(
            [shift_registration_id],
            cancellation_origin='Bdm',
            cancellation_description=cancellation_description,
            # Display message for odoo in case cancelled shift is a makeup and there is point to add
            counter_event_name="Admin BDM (annulation de rattrapage par une annulation de présence) - " + cancellation_description
        )

        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"]

            # 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.

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

            if permanent_unsuscribe is True:
                res["set_done"] = cm.set_cooperative_state("gone")
                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', 'suppleant_member_id'])
                    for am in associated_members:
                        # 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)

        except Exception as e:
            res["error"] = str(e)

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

def shift_subscription(request):
    """ 
        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 SOME makeups (the ones that we can keep).
    """
    res = {}
    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:
        api = OdooAPI()
        partner_id = int(data["partner_id"])
        m = CagetteMember(partner_id)
        try:
            curated_data = analyse_shift_subscription_request(api, data, partner_id)
        except MembersAppException as e:
            return JsonResponse({"message": str(e)}, status=409)
        if curated_data["unsubscribe_first"]:
            if curated_data["moving_from_std_to_ftop"]:
                # Hard unsubscribe (unselecting all makeups)
                m.unsubscribe_member()
            else:
                # Unselect makeups on target shift_template
                # (they would prevent new subscription)
                cs = CagetteShift()
                makeup_reg_ids = curated_data["makeups_reg_on_target_shift_template"]
                if makeup_reg_ids:
                    cs.unselect_makeup(makeup_reg_ids)
                # Perform soft unsubscribe that only unselect
                # makeups of shift_template to cancel (and
                # not makeups that are not linked to it)
                m.unsubscribe_member_but_exogenous_makeups()
        reg_id = m.create_coop_shift_subscription(
            curated_data["target_shift_template_id"],
            data["shift_type"]
        )
        # Return necessary data
        if reg_id is not None:
            c = [['id', '=', curated_data["target_shift_template_id"]]]
            f = ['id', 'name']
            res["shift_template"] = api.search_read('shift.template', c, f)[0]
            c = [['id', '=', partner_id]]
            f = ['cooperative_state', 'makeups_to_do']
            m = api.search_read('res.partner', c, f)[0]
            res["cooperative_state"] = m['cooperative_state']
            res["makeups_to_do"] = m['makeups_to_do']
            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)
    else:
        response = JsonResponse({"message": "Unauthorized"}, status=403)
    return response


def analyse_shift_subscription_request(api, data, partner_id):
    cm = CagetteMember(partner_id)
    cs = CagetteServices()
    res = {}
    shift_type = data["shift_type"]
    res["moving_from_std_to_ftop"] = False
    if shift_type == 1:
        res["target_shift_type"] = "standard"
        shift_template_id = int(data["shift_template_id"])
    elif shift_type == 2:
        res["target_shift_type"] = "ftop"
        # First try to get committees shift
        shift_template_id = cs.get_committees_shift_id()
        # If None, no committees shift, get the first ftop shift
        if shift_template_id is None:
            shift_template_id = cs.get_first_ftop_shift_id()
        c = [['id', '=', partner_id]]
        f = ['shift_type']
        res["moving_from_std_to_ftop"] = api.search_read('res.partner', c, f)[0]['shift_type'] == 'standard'
    else:
        raise MembersAppException("Le service cible n'est ni standard ni ftop."
                                  " L'exemption via un service spécial n'est plus supportée par l'application.")
    res["target_shift_template_id"] = shift_template_id
    # Make sure unsubscribe_first data sent by js is still consistent with bdd
    if data["unsubscribe_first"] == cm.has_state_unsubscribed_gone_or_associated():
        raise MembersAppException("Les informations affichées sont périmées."
                                  " Veuillez recharger la page avant de poursuivre.")
    if data["unsubscribe_first"]:
        res["makeups_reg_on_target_shift_template"] = cm.get_makeup_registrations_ids_on_shift_template(
            res["target_shift_template_id"]
        )
    res["unsubscribe_first"] = data["unsubscribe_first"]
    return res

# --- Gestion des binômes

def get_member_info(request, id):
    """Retrieve information about a member."""
    res = {}
    is_connected_user = CagetteUser.are_credentials_ok(request)
    if is_connected_user:
        api = OdooAPI()
        fields = [
            'id',
            'name',
            'sex',
            'cooperative_state',
            'email',
            'street',
            'street2',
            'zip',
            'city',
            'current_template_name',
            'shift_type',
            'parent_id',
            'suppleant_member_id',
            'is_associated_people',
            'parent_name',
            "makeups_to_do",
            "barcode_base"
        ]
        cond = [['id', '=', id]]
        member = api.search_read('res.partner', cond, fields)
        if member:
            member = member[0]
            if member['parent_id']:
                if not member['suppleant_member_id']:
                    return JsonResponse({"message": "Accès non supporté (rattaché non suppléant)."}, status=404)
                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']
            res['member'] = member
            response = JsonResponse(res)
        else:
            response = JsonResponse({"message": "Not found"}, status=404)
    else:
        res['message'] = "Unauthorized"
        response = JsonResponse(res, status=403)
    return response

def create_pair(request):
    """Create pair

    payload example:
    {
        "parent": {"id": 3075},
        "child": {"id": 3067}
    }
    """
    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 = [
                "birthdate",
                "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",
                "nb_associated_people",
                "current_template_name",
                "parent_id",
                "suppleant_member_id",
                "is_associated_people",
                "makeups_to_do",
                "final_standard_points",
                "final_ftop_points",
                "shift_type"
            ]
            child = api.search_read('res.partner', [['id', '=', child_id]], fields)[0]
            parent = api.search_read('res.partner', [['id', '=', parent_id]],
                                                    ['commercial_partner_id',
                                                     'nb_associated_people',
                                                     'current_template_name',
                                                     'makeups_to_do',
                                                     "final_standard_points",
                                                     "final_ftop_points",
                                                     'shift_type',
                                                     'parent_id',
                                                     'suppleant_member_id'])[0]

            errors = CagetteMember.can_become_suppleant(child_id)
            if errors:
                return JsonResponse({"errors": errors}, status=409)

            errors = CagetteMember.can_become_titulaire(parent_id)
            if errors:
                return JsonResponse({"errors": errors}, status=409)

            del child["id"]
            for field in child.keys():
                if field.endswith("_id"):
                    try:
                        child[field] = child[field][0]
                    except TypeError:
                        child[field] = False
            child['is_associated_people'] = True
            child['parent_id'] = parent['id']
            #Link the newly created attached suppleant contact and the suppleant member
            child['suppleant_member_id'] = child_id
            # 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'

            # fusion des rattrapages
            child_makeups = child['makeups_to_do']
            parent_makeups = parent['makeups_to_do']

            if child_makeups < 0 or parent_makeups < 0:
                coop_logger.error(
                    "Erreur : le suppléant (%s) ou le titulaire (%s) a un compteur de rattrapages négatif.",
                    child_makeups,
                    parent_makeups
                )
                return JsonResponse(
                    {"message": "Erreur : le suppléant ou le titulaire a un compteur de rattrapages négatif."},
                    status=409
                )

            child_scheduled_makeups = shifts.fonctions.get_scheduled_makeups(api, partner_ids=[child_id])
            parent_scheduled_makeups_length = len(shifts.fonctions.get_scheduled_makeups(api, partner_ids=[parent_id]))

            child_makeups += len(child_scheduled_makeups)
            parent_makeups += parent_scheduled_makeups_length

            unsubscription_limit = int(api.get_system_param('lacagette_membership.points_limit_to_get_unsubscribed'))
            min_total_points = unsubscription_limit + 1
            max_total_makeups = - min_total_points

            # 1. First of all, set the proper final number of points / total makeups of titulaire :
            parent_points_target = max(min_total_points, - (child_makeups + parent_makeups))
            update_std_member_makeups_to_do_and_points_given_points_target(
                parent_id, parent_points_target, 'création de binôme (transfert vers titulaire)'
            )
            # 2.Then, try to transfer some scheduled makeups of the suppleant on the titulaire, when possible.
            # Note that this second step will maintain the same point count and the same total number of makeups.
            handle_suppleant_scheduled_makeups_on_pairing(parent_id, child, child_scheduled_makeups, parent,
                                                              parent_scheduled_makeups_length, max_total_makeups)
            # 3. Final step is to set to reset points of the child. This is easy as the child has no more scheduled makeups.
            update_std_member_makeups_to_do_and_points_given_points_target(
                child_id, 0, 'création de binôme (mise à zéro suppléant)'
            )

            CagetteMember(child_id).unsubscribe_member()

            # get barcode rule id
            bbcode_rule = api.search_read("barcode.rule", [['for_associated_people', "=", True]], ['id'])[0]
            child['barcode_rule_id'] = bbcode_rule["id"]
            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
            attached_account = api.create('res.partner', child)
            # generate_base
            api.execute('res.partner', 'generate_base', [attached_account])

            # update child base account state after contact is created otherwise odoo complains that
            # setting status associated is not allowed
            api.update("res.partner", [child_id], {'cooperative_state': "associated"})

            response = JsonResponse({"message": "Succesfuly paired members"}, status=200)
        else:
            response = JsonResponse({"message": "Unauthorized"}, status=403)
        return response
    else:
        return JsonResponse({"message": "Method Not Allowed"}, status=405)


def handle_suppleant_scheduled_makeups_on_pairing(parent_id, child, child_scheduled_makeups, parent, parent_scheduled_makeups_length, max_total_makeups):
    """Unselect scheduled makeups of suppleant, and, when possible, transfert them to the titulaire."""
    # If one of the two is not standard there is nothing to do
    if parent['shift_type'] == 'standard' and child["shift_type"] == 'standard':
        for child_scheduled_makeup in child_scheduled_makeups:
            cs = CagetteShift()
            cs.unselect_makeup(
                child_scheduled_makeup['id'],
                cancellation_origin="Admin Bdm",
                cancellation_description="Désélection d'un rattrapage du suppléant à la création d'un binôme"
            )
            # If there is room for additional scheduled makeup on the titulaire, subscribe to the makeup !
            if parent_scheduled_makeups_length < max_total_makeups:
                data = {
                    'idPartner': parent_id,
                    'idShift': child_scheduled_makeup['shift_id'][0],
                    'shift_type': 'standard',
                    'is_makeup': True,
                    'origin': 'Transfert d\'un rattrapage sur le titulaire à la création de binôme'
                }
                cs.set_shift(data) # Automaticaly decrements makeups_to_do to preserve total count.
                parent_scheduled_makeups_length = parent_scheduled_makeups_length + 1

def delete_pair(request):
    """
    Administration des binômes membres
        Delete pair
    GET:
        Return template
    POST:
        payload example:
        {
          "child": {
            "id": "1620"
          },
          "gone": [
            "parent",
            "child"
          ]
        }
    """
    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())
            delete_pair_core(data)
            response = JsonResponse({"message": "Succesfuly unpaired members"}, status=200)
        else:
            response = JsonResponse({"message": "Unauthorized"}, status=403)
        return response
    else:
        return JsonResponse({"message": "Method Not Allowed"}, status=405)


def delete_pair_core(data):
    """
        Core of delete_pair
            argument example :
            {
              "child": {
                "id": "1620"
              },
              "gone": [
                "parent",
                "child"
              ]
            }
    """
    api = OdooAPI()
    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,
                                           "is_former_associated_people": True, "suppleant_member_id": False})
    child_member_update_fields = {'cooperative_state': "unsubscribed", "is_former_associated_people": True}
    if 'gone' in data and 'child' in data['gone']:
        child_member_update_fields['cooperative_state'] = "gone"
    api.update("res.partner", [child_contact['suppleant_member_id']], child_member_update_fields)
    if 'gone' in data and 'parent' in data['gone']:
        cm = CagetteMember(parent['id'])
        cm.unsubscribe_member()
        api.update("res.partner", [parent['id']], {'cooperative_state': "gone", "is_former_associated_people": True})
