# -*- coding: utf-8 -*-
"""
PLANNING TEAM - absences, conges payes et compteurs.

Deux facons de decompter une absence, selon ce que Jerome coche :

  * « CP »     -> on retire des jours ouvrables du solde de conges payes
                  (lundi -> samedi, jours feries exclus) ;
  * « heures » -> on retire des heures du planning, sans toucher au solde. Le creneau
                  reste visible mais hachure, pour qu'on voie tout de suite ou sont
                  les trous. C'est ce que le cabinet attend pour les arrets de travail
                  (« combien d'heures a deduire »).

Le solde affiche n'est jamais ecrase : il se recalcule toujours comme

    solde initial  -  jours pris  +  ajustements manuels

Chaque correction a la main est donc conservee avec sa date et son motif. C'est
exactement ce qui manquait en 2025, quand des CP oublies au decompte n'ont ete
rattrapes qu'en mai 2026.
"""

import datetime
import os
import re
import sys

BASE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE)

import calendrier as cal    # noqa: E402

CLASSEUR_CONGES = os.path.join(os.path.dirname(BASE), 'docs',
                               u'Planning vacances-R\xe9cap cong\xe9s.xlsx')
FEUILLE_CONGES = '2026-2027'

# Date a laquelle demarrent les soldes repris du classeur.
DEBUT_PERIODE = datetime.date(2026, 6, 1)

# Motifs d'absence. « decompte » dit ce qui se passe par defaut ; Jerome peut toujours
# basculer une absence de « cp » vers « heures » et inversement.
MOTIFS = [
    ('cp',                 u'Cong\xe9 pay\xe9',            'cp',     True),
    ('maladie',            u'Arr\xeat maladie',            'heures', False),
    ('maternite',          u'Maternit\xe9 / paternit\xe9', 'heures', True),
    ('conge_parental',     u'Cong\xe9 parental',           'heures', False),
    ('ajpp',               u'AJPP',                        'heures', False),
    ('revision',           u'Jour de r\xe9vision',         'heures', True),
    ('formation',          u'Formation / \xe9cole',        'heures', True),
    ('evenement_familial', u'\xc9v\xe9nement familial',    'heures', True),
    ('recuperation',       u'R\xe9cup\xe9ration',          'recup',  True),
    ('sans_solde',         u'Sans solde',                  'heures', False),
]

SCHEMA = """
CREATE TABLE IF NOT EXISTS absence (
    id            INTEGER PRIMARY KEY,
    salarie_id    INTEGER NOT NULL REFERENCES salarie(id) ON DELETE CASCADE,
    date_debut    TEXT NOT NULL,
    date_fin      TEXT NOT NULL,
    motif         TEXT NOT NULL,
    decompte      TEXT NOT NULL DEFAULT 'cp',    -- cp | heures | recup
    jours         REAL,                          -- jours ouvrables, si decompte = cp
    heures        REAL,                          -- heures retirees, si decompte = heures
    salaire_maintenu INTEGER NOT NULL DEFAULT 1,
    statut        TEXT NOT NULL DEFAULT 'validee',  -- demandee | validee | refusee
    demandee_par  TEXT,
    validee_par   TEXT,
    libelle       TEXT,                          -- le texte d'origine, si import
    origine       TEXT NOT NULL DEFAULT 'saisie',-- saisie | import
    cree_le       TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS cp_solde (
    salarie_id     INTEGER PRIMARY KEY REFERENCES salarie(id) ON DELETE CASCADE,
    solde_initial  REAL NOT NULL DEFAULT 0,
    date_initiale  TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS cp_ajustement (
    id         INTEGER PRIMARY KEY,
    salarie_id INTEGER NOT NULL REFERENCES salarie(id) ON DELETE CASCADE,
    jours      REAL NOT NULL,           -- signe : +2 rend deux jours, -1 en retire un
    motif      TEXT NOT NULL,
    fait_le    TEXT NOT NULL,
    auteur     TEXT
);

-- Compteurs d'heures, un par type et par personne :
--   recup — les heures a recuperer (positif) ou dues (negatif) ;
--   hs    — les heures supplementaires a payer, transmises au cabinet.
-- Meme principe que pour les conges payes : le solde n'est jamais ecrase, il se
-- recalcule comme solde initial + ecarts au cycle + ajustements manuels.
CREATE TABLE IF NOT EXISTS heures_solde (
    salarie_id    INTEGER NOT NULL REFERENCES salarie(id) ON DELETE CASCADE,
    type          TEXT NOT NULL,
    solde_initial REAL NOT NULL DEFAULT 0,
    date_initiale TEXT NOT NULL,
    PRIMARY KEY (salarie_id, type)
);

CREATE TABLE IF NOT EXISTS heures_ajustement (
    id         INTEGER PRIMARY KEY,
    salarie_id INTEGER NOT NULL REFERENCES salarie(id) ON DELETE CASCADE,
    type       TEXT NOT NULL,
    heures     REAL NOT NULL,      -- signe : +2 ajoute deux heures, -1 en retire une
    motif      TEXT NOT NULL,
    fait_le    TEXT NOT NULL,
    auteur     TEXT
);

CREATE INDEX IF NOT EXISTS idx_absence_salarie ON absence (salarie_id, date_debut);
"""

TYPES_HEURES = ('recup', 'hs')


TABLES = ('absence', 'cp_solde', 'cp_ajustement', 'heures_solde', 'heures_ajustement')


def tables_completes(cx, tables):
    """Toutes ces tables existent-elles deja ?"""
    presentes = {l[0] for l in cx.execute(
        "SELECT name FROM sqlite_master WHERE type = 'table'")}
    return all(t in presentes for t in tables)


def creer_schema(cx):
    """
    Ne fait rien si toutes les tables existent deja : `executescript` validerait la
    transaction en cours, ce qui rendrait definitives les ecritures d'une simulation.

    Le controle porte sur la liste complete, pas sur une seule table : sinon l'ajout
    d'une table dans une version ulterieure passerait inapercu.
    """
    if tables_completes(cx, TABLES):
        return
    cx.executescript(SCHEMA)
    cx.commit()


# ------------------------------------------------- lecture du classeur des conges

def _lire_feuille(chemin, nom_feuille):
    """{ 'D7': 'valeur' } pour une feuille du classeur des conges."""
    import zipfile
    import xml.etree.ElementTree as ET
    ns = '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}'

    with zipfile.ZipFile(chemin) as zf:
        with zf.open('xl/sharedStrings.xml') as f:
            chaines = [''.join(t.text or '' for t in si.iter(ns + 't'))
                       for si in ET.parse(f).getroot()]
        with zf.open('xl/workbook.xml') as f:
            noms = [s.get('name') for s in ET.parse(f).getroot().iter(ns + 'sheet')]
        if nom_feuille not in noms:
            raise ValueError(u'Feuille introuvable : %s' % nom_feuille)
        with zf.open('xl/worksheets/sheet%d.xml' % (noms.index(nom_feuille) + 1)) as f:
            cell = {}
            for c in ET.parse(f).getroot().iter(ns + 'c'):
                v = c.find(ns + 'v')
                if v is None or v.text is None:
                    continue
                texte = chaines[int(v.text)] if c.get('t') == 's' else v.text
                texte = (texte or '').strip()
                if texte:
                    cell[c.get('r')] = texte
    return cell


def _colonnes_par_prenom(cell):
    """
    La ligne 2 donne les prenoms ; la colonne suivante porte les jours poses.
    Renvoie { prenom : (colonne periode, colonne jours) }.
    """
    resultat = {}
    for ref, valeur in cell.items():
        m = re.match(r'^([A-Z]+)2$', ref)
        if not m or valeur in (u'Semaines', u'D\xe9but', u'Fin'):
            continue
        col = m.group(1)
        resultat[valeur] = (col, _colonne_suivante(col))
    return resultat


def _colonne_suivante(col):
    n = 0
    for ch in col:
        n = n * 26 + (ord(ch) - 64)
    n += 1
    s = ''
    while n > 0:
        n, r = divmod(n - 1, 26)
        s = chr(65 + r) + s
    return s


def dates_du_libelle(libelle, lundi):
    """
    Traduit un libelle du classeur des conges en vraies dates.

    Le classeur ecrit les periodes a la main, sous des formes tres variees :
        « x »                       -> toute la semaine
        « Lu 08/06 », « Le 31/08 »  -> un jour
        « du 04 au 08/08 »          -> une periode
        « 26-27/10 », « 22-24/07 »  -> une periode dans le mois
        « 15 +17/06 », « 02+03/06 » -> plusieurs jours
        « Lu 31/08 et Ma 01/09 »    -> plusieurs jours, chacun date

    Renvoie la liste des dates, ou None si le libelle reste indechiffrable — auquel
    cas on ne devine pas : mieux vaut une absence sans date qu'une fausse date.
    """
    texte = (libelle or '').strip()
    if not texte:
        return None
    if texte.lower() in ('x', 'xx'):
        return [lundi + datetime.timedelta(i) for i in range(6)]

    # Certaines cellules contiennent une vraie date Excel plutot qu'un texte : elle
    # arrive alors sous forme de nombre de jours depuis le 30/12/1899.
    if re.fullmatch(r'\d{5}(\.0+)?', texte):
        jour = datetime.date(1899, 12, 30) + datetime.timedelta(int(float(texte)))
        # On ne l'accepte que si elle tombe dans la semaine visee : au-dela, c'est une
        # saisie fautive qu'il vaut mieux signaler que deviner.
        if 0 <= (jour - lundi).days <= 5:
            return [jour]
        return None

    def caler(jour, mois):
        """L'annee est celle qui rapproche le plus la date du lundi de la semaine."""
        candidates = []
        for annee in (lundi.year - 1, lundi.year, lundi.year + 1):
            try:
                candidates.append(datetime.date(annee, mois, jour))
            except ValueError:
                pass
        return min(candidates, key=lambda d: abs((d - lundi).days)) if candidates else None

    dates = []

    # « du 03 au 08/08 » ou « du 11/08 au 15/08 »
    for m in re.finditer(r'du\s+(\d{1,2})(?:/(\d{1,2}))?\s+au\s+(\d{1,2})/(\d{1,2})',
                         texte, re.IGNORECASE):
        fin = caler(int(m.group(3)), int(m.group(4)))
        debut = caler(int(m.group(1)), int(m.group(2) or m.group(4)))
        if debut and fin and debut <= fin:
            dates.extend(debut + datetime.timedelta(i)
                         for i in range((fin - debut).days + 1))
    reste = re.sub(r'du\s+\d{1,2}(?:/\d{1,2})?\s+au\s+\d{1,2}/\d{1,2}', ' ',
                   texte, flags=re.IGNORECASE)

    # « 26-27/10 », « 05-06/06 » : une periode dans un meme mois
    for m in re.finditer(r'(\d{1,2})\s*-\s*(\d{1,2})/(\d{1,2})', reste):
        mois = int(m.group(3))
        debut, fin = caler(int(m.group(1)), mois), caler(int(m.group(2)), mois)
        if debut and fin and debut <= fin:
            dates.extend(debut + datetime.timedelta(i)
                         for i in range((fin - debut).days + 1))
    reste = re.sub(r'\d{1,2}\s*-\s*\d{1,2}/\d{1,2}', ' ', reste)

    # Les jours restants ecrits « jj/mm », et les jours nus rattaches au mois qui suit
    # (« 15 +17/06 » : le 15 appartient au mois du 17).
    explicites = list(re.finditer(r'(\d{1,2})/(\d{1,2})', reste))
    for m in explicites:
        jour = caler(int(m.group(1)), int(m.group(2)))
        if jour:
            dates.append(jour)
    if explicites:
        mois = int(explicites[0].group(2))
        avant = reste[:explicites[0].start()]
        for m in re.finditer(r'(?<![\d/])(\d{1,2})(?![\d/])', avant):
            jour = caler(int(m.group(1)), mois)
            if jour:
                dates.append(jour)

    if not dates:
        return None
    return sorted(set(dates))


def _periodes_contigues(dates):
    """Regroupe une liste de dates en periodes continues (debut, fin)."""
    periodes, debut, precedente = [], dates[0], dates[0]
    for jour in dates[1:]:
        if (jour - precedente).days > 1:
            periodes.append((debut, precedente))
            debut = jour
        precedente = jour
    periodes.append((debut, precedente))
    return periodes


def _nombre(texte):
    try:
        return float(str(texte).replace(',', '.'))
    except (TypeError, ValueError):
        return None


def lire_conges(chemin=None, feuille=FEUILLE_CONGES):
    """
    Extrait du classeur : le solde de chacun au 1er juin 2026, et la liste des
    periodes posees depuis, semaine par semaine.

    Les libelles sont conserves tels quels (« Lu 08/06 », « du 04 au 08/08 », « x »
    pour une semaine entiere) : ils ne sont pas assez reguliers pour etre transformes
    en dates fiables, mais ils gardent la trace de ce qui a ete pose. Les periodes
    importees servent au solde, pas au planning : l'outil demarre en septembre 2026.

    Renvoie { prenom : {'solde': float, 'periodes': [...]} }
    """
    cell = _lire_feuille(chemin or CLASSEUR_CONGES, feuille)
    colonnes = _colonnes_par_prenom(cell)

    resultat = {}
    for prenom, (col_periode, col_jours) in colonnes.items():
        solde = _nombre(cell.get(col_jours + '6'))
        if solde is None:
            continue                      # colonne sans compteur (Jerome)
        periodes = []
        for ligne in range(7, 59):        # lignes de semaines, avant les totaux
            libelle = cell.get('%s%d' % (col_periode, ligne))
            brut = cell.get('%s%d' % (col_jours, ligne))
            if not libelle and not brut:
                continue
            jours = _nombre(brut)
            periodes.append({
                'semaine': _nombre(cell.get('A%d' % ligne)),
                'libelle': libelle or '',
                'jours': jours,
                'hors_cp': jours is None and brut is not None,
                'note': brut if jours is None else None,
            })
        resultat[prenom] = {'solde': solde, 'periodes': periodes}
    return resultat


# ------------------------------------------------------------------- import

# Le classeur des conges nomme les gens par leur prenom ; la base par leur initiale.
CORRESPONDANCE = {
    u'Aurore': 'A', u'Nicolas': 'N', u'Marie-C\xe9cile': 'MC', u'Sariaka': 'Sa',
    u'Val\xe9rie': 'V', u'Elodie': 'E', u'\xc9lodie': 'E', u'Gwendoline': 'G',
    u'St\xe9phanie': 'St', u'Patricia': 'P', u'Thomas': 'T', u'Chlo\xe9': 'C',
}


def importer_soldes(cx, chemin=None, bavard=True):
    """Reprend les soldes et l'historique du classeur des conges."""
    creer_schema(cx)
    cx.execute('DELETE FROM cp_solde')
    cx.execute("DELETE FROM absence WHERE origine IN ('import', 'import-date')")

    par_initiale = {l['initiale']: l['id']
                    for l in cx.execute('SELECT id, initiale FROM salarie')}
    donnees = lire_conges(chemin)
    maintenant = datetime.datetime.now().isoformat(timespec='seconds')

    inconnus, sans_date, importes, dates_lues = [], [], 0, 0
    for prenom, bloc in donnees.items():
        initiale = CORRESPONDANCE.get(prenom)
        sid = par_initiale.get(initiale) if initiale else None
        if sid is None:
            inconnus.append(prenom)
            continue

        cx.execute(
            'INSERT INTO cp_solde (salarie_id, solde_initial, date_initiale) VALUES (?, ?, ?)',
            (sid, bloc['solde'], DEBUT_PERIODE.isoformat()))

        for p in bloc['periodes']:
            if p['jours'] is None:
                continue                  # « HS », « REVISION »... : pas des CP
            annee = 2026 if (p['semaine'] or 0) >= 23 else 2027
            lundi = cal.lundi_de(annee, int(p['semaine']))

            dates = dates_du_libelle(p['libelle'], lundi)
            if dates:
                # Libelle compris : on pose de vraies dates, et l'absence peut donc
                # apparaitre au planning.
                periodes = _periodes_contigues(dates)
                part = round(p['jours'] / float(len(periodes)), 2)
                for debut, fin in periodes:
                    cx.execute(
                        """INSERT INTO absence
                             (salarie_id, date_debut, date_fin, motif, decompte, jours,
                              libelle, origine, cree_le)
                           VALUES (?, ?, ?, 'cp', 'cp', ?, ?, 'import-date', ?)""",
                        (sid, debut.isoformat(), fin.isoformat(), part,
                         p['libelle'], maintenant))
                    importes += 1
                dates_lues += 1
            else:
                # Libelle indechiffrable : l'absence est ancree sur la semaine entiere
                # et sert UNIQUEMENT au solde. Elle ne doit jamais peindre le planning,
                # sans quoi la personne paraitrait absente toute la semaine.
                cx.execute(
                    """INSERT INTO absence
                         (salarie_id, date_debut, date_fin, motif, decompte, jours,
                          libelle, origine, cree_le)
                       VALUES (?, ?, ?, 'cp', 'cp', ?, ?, 'import', ?)""",
                    (sid, lundi.isoformat(),
                     (lundi + datetime.timedelta(5)).isoformat(),
                     p['jours'], p['libelle'], maintenant))
                importes += 1
                sans_date.append(u'%s : « %s » (semaine %s)'
                                 % (prenom, p['libelle'], p['semaine']))
    cx.commit()

    if bavard:
        print(u'Cong\xe9s import\xe9s : %d soldes, %d p\xe9riodes '
              u'(%d libell\xe9s dat\xe9s, %d ancr\xe9s sur la semaine).'
              % (len(donnees) - len(inconnus), importes, dates_lues, len(sans_date)))
        for ligne in sans_date:
            print(u'   sans date : %s' % ligne)
        if inconnus:
            print(u'Colonnes sans correspondance : %s' % ', '.join(sorted(inconnus)))
    return importes


# ------------------------------------------------------------------ compteurs

def solde_cp(cx, salarie_id):
    """
    Le solde de conges payes, recalcule :
        solde initial  -  jours pris  +  ajustements manuels
    """
    initial = cx.execute(
        'SELECT solde_initial FROM cp_solde WHERE salarie_id = ?',
        (salarie_id,)).fetchone()
    initial = initial['solde_initial'] if initial else 0.0

    pris = cx.execute(
        """SELECT COALESCE(SUM(jours), 0) AS n FROM absence
            WHERE salarie_id = ? AND decompte = 'cp' AND statut = 'validee'""",
        (salarie_id,)).fetchone()['n']

    ajuste = cx.execute(
        'SELECT COALESCE(SUM(jours), 0) AS n FROM cp_ajustement WHERE salarie_id = ?',
        (salarie_id,)).fetchone()['n']

    return {'initial': round(initial, 2), 'pris': round(pris, 2),
            'ajustements': round(ajuste, 2),
            'solde': round(initial - pris + ajuste, 2)}


def soldes(cx):
    """Le compteur de CP de chacun, dans l'ordre d'affichage."""
    resultat = []
    for l in cx.execute(
            """SELECT id, initiale, prenom, groupe FROM salarie
                WHERE actif = 1 ORDER BY groupe_rang, ordre"""):
        bloc = solde_cp(cx, l['id'])
        bloc.update({'salarie_id': l['id'], 'initiale': l['initiale'],
                     'prenom': l['prenom'], 'groupe': l['groupe']})
        resultat.append(bloc)
    return resultat


def ajuster_solde(cx, salarie_id, nouveau_solde, motif, auteur=None):
    """
    Corrige le solde a la main sans jamais l'ecraser : on enregistre l'ecart comme un
    ajustement date et motive. Le solde reste donc toujours explicable.
    """
    actuel = solde_cp(cx, salarie_id)['solde']
    ecart = round(float(nouveau_solde) - actuel, 2)
    if abs(ecart) < 0.005:
        return {'ecart': 0.0, 'solde': actuel}
    if not (motif or '').strip():
        raise ValueError(u'un motif est obligatoire pour corriger un solde')

    cx.execute(
        """INSERT INTO cp_ajustement (salarie_id, jours, motif, fait_le, auteur)
           VALUES (?, ?, ?, ?, ?)""",
        (salarie_id, ecart, motif.strip(),
         datetime.datetime.now().isoformat(timespec='seconds'), auteur))
    cx.commit()
    return {'ecart': ecart, 'solde': solde_cp(cx, salarie_id)['solde']}


# ------------------------------------------------------- compteurs d'heures

def solde_heures(cx, salarie_id, type_compteur, ecart_planning=0.0):
    """
    Le solde d'un compteur d'heures :
        solde initial  +  ecarts au cycle (recup seulement)  +  ajustements manuels

    `ecart_planning` vient de reel.compteur_recuperation : les heures retirees ou
    ajoutees a la main sur le planning. Elles n'alimentent que la recuperation ; les
    heures supplementaires, elles, resultent d'un arbitrage explicite de Jerome.
    """
    creer_schema(cx)
    initial = cx.execute(
        'SELECT solde_initial FROM heures_solde WHERE salarie_id = ? AND type = ?',
        (salarie_id, type_compteur)).fetchone()
    initial = initial['solde_initial'] if initial else 0.0

    ajuste = cx.execute(
        """SELECT COALESCE(SUM(heures), 0) AS n FROM heures_ajustement
            WHERE salarie_id = ? AND type = ?""",
        (salarie_id, type_compteur)).fetchone()['n']

    ecart = ecart_planning if type_compteur == 'recup' else 0.0
    return {'initial': round(initial, 2), 'planning': round(ecart, 2),
            'ajustements': round(ajuste, 2),
            'solde': round(initial + ecart + ajuste, 2)}


def soldes_heures(cx, ecarts=None):
    """Les deux compteurs d'heures de chacun. `ecarts` = { salarie_id : heures }."""
    ecarts = ecarts or {}
    resultat = []
    for l in cx.execute(
            """SELECT id, initiale, prenom, groupe, couleur FROM salarie
                WHERE actif = 1 AND hors_planning = 0
                ORDER BY groupe_rang, ordre"""):
        bloc = {k: l[k] for k in l.keys()}
        bloc['salarie_id'] = bloc.pop('id')
        for type_compteur in TYPES_HEURES:
            bloc[type_compteur] = solde_heures(cx, bloc['salarie_id'], type_compteur,
                                               ecarts.get(bloc['salarie_id'], 0.0))
        resultat.append(bloc)
    return resultat


def ajuster_heures(cx, salarie_id, type_compteur, nouveau_solde, motif,
                   ecart_planning=0.0, auteur=None):
    """Corrige un compteur d'heures a la main, en gardant la trace de l'ecart."""
    if type_compteur not in TYPES_HEURES:
        raise ValueError(u'compteur inconnu : %s' % type_compteur)
    actuel = solde_heures(cx, salarie_id, type_compteur, ecart_planning)['solde']
    ecart = round(float(nouveau_solde) - actuel, 2)
    if abs(ecart) < 0.005:
        return {'ecart': 0.0, 'solde': actuel}
    if not (motif or '').strip():
        raise ValueError(u'un motif est obligatoire pour corriger un compteur')

    cx.execute(
        """INSERT INTO heures_ajustement (salarie_id, type, heures, motif, fait_le, auteur)
           VALUES (?, ?, ?, ?, ?, ?)""",
        (salarie_id, type_compteur, ecart, motif.strip(),
         datetime.datetime.now().isoformat(timespec='seconds'), auteur))
    cx.commit()
    return {'ecart': ecart,
            'solde': solde_heures(cx, salarie_id, type_compteur, ecart_planning)['solde']}


def basculer_recup_en_hs(cx, salarie_id, heures, motif=None, auteur=None):
    """
    L'arbitrage du cadrage : « les deux selon les cas ». On sort des heures de la
    recuperation pour les passer en heures supplementaires a payer.
    """
    heures = round(float(heures), 2)
    if heures <= 0:
        raise ValueError(u'indiquer un nombre d’heures positif')
    libelle = motif or (u'%g h de r\xe9cup\xe9ration pass\xe9es en heures suppl\xe9mentaires'
                        % heures)
    maintenant = datetime.datetime.now().isoformat(timespec='seconds')
    cx.execute(
        """INSERT INTO heures_ajustement (salarie_id, type, heures, motif, fait_le, auteur)
           VALUES (?, 'recup', ?, ?, ?, ?)""",
        (salarie_id, -heures, libelle, maintenant, auteur))
    cx.execute(
        """INSERT INTO heures_ajustement (salarie_id, type, heures, motif, fait_le, auteur)
           VALUES (?, 'hs', ?, ?, ?, ?)""",
        (salarie_id, heures, libelle, maintenant, auteur))
    cx.commit()


def historique_heures(cx, salarie_id=None):
    conditions, params = ['1 = 1'], []
    if salarie_id:
        conditions.append('a.salarie_id = ?')
        params.append(salarie_id)
    lignes = cx.execute(
        """SELECT a.*, s.prenom FROM heures_ajustement a
             JOIN salarie s ON s.id = a.salarie_id
            WHERE %s ORDER BY a.fait_le DESC""" % ' AND '.join(conditions), params)
    return [dict(l) for l in lignes]


def historique_ajustements(cx, salarie_id=None):
    if salarie_id:
        lignes = cx.execute(
            """SELECT a.*, s.prenom FROM cp_ajustement a
                 JOIN salarie s ON s.id = a.salarie_id
                WHERE a.salarie_id = ? ORDER BY a.fait_le DESC""", (salarie_id,))
    else:
        lignes = cx.execute(
            """SELECT a.*, s.prenom FROM cp_ajustement a
                 JOIN salarie s ON s.id = a.salarie_id
                ORDER BY a.fait_le DESC""")
    return [dict(l) for l in lignes]


# ------------------------------------------------------------------- absences

def poser_absence(cx, salarie_id, date_debut, date_fin, motif,
                  decompte=None, heures=None, salaire_maintenu=None,
                  statut='validee', libelle=None, jours=None):
    """
    Enregistre une absence. En mode « cp », les jours ouvrables sont comptes
    automatiquement (lundi -> samedi, feries exclus).
    """
    reference = {m[0]: m for m in MOTIFS}
    if motif not in reference:
        raise ValueError(u'motif inconnu : %s' % motif)
    _, _, decompte_defaut, maintenu_defaut = reference[motif]
    decompte = decompte or decompte_defaut
    if salaire_maintenu is None:
        salaire_maintenu = maintenu_defaut

    debut = datetime.date.fromisoformat(date_debut)
    fin = datetime.date.fromisoformat(date_fin)
    if fin < debut:
        raise ValueError(u'la date de fin precede la date de debut')

    # `jours` est fourni par l'appelant, qui applique la regle des bordures (voir
    # reel.compter_cp). Sans lui, on retombe sur le decompte brut en jours ouvrables.
    if decompte == 'cp' and jours is None:
        jours = float(cal.compter_ouvrables(debut, fin))
    elif decompte != 'cp':
        jours = None

    cur = cx.execute(
        """INSERT INTO absence
             (salarie_id, date_debut, date_fin, motif, decompte, jours, heures,
              salaire_maintenu, statut, libelle, origine, cree_le)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'saisie', ?)""",
        (salarie_id, debut.isoformat(), fin.isoformat(), motif, decompte, jours,
         heures, 1 if salaire_maintenu else 0, statut, libelle,
         datetime.datetime.now().isoformat(timespec='seconds')))
    cx.commit()
    return cur.lastrowid


def supprimer_absence(cx, absence_id):
    cx.execute('DELETE FROM absence WHERE id = ?', (absence_id,))
    cx.commit()


def absences(cx, debut=None, fin=None, salarie_id=None):
    conditions, params = ['1 = 1'], []
    if debut:
        conditions.append('a.date_fin >= ?')
        params.append(debut)
    if fin:
        conditions.append('a.date_debut <= ?')
        params.append(fin)
    if salarie_id:
        conditions.append('a.salarie_id = ?')
        params.append(salarie_id)
    lignes = cx.execute(
        """SELECT a.*, s.initiale, s.prenom, s.couleur FROM absence a
             JOIN salarie s ON s.id = a.salarie_id
            WHERE %s ORDER BY a.date_debut""" % ' AND '.join(conditions), params)
    return [dict(l) for l in lignes]


if __name__ == '__main__':
    import base as bd

    cx = bd.connexion()
    if len(sys.argv) > 1 and sys.argv[1] == 'import':
        importer_soldes(cx)

    print(u'\n%-14s %9s %8s %11s %9s' % (u'Salari\xe9', u'Initial', u'Pris',
                                         u'Ajustements', u'Solde'))
    for s in soldes(cx):
        print(u'%-14s %9.2f %8.2f %11.2f %9.2f'
              % (s['prenom'], s['initial'], s['pris'], s['ajustements'], s['solde']))
    cx.close()
