from django.db import models
from outils.common_imports import *

from outils.common import OdooAPI
from outils.common import Verification
from members.models import CagetteMember
from members.models import CagetteUser
import shifts.fonctions

from pytz import timezone

import locale
import re

import dateutil.parser

tz = pytz.timezone("Europe/Paris")

class CagetteShift(models.Model):
    """Class to handle cagette Odoo Shift."""

    def __init__(self):
        """Init with odoo id."""
        self.tz = pytz.timezone("Europe/Paris")
        self.o_api = OdooAPI()

    def get_cycle_week_data(self, date=None):
        result = {}
        try:
            res_param = self.o_api.search_read('ir.config_parameter',
                                               [['key', '=', 'coop_shift.week_a_date']],
                                               ['value'])
            if res_param:
                import math
                WEEKS = ['A', 'B', 'C', 'D']
                start_A = tz.localize(datetime.datetime.strptime(res_param[0]['value'], '%Y-%m-%d'))
                result['start'] = start_A
                now = datetime.datetime.now(tz)  # + datetime.timedelta(hours=72)
                if date is not None:
                    now = tz.localize(datetime.datetime.strptime(date, '%Y-%m-%d'))
                diff = now - start_A
                weeks_diff = diff.total_seconds() / 3600 / 24 / 7
                week_index = math.floor(weeks_diff % 4)
                result['week_name'] = WEEKS[week_index]
                result['start_date'] = start_A + datetime.timedelta(weeks=math.floor(weeks_diff))

        except Exception as e:
            coop_logger.error("get_current_cycle_week_data %s", str(e))
            result['error'] = str(e)
        return result

    def is_matching_ftop_rules(self, partner_id, idNewShift, idOldShift=0):
        answer = True
        rules = getattr(settings, 'FTOP_SERVICES_RULES', {})
        if ("successive_shifts_allowed" in rules
             or
            "max_shifts_per_cycle" in rules
            ):
            try:
                now = datetime.datetime.now(tz)
                # Have to retrive shifts (from now to a cycle period forward to check rules respect)
                [shift_registrations, is_ftop] = shifts.fonctions.get_shift_partner(self.o_api, partner_id, now + datetime.timedelta(weeks=4))
                new_shift = self.get_shift(idNewShift)  # WARNING : use date_begin_tz while shift_registrations use date_begin (UTC)
                if "successive_shifts_allowed" in rules:
                    min_duration = getattr(settings, 'MIN_SHIFT_DURATION', 2)
                    for sr in shift_registrations:
                        if int(sr['shift_id'][0]) != int(idOldShift):
                            diff = (datetime.datetime.strptime(sr['date_begin'], '%Y-%m-%d %H:%M:%S').astimezone(tz)
                                    - tz.localize(datetime.datetime.strptime(new_shift['date_begin_tz'], '%Y-%m-%d %H:%M:%S')))
                            if abs(diff.total_seconds() / 3600) < (min_duration * 2) * (int(rules['successive_shifts_allowed']) + 1):
                                answer = False
                            #  coop_logger.info(sr['date_begin'] + ' - ' + new_shift['date_begin_tz'])
                            #  coop_logger.info(str(diff.total_seconds()/3600) + 'h')
                if "max_shifts_per_cycle" in rules:
                    [ymd, hms] = new_shift['date_begin_tz'].split(" ")
                    cw = self.get_cycle_week_data(ymd)
                    if 'start_date' in cw:
                        sd = cw['start_date']
                        ed = cw['start_date'] + datetime.timedelta(weeks=4)
                        [cycle_shift_regs, is_ftop] = shifts.fonctions.get_shift_partner(self.o_api, partner_id, start_date=sd, end_date=ed)
                        if len(cycle_shift_regs) >= int(rules['max_shifts_per_cycle']):
                            answer = False
                            coop_logger.info("services max par cycle atteint pour partner_id %s", str(partner_id))
            except Exception as e:
                coop_logger.error("is_shift_exchange_allowed %s %s", str(e), str(new_shift))
        return answer

    def is_shift_exchange_allowed(self, idOldShift, idNewShift, shift_type, partner_id):
        answer = True
        min_delay = getattr(settings, 'STANDARD_BLOCK_SERVICE_EXCHANGE_DELAY', 0)
        if shift_type == "ftop":
            min_delay = getattr(settings, 'FTOP_BLOCK_SERVICE_EXCHANGE_DELAY', 0)
        if min_delay > 0:
            now = datetime.datetime.now(tz)
            old_shift = self.get_shift(idOldShift)
            day_before_old_shift_date_start = \
                tz.localize(datetime.datetime.strptime(old_shift['date_begin_tz'], '%Y-%m-%d %H:%M:%S')
                            - datetime.timedelta(hours=min_delay))

            if now > day_before_old_shift_date_start:
                answer = False
            elif shift_type == "ftop":
                answer = self.is_matching_ftop_rules(partner_id, idNewShift, idOldShift)


        return answer

    def get_shift(self, id):
        """Get one shift by id"""
        cond = [['id', '=', id]]

        fields = ['date_begin_tz']
        listService = self.o_api.search_read('shift.shift', cond, fields)

        try:
            return listService[0]
        except Exception as e:
            coop_logger.error("get_shift %s", str(e))
            return None

    def is_matching_ftop_rules(self, partner_id, idNewShift, idOldShift=0):
        answer = True
        rules = getattr(settings, 'FTOP_SERVICES_RULES', {})
        if ("successive_shifts_allowed" in rules
             or
            "max_shifts_per_cycle" in rules
            ):
            try:
                now = datetime.datetime.now(tz)
                # Have to retrive shifts (from now to a cycle period forward to check rules respect)
                [shift_registrations, is_ftop] = shifts.fonctions.get_shift_partner(self.o_api, partner_id, now + datetime.timedelta(weeks=4))
                new_shift = self.get_shift(idNewShift)  # WARNING : use date_begin_tz while shift_registrations use date_begin (UTC)
                if "successive_shifts_allowed" in rules:
                    min_duration = getattr(settings, 'MIN_SHIFT_DURATION', 2)
                    for sr in shift_registrations:
                        if int(sr['shift_id'][0]) != int(idOldShift):
                            diff = (datetime.datetime.strptime(sr['date_begin'], '%Y-%m-%d %H:%M:%S').astimezone(tz)
                                    - tz.localize(datetime.datetime.strptime(new_shift['date_begin_tz'], '%Y-%m-%d %H:%M:%S')))
                            if abs(diff.total_seconds() / 3600) < (min_duration * 2) * (int(rules['successive_shifts_allowed']) + 1):
                                answer = False
                            #  coop_logger.info(sr['date_begin'] + ' - ' + new_shift['date_begin_tz'])
                            #  coop_logger.info(str(diff.total_seconds()/3600) + 'h')
                if "max_shifts_per_cycle" in rules:
                    [ymd, hms] = new_shift['date_begin_tz'].split(" ")
                    cw = self.get_cycle_week_data(ymd)
                    if 'start_date' in cw:
                        sd = cw['start_date']
                        ed = cw['start_date'] + datetime.timedelta(weeks=4)
                        [cycle_shift_regs, is_ftop] = shifts.fonctions.get_shift_partner(self.o_api, partner_id, start_date=sd, end_date=ed)
                        if len(cycle_shift_regs) >= int(rules['max_shifts_per_cycle']):
                            answer = False
                            coop_logger.info("services max par cycle atteint pour partner_id %s", str(partner_id))
            except Exception as e:
                coop_logger.error("is_shift_exchange_allowed %s %s", str(e), str(new_shift))
        return answer

    def is_shift_exchange_allowed(self, idOldShift, idNewShift, shift_type, partner_id):
        answer = True
        min_delay = getattr(settings, 'STANDARD_BLOCK_SERVICE_EXCHANGE_DELAY', 0)
        if shift_type == "ftop":
            min_delay = getattr(settings, 'FTOP_BLOCK_SERVICE_EXCHANGE_DELAY', 0)
        if min_delay > 0:
            now = datetime.datetime.now(tz)
            old_shift = self.get_shift(idOldShift)
            day_before_old_shift_date_start = \
                tz.localize(datetime.datetime.strptime(old_shift['date_begin_tz'], '%Y-%m-%d %H:%M:%S')
                            - datetime.timedelta(hours=min_delay))

            if now > day_before_old_shift_date_start:
                answer = False
            elif shift_type == "ftop":
                answer = self.is_matching_ftop_rules(partner_id, idNewShift, idOldShift)


        return answer

    def get_data_partner(self, id):
        """Retrieve partner data useful to make decision about shift options"""
        cond = [['id', '=', id]]
        fields = ['display_name', 'display_std_points',
                  'shift_type', 'date_alert_stop', 'date_delay_stop', 'extension_ids',
                  'cooperative_state', 'final_standard_point', 'create_date',
                  'final_ftop_point', 'shift_type', 'leave_ids', 'makeups_to_do', 'barcode_base',
                  'street', 'street2', 'zip', 'city', 'mobile', 'phone', 'function', 'email',
                  'is_associated_people', 'parent_id', 'suppleant_member_id', 'extra_shift_done']
        partnerData = self.o_api.search_read('res.partner', cond, fields, 1)
        if partnerData:
            partnerData = partnerData[0]
            if partnerData['suppleant_member_id']:
                cond = [['id', '=', partnerData['parent_id'][0]]]
                fields = ['create_date', 'makeups_to_do', 'date_delay_stop', 'extra_shift_done']
                parentData = self.o_api.search_read('res.partner', cond, fields, 1)
                if parentData:
                    partnerData['parent_create_date'] = parentData[0]['create_date']
                    partnerData['parent_makeups_to_do'] = parentData[0]['makeups_to_do']
                    partnerData['parent_date_delay_stop'] = parentData[0]['date_delay_stop']
                    partnerData['parent_extra_shift_done'] = parentData[0]['extra_shift_done']

            if partnerData['shift_type'] == 'standard':
                partnerData['in_ftop_team'] = False
                #  Because 'in_ftop_team' doesn't seem to be reset to False in Odoo
            if partnerData['suppleant_member_id']:
                cond = [['partner_id.id', '=', partnerData['parent_id'][0]]]
            else:
                cond = [['partner_id.id', '=', id]]
            fields = ['shift_template_id', 'is_current']
            shiftTemplate = self.o_api.search_read('shift.template.registration', cond, fields)
            if (shiftTemplate and len(shiftTemplate) > 0):
                s_t_id = None
                for s_t in shiftTemplate:
                    if s_t['is_current'] is True:
                        s_t_id = s_t['shift_template_id'][0]
                if not (s_t_id is None):
                    cond = [['shift_template_id.id', '=', int(s_t_id)],
                            ['date_begin_tz', '>', datetime.datetime.now().isoformat()]]
                    fields = ['date_begin_tz', 'name']
                    nextShifts = self.o_api.search_read('shift.shift', cond, fields, 1)
                    if nextShifts:
                        (d, h) = nextShifts[0]['date_begin_tz'].split(' ')
                        partnerData['next_regular_shift_date'] = d
                        partnerData['regular_shift_name'] = nextShifts[0]['name']
            partnerData['is_leave'] = False
            if len(partnerData['leave_ids']) > 0:
                # Is member in active leave period
                now = datetime.datetime.now().isoformat()
                cond = [['id', 'in', partnerData['leave_ids']],
                        ['start_date', '<', now],
                        ['stop_date', '>', now], ['state', '!=', 'cancel']]
                fields = ['start_date', 'stop_date', 'type_id', 'state']
                res_leaves = self.o_api.search_read('shift.leave', cond, fields)

                if res_leaves and len(res_leaves) > 0:
                    # TODO : Consider > 1 results
                    partnerData['is_leave'] = True
                    partnerData["leave_start_date"] = res_leaves[0]["start_date"]
                    partnerData["leave_stop_date"] = res_leaves[0]["stop_date"]

        return partnerData


    def shift_is_makeup(self, id):
        """vérifie si une shift est un rattrapage"""
        fields = ["is_makeup", "id"] 
        cond = [['id', '=', id]]
        shiftData = self.o_api.search_read('shift.registration', cond, fields)
        return shiftData[0]["is_makeup"]


    def get_shift_calendar(self, is_ftop, start, end):
        """Récupère les shifts à partir de maintenant pour le calendier"""
        max_weeks_ahead = getattr(settings,'FTOP_SHIFTS_VIEW_LIMIT', None)
        cond = [['date_begin', '>', datetime.datetime.now().isoformat()],
                ['state', '!=', 'cancel']]
        try:
            start_d = datetime.datetime.strptime(start, '%Y-%m-%d')
            cond.append(['date_begin', '>=', start_d.isoformat()])
        except:
            pass
        try:
            end_d = datetime.datetime.strptime(end, '%Y-%m-%d')
            cond.append(['date_end', '<=', end_d.isoformat()])
        except:
            pass
        if max_weeks_ahead and is_ftop:
            max_end = datetime.datetime.now() + datetime.timedelta(weeks=max_weeks_ahead)
            cond.append(['date_end', '<=', max_end.isoformat()])

        # 2018-11-25 seats_available instead of seats_max
        fields = ['date_begin_tz',
                  'date_end_tz', 'name',
                  'shift_template_id',
                  'event_type_id', 'seats_reserved',
                  'seats_available', 'registration_ids', 'address_id', 'shift_type_id']
        listService = self.o_api.search_read('shift.shift', cond, fields)

        return listService

    def get_leave(self, idPartner):
        """Récupération des congés en cours du membre"""
        now = datetime.datetime.now().isoformat()
        cond = [['partner_id', '=', idPartner], ['start_date', '<', now], ['stop_date', '>', now]]
        fields = ['stop_date', 'id', 'start_date']
        return self.o_api.search_read('shift.leave', cond, fields)

    def get_shift_ticket(self,idShift, shift_type):
        """Récupérer le shift_ticket suivant le membre et flotant ou pas"""
        if getattr(settings, 'USE_STANDARD_SHIFT', True) == False:
            shift_type = "ftop"
        fields = ['shift_ticket_ids']
        cond = [['id', "=", idShift]]
        listeTicket = self.o_api.search_read('shift.shift', cond, fields)
        if shift_type == "ftop":
            return listeTicket[0]['shift_ticket_ids'][1]
        else:
            return listeTicket[0]['shift_ticket_ids'][0]

    def set_shift(self, data):
        """Shift registration"""
        st_r_id = False
        try:
            shift_type = "standard"
            if data['shift_type'] == "ftop" or getattr(settings, 'USE_STANDARD_SHIFT', True) == False:
                shift_type = "ftop"
            fieldsDatas = { "partner_id": data['idPartner'],
                            "shift_id": data['idShift'],
                            "shift_ticket_id": self.get_shift_ticket(data['idShift'], data['shift_type']),
                            "shift_type": shift_type,  
                            "origin": 'memberspace',
                            "is_makeup": data['is_makeup'],
                            "state": 'open'}
            if (shift_type == "standard" and data['is_makeup'] is not True) or shift_type == "ftop":
                fieldsDatas['template_created'] = 1  # It's not true but otherwise, presence add 1 standard point , which is not wanted

            st_r_id = self.o_api.create('shift.registration', fieldsDatas)
        except Exception as e:
            coop_logger.error("Set shift : %s, %s", str(e), str(data))
            if 'This partner is already registered on this Shift' in str(e) or 'sql_constraint' in str(e):
                res = self.reopen_shift(data)
                if res:
                    st_r_id = True
        return st_r_id
    
    def affect_shift(self, data):
        """Affect shift to partner, his associate or both"""
        response = None
        # partner_id can be 'associated_people' one, which is never use as shift partner_id reference
        # So, let's first retrieved data about the res.partner involved
        cond = [['id', '=', int(data['idPartner'])]]
        fields = ['parent_id', 'suppleant_member_id']
        partner = self.o_api.search_read('res.partner', cond, fields, 1)
        if partner:
            if partner[0]['suppleant_member_id']:
                partner_id = partner[0]['parent_id'][0]
            else:
                partner_id = int(data['idPartner'])
            cond = [['partner_id', '=', partner_id],
                    ['id', '=', int(data['idShiftRegistration'])]]
            fields = ['id']
            try:
                # make sure there is coherence between shift.registration id and partner_id (to avoid forged request)
                shit_to_affect = self.o_api.search_read('shift.registration', cond, fields, 1)
                if (len(shit_to_affect) == 1):
                    shift_res = shit_to_affect[0]
                    fieldsDatas = { "associate_registered":data['affected_partner']}
                    response = self.o_api.update('shift.registration', [shift_res['id']],  fieldsDatas)
            except Exception as e:
                coop_logger.error("Model affect shift : %s", str(e))
        else:
            coop_logger.error("Model affect shift nobody found : %s", str(cond))
        return response

    def cancel_shift(self, idsRegisteur, origin='memberspace', description=''):
        """Annule un shift"""
        fieldsDatas = { "related_shift_state": 'cancel',
                        "origin": origin,
                        "state": 'cancel',
                        "cancellation_description": description}
        return self.o_api.update('shift.registration', idsRegisteur,  fieldsDatas)

    def reopen_shift(self, data):
        """Use when a member select a shift he has canceled before"""
        response = None
        cond = [['partner_id', '=', int(data['idPartner'])],
                ['shift_id', '=', int(data['idShift'])],
                ['state', '=', 'cancel']]
        fields = ['id','origin']
        try:
            canceled_res = self.o_api.search_read('shift.registration', cond, fields, 1)
            if (len(canceled_res) == 1):
                shift_res = canceled_res[0]
                fieldsDatas = { "related_shift_state":'open',
                                "state": 'open',
                                "is_makeup":data['is_makeup'],
                                "origin":canceled_res[0]['origin'] + ' reopened from memberspace'}

                #following code is required to properly set template_created.
                #TODO : factor with set_shift code
                shift_type = "standard"
                if data['shift_type'] == "ftop" or getattr(settings, 'USE_STANDARD_SHIFT', True) == False:
                    shift_type = "ftop"
                if (shift_type == "standard" and data['is_makeup'] is not True) or shift_type == "ftop":
                    fieldsDatas['template_created'] = 1  # It's not true but otherwise, presence add 1 standard point , which is not wanted
                else: #the else does not exist in set_shift but is mandatory here as template_created value in reopened registration can be any value
                    fieldsDatas["template_created"] = False

                response = self.o_api.update('shift.registration', [shift_res['id']],  fieldsDatas)
        except Exception as e:
            coop_logger.error("Reopen shift : %s", str(e))
        return response

    def create_delay(self, data, duration=28, ext_name="Extension créée depuis l'espace membre"):
        """
        Create a delay for a member.
        If no duration is specified, a delay is by default 28 days from the given start_date.

        If the partner already has a current extension: extend it by [duration] days.
        Else, create a 28 days delay.

        Args:
            data
                idPartner: int
                start_date: string date at iso format (eg. "2019-11-19")
                    Date from which the delay end date is calculated
                (optionnal) extension_beginning: string date at iso format
                    If specified, will be the actual starting date of the extension.
                    Should be inferior than start_date.
                    (at creation only: odoo ignores delays if today's not inside)
            duration: nb of days
            ext_name: will be displayed in odoo extensions list
        """
        action = 'create'

        # Get partner extension ids
        cond = [['id', '=', data['idPartner']]]
        fields = ['extension_ids']
        partner_extensions = self.o_api.search_read('res.partner', cond, fields)
        response = False
        # If has extensions
        if 'extension_ids' in partner_extensions[0]:
            # Look for current extension: started before today and ends after
            current_extension = False
            for ext_id in partner_extensions[0]['extension_ids']:
                cond = [['id','=',ext_id]]
                extension = self.o_api.search_read('shift.extension', cond)
                extension = extension[0]

                if datetime.datetime.strptime(extension['date_start'], '%Y-%m-%d') <= datetime.datetime.now() and\
                        datetime.datetime.strptime(extension['date_stop'], '%Y-%m-%d') > datetime.datetime.now():
                    current_extension = extension
                    break

            # Has a current extension -> Update it
            if current_extension != False:
                action = 'update'

        # Update current extension
        if action == 'update':
            ext_date_stop = datetime.datetime.strptime(extension['date_stop'], '%Y-%m-%d').date()
            ext_new_date_stop = (ext_date_stop + datetime.timedelta(days=duration))

            update_data = {
                'date_stop': ext_new_date_stop.isoformat()
            }

            response = self.o_api.update('shift.extension', current_extension['id'],  update_data)
        # Create the extension
        else:
            # Get the 'Extension' type id
            extension_types = self.o_api.search_read('shift.extension.type')
            ext_type_id = getattr(settings, 'EXTENSION_TYPE_ID', 1)
            for val in extension_types:
                if val['name'] == 'Extension':
                    ext_type_id = val['id']
            
            starting_date = datetime.datetime.strptime(data['start_date'], '%Y-%m-%d').date()
            ending_date = (starting_date + datetime.timedelta(days=duration))

            if 'extension_beginning' in data:
                starting_date = datetime.datetime.strptime(data['extension_beginning'], '%Y-%m-%d').date()

            fields= {
                "partner_id":   data['idPartner'],
                "type_id":      ext_type_id,
                "date_start":   starting_date.isoformat(),
                "date_stop":    ending_date.isoformat(),
                "name":         ext_name
            }
            
            response = self.o_api.create('shift.extension', fields)

        return response

    @staticmethod
    def reset_members_positive_points():
        """
        Look for all the members with standard points > 0 when registered for more than a month and reset them to 0
        -> As an intern rule, members can't have more than 0 standard point (except during the first month)

        --- Called by a cron script
        """
        api = OdooAPI()

        # Get concerned members id and points
        lastmonth = (datetime.date.today() - datetime.timedelta(days=28)).isoformat()

        cond = [['is_member', '=', True],
                ['final_standard_point', '>', 0],
                ['create_date', '<', lastmonth]]
        fields = ['id', 'final_standard_point']
        members_data = api.search_read('res.partner', cond, fields)

        # For each, set points to 0
        res = True
        for member_data in members_data:
            try:
                fields = {
                    'name': 'RAZ des points positifs',
                    'shift_id': False,
                    'type': 'standard',
                    'partner_id': member_data['id'],
                    'point_qty': -int(member_data['final_standard_point'])
                }

                api.create('shift.counter.event', fields)
            except:
                res = False

        return res

    def get_test(self, odooModel, cond, fieldsDatas):
        return self.o_api.search_read(odooModel, cond, fieldsDatas, limit = 1000)

    def get_member_makeups_to_do(self, partner_id):
        cond = [['id', '=', partner_id]]
        fields = ['makeups_to_do']
        return self.o_api.search_read('res.partner', cond, fields)[0]["makeups_to_do"]

    def decrement_makeups_to_do(self, partner_id):
        """ Decrements partners makeups to do if > 0 """
        makeups_to_do = self.get_member_makeups_to_do(partner_id)

        if makeups_to_do > 0:
            makeups_to_do -= 1
            f = { "makeups_to_do": makeups_to_do }

            return self.o_api.update('res.partner', [partner_id], f)
        else:
            return "makeups already at 0"

    def member_can_have_delay(self, partner_id):
        """ Can a member have a delay? """
        answer = False
        try:
            answer = self.o_api.execute('res.partner', 'can_have_extension', [partner_id])
        except Exception as e:
            coop_logger.error("member_can_have_delay : %s", str(e))
        return answer

    def update_counter_event(self, fields):
        """ Add/remove points """
        return self.o_api.create('shift.counter.event', fields)


class CagetteServices(models.Model):
    """Class to handle cagette Odoo services."""

    @staticmethod
    def get_all_shift_templates():
        """Return all recorded shift templates recorded in Odoo database."""
        creneaux = {}
        try:
            api = OdooAPI()
            f = ['name', 'week_number', 'start_datetime_tz', 'end_datetime_tz',
                 'seats_reserved', 'shift_type_id', 'seats_max',
                 'seats_available','registration_qty']
            c = [['active', '=', True]]
            shift_templates = api.search_read('shift.template', c, f)

            # Get count of active registrations for each shift template
            # shift_templates_active_count = api.execute('lacagette_shifts', 'get_active_shifts', [])
            # With LGDS tests, seats_reserved reflects better what's shown in Odoo ...

            title = re.compile(r"^(\w{1})(\w{2,3})\.? - (\d{2}:\d{2}) ?-? ?(\w*)")
            for l in shift_templates:
                line = {}
                end = time.strptime(l['end_datetime_tz'], "%Y-%m-%d %H:%M:%S")
                end_min = str(end.tm_min)
                if end_min == '0':
                    end_min = '00'
                line['end'] = str(end.tm_hour) + ':' + end_min
                line['max'] = l['seats_max']
                line['reserved'] = l['registration_qty']
                line['week'] = l['week_number']
                line['id'] = l['id']
                line['type'] = l['shift_type_id'][0]
                line['name'] = l['name']
                t_elts = title.search(l['name'])
                if t_elts:
                    line['day'] = t_elts.group(2)
                    line['begin'] = t_elts.group(3)
                    line['place'] = t_elts.group(4)

                creneaux[str(l['id'])] = {'data': line}
        except Exception as e:
            coop_logger.error(str(e))
        return creneaux

    @staticmethod
    def get_shift_templates_next_shift(id):
        """Retrieve next shift template shift."""
        api = OdooAPI()
        c = [['shift_template_id.id', '=', id],
             ['date_begin', '>=', datetime.datetime.now().isoformat()]]
        f = ['date_begin']
        # c = [['id','=',2149]]
        shift = {}
        res = api.search_read('shift.shift', c, f, 1, 0, 'date_begin ASC')
        if (res and res[0]):
            locale.setlocale(locale.LC_ALL, 'fr_FR.utf8')
            local_tz = pytz.timezone('Europe/Paris')
            date, t = res[0]['date_begin'].split(' ')
            year, month, day = date.split('-')
            start = datetime.datetime(int(year), int(month), int(day),
                                      0, 0, 0, tzinfo=pytz.utc)
            start_date = start.astimezone(local_tz)
            shift['date_begin'] = start_date.strftime("%A %d %B %Y")
        return shift

    @staticmethod
    def get_services_at_time(time, tz_offset, with_members=True):
        """Retrieve present services with members linked."""

        default_acceptable_minutes_after_shift_begins = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_AFTER_SHIFT_BEGINS', 15)
        minutes_before_shift_starts_delay = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_BEFORE_SHIFT', 15)
        minutes_after_shift_starts_delay = default_acceptable_minutes_after_shift_begins
        late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
        max_duration = getattr(settings, 'MAX_DURATION', 180)
        if late_mode is True:
            minutes_after_shift_starts_delay = getattr(settings, 'ENTRANCE_VALIDATION_GRACE_DELAY', 60)
        api = OdooAPI()
        now = dateutil.parser.parse(time) - datetime.timedelta(minutes=tz_offset)
        start1 = now + datetime.timedelta(minutes=minutes_before_shift_starts_delay)
        start2 = now - datetime.timedelta(minutes=minutes_after_shift_starts_delay)
        end = start1 + datetime.timedelta(minutes=max_duration)
        cond = [['date_end_tz', '<=', end.isoformat()]]
        cond.append('|')
        cond.append(['date_begin_tz', '>=', start1.isoformat()])
        cond.append(['date_begin_tz', '>=', start2.isoformat()])
        fields = ['name', 'week_number', 'registration_ids',
                  'standard_registration_ids',
                  'shift_template_id', 'shift_ticket_ids',
                  'date_begin_tz', 'date_end_tz', 'state']
        services = api.search_read('shift.shift', cond, fields,order ="date_begin_tz ASC")
        for s in services:
            if (len(s['registration_ids']) > 0):
                if late_mode is True:
                    s['late'] = (
                                 now.replace(tzinfo=None)
                                 -
                                 dateutil.parser.parse(s['date_begin_tz']).replace(tzinfo=None)
                                 ).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins
                if with_members is True:
                    cond = [['id', 'in', s['registration_ids']], ['state', 'not in', ['cancel', 'waiting', 'draft']]]
                    fields = ['partner_id', 'shift_type', 'state', 'is_late', 'associate_registered']
                    members = api.search_read('shift.registration', cond, fields)
                    s['members'] = sorted(members, key=lambda x: x['partner_id'][0])
                    if len(s['members']) > 0:
                        # search for associated people linked to these members
                        mids = []
                        for m in s['members']:
                            mids.append(m['partner_id'][0])
                        cond = [['parent_id', 'in', mids]]
                        fields = ['id', 'parent_id', 'name', 'barcode_base', 'suppleant_member_id']
                        attached = api.search_read('res.partner', cond, fields)
                        associated = [x for x in attached if x['suppleant_member_id']]

                        if len(associated) > 0:
                            for m in s['members']:
                                for a in associated:
                                    if int(a['parent_id'][0]) == int(m['partner_id'][0]):
                                        m['partner_name'] = m['partner_id'][1]
                                        m['partner_id'][1] += ' en binôme avec ' + a['name']
                                        m['associate_name'] = str(a['barcode_base']) + ' - ' + a['name']
                                        

        return services

    @staticmethod
    def registration_done(request, registration_id, overrided_date="", typeAction=""):
        """Equivalent to click present in presence form."""
        has_right_to_overriden_entrance_date = Verification.has_right_to_overriden_entrance_date(request)

        if (len(overrided_date) == 0 or has_right_to_overriden_entrance_date):
            api = OdooAPI()
            f = {'state': 'done'}

            if(typeAction != "normal" and typeAction != ""):
                f['associate_registered'] = typeAction

            if typeAction == "both":
                f['should_increment_extra_shift_done'] = True
            else:
                f['should_increment_extra_shift_done'] = False
                
            late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
            if late_mode is True:
                # services = CagetteServices.get_services_at_time('14:28',0, with_members=False)
                if len(overrided_date) > 0 and getattr(settings, 'APP_ENV', "prod") != "prod":
                    now = overrided_date
                else:
                    local_tz = pytz.timezone('Europe/Paris')
                    now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(local_tz).strftime("%H:%MZ")
                # coop_logger.info("Maintenant = %s (overrided %s) %s", now, overrided_date)
                services = CagetteServices.get_services_at_time(now, 0, with_members=False)
                if len(services) > 0:
                    # Notice : Despite is_late is defined as boolean in Odoo, 0 or 1 is needed for api call
                    is_late = 0
                    if services[0]['late'] is True:
                        is_late = 1
                    f['is_late'] = is_late
                else:
                    return False
            return api.update('shift.registration', [int(registration_id)], f)
        else:
            return False

    @staticmethod
    def reopen_registration(registration_id, overrided_date=""):
        api = OdooAPI()
        f = {'state': 'open'}
        try:
            cond = [['id', '=', int(registration_id)]]
            fields = ['partner_id']
            res = api.search_read('shift.registration', cond, fields)
            coop_logger.info("On invalide la présence de  %s ", res[0]['partner_id'][1])
        except Exception as e:
            coop_logger.error("On invalide shift_registration  %s (erreur : %s)", str(registration_id), str(e))
        return api.update('shift.registration', [int(registration_id)], f)

    @staticmethod
    def record_rattrapage(mid, sid, stid, typeAction):
        """Add a shift registration for member mid.

        (shift sid, shift ticket stid)
        Once created, shift presence is confirmed.
        """
        api = OdooAPI()
        fields = {
            "partner_id": mid,
            "shift_id": sid,
            "shift_ticket_id": stid,
            "shift_type": "standard",  # ou ftop -> voir condition
            "related_shift_state": 'confirm',
            "state": 'open'}
        reg_id = api.create('shift.registration', fields)

        f = {'state': 'done'}
        if(typeAction != "normal" and typeAction != ""):
            f['associate_registered'] = typeAction
        if typeAction == "both":
            f['should_increment_extra_shift_done'] = True
        else:
            f['should_increment_extra_shift_done'] = False

        return api.update('shift.registration', [int(reg_id)], f)

    @staticmethod
    def record_absences(date):
        """Called by cron script."""
        import dateutil.parser
        if len(date) > 0:
            now = dateutil.parser.parse(date)
        else:
            now = datetime.datetime.now()
        # now = dateutil.parser.parse('2020-09-15T15:00:00Z')
        date_24h_before = now - datetime.timedelta(hours=24)
        # let authorized people time to set presence for those who came in late
        end_date = now - datetime.timedelta(hours=2)
        api = OdooAPI()

        # Let's start by adding an extra shift to associated member who came together
        cond = [['date_begin', '>=', date_24h_before.isoformat()],
                ['date_begin', '<=', end_date.isoformat()],
                ['state', '=', 'done'], 
                ['associate_registered', '=', 'both'],
                ['should_increment_extra_shift_done', '=', True]]
        fields = ['id', 'state', 'partner_id', 'date_begin']
        res = api.search_read('shift.registration', cond, fields)

        extra_shift_done_incremented_srids = []  # shift registration ids
        for r in res:
            cond = [['id', '=', r['partner_id'][0]]]
            fields = ['id','extra_shift_done']
            res_partner = api.search_read('res.partner', cond, fields)

            f = {'extra_shift_done': res_partner[0]['extra_shift_done'] + 1 }
            api.update('res.partner', [r['partner_id'][0]], f)

            extra_shift_done_incremented_srids.append(int(r['id']))

        # Make sure the counter isn't incremented twice
        f = {'should_increment_extra_shift_done': False}
        api.update('shift.registration', extra_shift_done_incremented_srids, f)

        absence_status = 'excused'
        res_c = api.search_read('ir.config_parameter',
                                [['key', '=', 'lacagette_membership.absence_status']],
                                ['value'])
        if len(res_c) == 1:
            absence_status = res_c[0]['value']
        cond = [['date_begin', '>=', date_24h_before.isoformat()],
                ['date_begin', '<=', end_date.isoformat()],
                ['state', '=', 'open']]
        fields = ['state', 'partner_id', 'date_begin', 'shift_id']
        res = api.search_read('shift.registration', cond, fields)
        partner_ids = []
        shift_ids = []
        for r in res:
            partner_ids.append(int(r['partner_id'][0]))
            shift_id = int(r['shift_id'][0])
            if shift_id not in shift_ids:   
                shift_ids.append(shift_id)
        #TODO : improve name of following method
        canceled_reg_ids, excluded_partner, ids = CagetteServices.fetch_registrations_infos_excluding_exempted_people(
            api, partner_ids, res
        )
        f = {'state': absence_status, 'date_closed': now.isoformat()}
        update_shift_reg_result = {'update': False, 'reg_shift': res, 'errors': []}
        individual_update_result = {}
        for mysregid in ids:
            try:
                individual_update_result[str(mysregid)] = api.update('shift.registration', [mysregid], f)
            except Exception as e:
                coop_logger.error("Error on updating shift.registration ids %s :   %s", str(mysregid), str(e))
        update_shift_reg_result['update'] = individual_update_result

        # With new way of overriding awesomefoodcoop status management, the following line is no more useful
        # update_shift_reg_result['process_status_res'] = api.execute('res.partner','run_process_target_status', [])
        # change shift state by triggering button_done method for all related shifts
        if len(canceled_reg_ids) > 0:
            f = {'state': 'cancel', 'date_closed': now.isoformat()}
            api.update('shift.registration', canceled_reg_ids, f)
        for sid in shift_ids:
            try:
                api.execute('shift.shift', 'button_done', sid)
            except Exception as e:
                marshal_none_error = 'cannot marshal None unless allow_none is enabled'
                if not (marshal_none_error in str(e)):
                    update_shift_reg_result['errors'].append({'shift_id': sid, 'msg' :str(e)})

        return update_shift_reg_result

    @staticmethod
    def fetch_registrations_infos_excluding_exempted_people(api, partner_ids, res):
        #TODO : document this method
        ids = []
        excluded_partner = []
        canceled_reg_ids = []  # for exempted people
        res_exempted = shifts.fonctions.get_exempted_ids_from(api, partner_ids)
        for r in res_exempted:
            excluded_partner.append(int(r['id']))
        for r in res:
            if not (int(r['partner_id'][0]) in excluded_partner):
                d_begin = r['date_begin']
                (d, h) = d_begin.split(' ')
                (_h, _m, _s) = h.split(':')
                if int(_h) < 21:
                    ids.append(int(r['id']))
            else:
                canceled_reg_ids.append(int(r['id']))
        return canceled_reg_ids, excluded_partner, ids

    @staticmethod
    def close_ftop_service():
        """Called by cron script"""
        # Retrieve the latest past FTOP service
        import dateutil.parser
        now = datetime.datetime.now()
        # now = dateutil.parser.parse('2019-10-20T00:00:00Z')
        cond = [['shift_type_id','=', 2],['date_end', '<=', now.isoformat()],['state','=', 'draft'], ['active', '=', True]]
        fields = ['name']
        api = OdooAPI()
        res = api.search_read('shift.shift', cond, fields,order ="date_end ASC", limit=1)
        # return res[0]['id']
        result = {}
        if res and len(res) > 0:
            result['service_found'] = True
            # Exceptions are due to the fact API returns None whereas the action is really done !...
            marshal_none_error = 'cannot marshal None unless allow_none is enabled'
            actual_errors = 0
            try:
                api.execute('shift.shift', 'button_confirm', [res[0]['id']])
            except Exception as e:
                if not (marshal_none_error in str(e)):
                    result['exeption_confirm'] = str(e)
                    actual_errors += 1
            try:
                api.execute('shift.shift', 'button_makeupok', [res[0]['id']])
            except Exception as e:
                if not (marshal_none_error in str(e)):
                    result['exeption_makeupok'] = str(e)
                    actual_errors += 1
            try:
                api.execute('shift.shift', 'button_done', [res[0]['id']])
            except Exception as e:
                if not (marshal_none_error in str(e)):
                    result['exeption_done'] = str(e)
                    actual_errors += 1
            if actual_errors == 0:
                result['done'] = True
            else:
                result['done'] = False
            result['actual_errors'] = actual_errors
        else:
            result['service_found'] = False
        return result

    @staticmethod
    def get_committees_shift_id():
        shift_id = None
        try:
            api = OdooAPI()
            res = api.search_read('ir.config_parameter',
                                  [['key','=', 'lacagette_membership.committees_shift_id']],
                                  ['value'])
            if len(res) > 0:
                try:
                    shift_id = int(res[0]['value'])
                except:
                    pass
        except:
            pass
        return shift_id

    @staticmethod
    def get_exemptions_shift_id():
        shift_id = None
        try:
            api = OdooAPI()
            res = api.search_read('ir.config_parameter',
                                  [['key','=', 'lacagette_exemptions.exemptions_shift_id']],
                                  ['value'])
            if len(res) > 0:
                try:
                    shift_id = int(res[0]['value'])
                except:
                    pass
        except:
            pass
        return shift_id

    @staticmethod
    def get_first_ftop_shift_id():
        shift_id = None
        try:
            api = OdooAPI()
            res = api.search_read('shift.template',
                                  [['shift_type_id','=', 2]],
                                  ['id', 'registration_qty'])

            # Get the ftop shift template with the max registrations: most likely the one in use
            ftop_shift = {'id': None, 'registration_qty': 0}
            for shift_reg in res:
                if shift_reg["registration_qty"] > ftop_shift["registration_qty"]:
                    ftop_shift = shift_reg

            try:
                shift_id = int(ftop_shift['id'])
            except:
                pass
        except:
            pass
        return shift_id

    @staticmethod
    def easy_validate_shift_presence(coop_id):
        """Add a presence point if the request is valid."""
        res = {}
        try:
            committees_shift_id = CagetteServices.get_committees_shift_id()
            api = OdooAPI()
            # let verify coop_id is corresponding to a ftop subscriber
            cond = [['id', '=', coop_id]]
            fields = ['tmpl_reg_line_ids']
            coop = api.search_read('res.partner', cond, fields)
            if coop:
                if len(coop[0]['tmpl_reg_line_ids']) > 0 :
                    cond = [['id', '=', coop[0]['tmpl_reg_line_ids'][0]]]
                    fields = ['shift_template_id']
                    shift_templ_res = api.search_read('shift.template.registration.line', cond, fields)
                    if (len(shift_templ_res) > 0
                        and
                        shift_templ_res[0]['shift_template_id'][0] == committees_shift_id):
                        ok_for_adding_pt = False
                        mininum_seconds_interval = getattr(settings, 'MINIMUM_SECONDS_BETWEEN_TWO_COMITEE_VALIDATION', 3600 * 24)
                        evt_name = getattr(settings, 'ENTRANCE_ADD_PT_EVENT_NAME', 'Validation service comité')

                        if mininum_seconds_interval > 0:
                            #  A constraint has been set to prevent from adding more than 1 point during a time period
                            #  Let's find out when was the last time a "special point" has been addes
                            c = [['partner_id', '=', coop_id], ['name', '=', evt_name]]
                            f = ['create_date']
                            last_point_mvts = api.search_read('shift.counter.event', c, f,
                                                              order ="create_date DESC", limit=1)
                            
                            if len(last_point_mvts):
                                now = datetime.datetime.now()
                                past = datetime.datetime. strptime(last_point_mvts[0]['create_date'],
                                                                   '%Y-%m-%d %H:%M:%S')
                                if (now - past).total_seconds() >= mininum_seconds_interval:
                                    ok_for_adding_pt = True
                            else:
                                ok_for_adding_pt = True
                        else:
                            #  mininum_seconds_interval is 0 : Point can we added without any condition
                            ok_for_adding_pt = True

                        if ok_for_adding_pt is True:
                            res['evt_id'] = CagetteMember(coop_id).add_pts('ftop', 1, evt_name)
                        else:
                            res['error'] = "Un point a déjà été ajouté trop récemment."
                    else:
                        res['error'] = "Vous n'avez pas le droit d'ajouter un point."
                else:
                    res['error'] = "Unregistred coop"
            else:
                res['error'] = "Invalid coop id"
        except Exception as e:
            coop_logger.error("easy_validate_shift_presence :  %s %s", str(coop_id), str(e))
        return res

class CagetteService(models.Model):
    """Class to handle cagette Odoo service."""

    def __init__(self, id):
        """Init with odoo id."""
        self.id = int(id)
        self.o_api = OdooAPI()

    def _process_associated_people_extra_shift_done(self):
        cond = [['shift_id', '=', self.id],
                ['state', '=', 'done'], 
                ['associate_registered', '=', 'both'],
                ['should_increment_extra_shift_done', '=', True]]
        fields = ['id', 'state', 'partner_id', 'date_begin']
        res = self.o_api.search_read('shift.registration', cond, fields)
        extra_shift_done_incremented_srids = []  # shift registration ids
        for r in res:
            cond = [['id', '=', r['partner_id'][0]]]
            fields = ['id','extra_shift_done']
            res_partner = self.o_api.search_read('res.partner', cond, fields)

            f = {'extra_shift_done': res_partner[0]['extra_shift_done'] + 1 }
            self.o_api.update('res.partner', [r['partner_id'][0]], f)

            extra_shift_done_incremented_srids.append(int(r['id']))

        # Make sure the counter isn't incremented twice
        f = {'should_increment_extra_shift_done': False}
        self.o_api.update('shift.registration', extra_shift_done_incremented_srids, f)

    def _process_related_shift_registrations(self):
        now = datetime.datetime.now()
        absence_status = 'excused'
        res_c = self.o_api.search_read('ir.config_parameter',
                                [['key', '=', 'lacagette_membership.absence_status']],
                                ['value'])
        if len(res_c) == 1:
            absence_status = res_c[0]['value']
        cond = [['shift_id', '=', self.id],
                ['state', '=', 'open']]
        fields = ['state', 'partner_id', 'date_begin']
        res = self.o_api.search_read('shift.registration', cond, fields)
        partner_ids = []
        for r in res:
            partner_ids.append(int(r['partner_id'][0]))
        #TODO : improve name of following method
        canceled_reg_ids, excluded_partner, ids = CagetteServices.fetch_registrations_infos_excluding_exempted_people(
            api, partner_ids, res
        )
        f = {'state': absence_status, 'date_closed': now.isoformat()}
        update_shift_reg_result = {'update': self.o_api.update('shift.registration', ids, f), 'reg_shift': res, 'errors': []}
        if update_shift_reg_result['update'] is True:
            update_shift_reg_result['process_status_res'] = self.o_api.execute('res.partner','run_process_target_status', [])
            # change shift state by triggering button_done method for all related shifts
            if len(canceled_reg_ids) > 0:
                f = {'state': 'cancel', 'date_closed': now.isoformat()}
                self.o_api.update('shift.registration', canceled_reg_ids, f)
           
            try:
                self.o_api.execute('shift.shift', 'button_done', self.id)
            except Exception as e:
                marshal_none_error = 'cannot marshal None unless allow_none is enabled'
                if not (marshal_none_error in str(e)):
                    update_shift_reg_result['errors'].append({'shift_id': self.id, 'msg' :str(e)})

        return update_shift_reg_result

    def record_absences(self, request):
        """Can only been executed if an Odoo user is beeing connected."""
        res = {}
        try:
            if CagetteUser.are_credentials_ok(request) is True:
                self._process_associated_people_extra_shift_done()
                res = self._process_related_shift_registrations()
            else:
                res['error'] = 'Forbidden'
        except Exception as e:
            coop_logger.error("CagetteService.record_absences :  %s %s", str(self.id), str(e))
            res['error'] = str(e)
        return res