Overview
Comment:Merge dev into trunk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both
Files: files | file ages | folders
SHA1: 81dd73fb2691422def62be69a2ce9be1b99df824
User & Date: bohwaz on 2020-12-06 18:40:08
Other Links: manifest | tags
Context
2020-12-06
18:41
New release check-in: e45b645e78 user: bohwaz tags: trunk, stable, 1.0.0-rc8
18:40
Merge dev into trunk check-in: 81dd73fb26 user: bohwaz
14:57
Transfer was reversed check-in: 3af5abf868 user: bohwaz tags: dev
2020-09-08
18:53
Limiter la longueur du champ de recherche check-in: 0277842dc6 user: bohwaz tags: trunk, stable
Changes

Modified .travis.yml from [ab9c965cec] to [4da8c0eef1].

1
2
3
4
5
6
7
8
9
10
11
12
language: php
php:
  - '5.6'
  - '7.0'
  - '7.1'
  - '7.2'
  - '7.3'
  - '7.4'

install:
  - make -C src deps



<
<
<







1
2



3
4
5
6
7
8
9
language: php
php:



  - '7.2'
  - '7.3'
  - '7.4'

install:
  - make -C src deps

Name change from src/COPYING to COPYING.

Modified README.md from [b24af5ed67] to [b535b9dd6a].

1
2

3

4

5

6

7

8

9

10

11

12




13

Garradin - Gestionnaire d'association libre
===========================================



Inclus les bibliothèques suivantes :



- Gibberish AES

  https://github.com/mdp/gibberish-aes

  Copyright : Mark Percival 2008 - http://markpercival.us

  Licence : MIT



- KD2fw

  Copyright : 2001-2018 BohwaZ




  Licence : GNU AGPL v3

|
|
>

>
|
>

>
|
>
|
>
|
>
|
>

>
|
>
|
>
>
>
>
|
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Garradin - Le gestionnaire d'association

Garradin est un logiciel de gestion de petite et moyenne association.

Plus d'infos sur le site de développement ici : [fossil.kd2.org/garradin](https://fossil.kd2.org/garradin/)

[Documentation développeuse⋅développeur](https://fossil.kd2.org/garradin/wiki?name=Documentation+d%C3%A9veloppeur)

Il est possible d'essayer et utiliser gratuitement Garradin sur la plateforme [Garradin.eu](https://garradin.eu/).

Le code sur Github, n'est qu'un miroir, le développement principal se passe sur Fossil.

## Licence

GNU Affero GPL v3 (voir fichier COPYING)

Cette licence permet la libre redistribution, utilisation et modification du logiciel.

La seule condition est de re-partager les éventuelles modifications apportées.

Cette clause s'applique même si le logiciel n'est pas distribué et simplement installé sur un serveur.

## Code utilisé

Inclus les bibliothèques suivantes :

* [KD2fw](https://fossil.kd2.org/kd2fw/) - Copyright : 2001-2020+ BohwaZ - Licence : GNU AGPL v3
* [Gibberish AES](https://github.com/mdp/gibberish-aes) - Copyright : Mark Percival 2008 - http://markpercival.us -Licence : MIT

Name change from src/include/data/0.7.0.sql to archives/0.7.0_migration.sql.

Name change from src/include/data/0.7.2.sql to archives/0.7.2_migration.sql.

Modified archives/0.8.0_migration.sql from [34e1625504] to [b56b968950].

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
-- Sinon les nouveaux ne seront pas créés sur la nouvelle table
DROP TRIGGER wiki_recherche_delete;
DROP TRIGGER wiki_recherche_update;
DROP TRIGGER wiki_recherche_contenu_insert;
DROP TRIGGER wiki_recherche_contenu_chiffre;

-- Création des tables mises à jour (et de leurs index)
.read schema.sql

-- Copie des données
INSERT INTO cotisations_membres SELECT * FROM cotisations_membres_old;
INSERT INTO rappels SELECT * FROM rappels_old;
INSERT INTO rappels_envoyes SELECT id, id_membre, id_cotisation, id_rappel, date, media FROM rappels_envoyes_old;
INSERT INTO wiki_pages SELECT * FROM wiki_pages_old;
INSERT INTO wiki_revisions SELECT * FROM wiki_revisions_old;







|







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
-- Sinon les nouveaux ne seront pas créés sur la nouvelle table
DROP TRIGGER wiki_recherche_delete;
DROP TRIGGER wiki_recherche_update;
DROP TRIGGER wiki_recherche_contenu_insert;
DROP TRIGGER wiki_recherche_contenu_chiffre;

-- Création des tables mises à jour (et de leurs index)
.read 0.8.0_schema.sql

-- Copie des données
INSERT INTO cotisations_membres SELECT * FROM cotisations_membres_old;
INSERT INTO rappels SELECT * FROM rappels_old;
INSERT INTO rappels_envoyes SELECT id, id_membre, id_cotisation, id_rappel, date, media FROM rappels_envoyes_old;
INSERT INTO wiki_pages SELECT * FROM wiki_pages_old;
INSERT INTO wiki_revisions SELECT * FROM wiki_revisions_old;

Added archives/0.8.0_schema.sql version [1c0ae41b79].













































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);

-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
-- compta_categorie_cotisations => id_categorie
-- compta_categorie_dons => id_categorie

CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,
    description TEXT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) ON DELETE SET NULL
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY NOT NULL,
    id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta

    intitule TEXT NOT NULL,
    description TEXT NULL,
    montant REAL NOT NULL,

    duree INTEGER NULL, -- En jours
    debut TEXT NULL, -- timestamp
    fin TEXT NULL,

    FOREIGN KEY (id_categorie_compta) REFERENCES compta_categories (id)
);

CREATE TABLE IF NOT EXISTS cotisations_membres
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)
);

CREATE UNIQUE INDEX IF NOT EXISTS cm_unique ON cotisations_membres (id_membre, id_cotisation, date);

CREATE TABLE IF NOT EXISTS membres_operations
-- Liaision des enregistrement des paiements en compta
(
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_operation INTEGER NOT NULL REFERENCES compta_journal (id) ON DELETE CASCADE,
    id_cotisation INTEGER NULL REFERENCES cotisations_membres (id) ON DELETE SET NULL,

    PRIMARY KEY (id_membre, id_operation)
);

CREATE TABLE IF NOT EXISTS rappels
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    sujet TEXT NOT NULL,
    texte TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,
    id_rappel INTEGER NULL REFERENCES rappels (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),

    media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);

--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki
(
    id INTEGER PRIMARY KEY NOT NULL,
    uri TEXT NOT NULL, -- URI unique (équivalent NomPageWiki)
    titre TEXT NOT NULL,
    date_creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_creation) IS NOT NULL AND datetime(date_creation) = date_creation),
    date_modification TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_modification) IS NOT NULL AND datetime(date_modification) = date_modification),
    parent INTEGER NOT NULL DEFAULT 0, -- ID de la page parent
    revision INTEGER NOT NULL DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte)
    droit_lecture INTEGER NOT NULL DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe)
    droit_ecriture INTEGER NOT NULL DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe)
);

CREATE UNIQUE INDEX IF NOT EXISTS wiki_uri ON wiki_pages (uri);

CREATE VIRTUAL TABLE IF NOT EXISTS wiki_recherche USING fts4
-- Table dupliquée pour chercher une page
(
    id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire
    titre TEXT NOT NULL,
    contenu TEXT NULL, -- Contenu de la dernière révision
    FOREIGN KEY (id) REFERENCES wiki_pages(id)
);

CREATE TABLE IF NOT EXISTS wiki_revisions
-- Révisions du contenu des pages
(
    id_page INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE,
    revision INTEGER NULL,

    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL,

    contenu TEXT NOT NULL,
    modification TEXT NULL, -- Description des modifications effectuées
    chiffrement INTEGER NOT NULL DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),

    PRIMARY KEY(id_page, revision)
);

CREATE INDEX IF NOT EXISTS wiki_revisions_id_page ON wiki_revisions (id_page);
CREATE INDEX IF NOT EXISTS wiki_revisions_id_auteur ON wiki_revisions (id_auteur);

-- Triggers pour synchro avec table wiki_pages
CREATE TRIGGER IF NOT EXISTS wiki_recherche_delete AFTER DELETE ON wiki_pages
    BEGIN
        DELETE FROM wiki_recherche WHERE id = old.id;
    END;

CREATE TRIGGER IF NOT EXISTS wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages
    BEGIN
        UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id;
    END;

-- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1
    BEGIN
        UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page;
    END;

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;

/*
CREATE TABLE wiki_suivi
-- Suivi des pages
(
    id_membre INTEGER NOT NULL,
    id_page INTEGER NOT NULL,

    PRIMARY KEY (id_membre, id_page),

    FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
    FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire
);
*/

--
-- COMPTA
--

CREATE TABLE IF NOT EXISTS compta_exercices
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    libelle TEXT NOT NULL,

    debut TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(debut) IS NOT NULL AND date(debut) = debut),
    fin TEXT NULL DEFAULT NULL CHECK (fin IS NULL OR (date(fin) IS NOT NULL AND date(fin) = fin)),

    cloture INTEGER NOT NULL DEFAULT 0
);


CREATE TABLE IF NOT EXISTS compta_comptes
-- Plan comptable
(
    id TEXT NOT NULL PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
    parent TEXT NOT NULL DEFAULT 0,

    libelle TEXT NOT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
    desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
);

CREATE INDEX IF NOT EXISTS compta_comptes_parent ON compta_comptes (parent);

CREATE TABLE IF NOT EXISTS compta_comptes_bancaires
-- Comptes bancaires
(
    id TEXT NOT NULL PRIMARY KEY,

    banque TEXT NOT NULL,

    iban TEXT NULL,
    bic TEXT NULL,

    FOREIGN KEY(id) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS compta_projets
-- Projets (compta analytique)
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS compta_journal
-- Journal des opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL,
    remarques TEXT NULL,
    numero_piece TEXT NULL, -- N° de pièce comptable

    montant REAL NOT NULL,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    moyen_paiement TEXT NULL,
    numero_cheque TEXT NULL,

    compte_debit TEXT NULL, -- N° du compte dans le plan, NULL est utilisé pour une opération qui vient d'un exercice précédent
    compte_credit TEXT NULL, -- N° du compte dans le plan

    id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
    id_auteur INTEGER NULL,
    id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
    id_projet INTEGER NULL,

    FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
    FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
    FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
    FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
    FOREIGN KEY(id_auteur) REFERENCES membres(id) ON DELETE SET NULL,
    FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) ON DELETE SET NULL,
    FOREIGN KEY(id_projet) REFERENCES compta_projets(id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX IF NOT EXISTS compta_operations_date ON compta_journal (date);
CREATE INDEX IF NOT EXISTS compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX IF NOT EXISTS compta_operations_auteur ON compta_journal (id_auteur);

CREATE TABLE IF NOT EXISTS compta_moyens_paiement
-- Moyens de paiement
(
    code TEXT NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL
);

--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');

CREATE TABLE IF NOT EXISTS compta_categories
-- Catégories pour simplifier le plan comptable
(
    id INTEGER NOT NULL PRIMARY KEY,
    type INTEGER NOT NULL DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)

    intitule TEXT NOT NULL,
    description TEXT NULL,

    compte TEXT NOT NULL, -- Compte affecté par cette catégorie

    FOREIGN KEY(compte) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,
    auteur TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
    id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    id_auteur INTEGER NULL REFERENCES membres (id)
);

CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
    datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(datetime) IS NOT NULL AND datetime(datetime) = datetime), -- Date d'ajout ou mise à jour du fichier
    id_contenu INTEGER NOT NULL REFERENCES fichiers_contenu (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS fichiers_date ON fichiers (datetime);

CREATE TABLE IF NOT EXISTS fichiers_contenu
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier
    taille INTEGER NOT NULL, -- Taille en octets
    contenu BLOB NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES membres (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES wiki_pages (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

Modified archives/0.8.3_migration.sql from [68c67cd1a9] to [78f64dc820].

1
2
3
4
5
6
7
8
9
10
11
-- Ajout d'une clause ON DELETE SET NULL sur la table cotisations
ALTER TABLE cotisations_membres RENAME TO cotisations_membres_old;

-- Création des tables mises à jour (et de leurs index)
.read schema.sql

-- Copie des données
INSERT INTO cotisations_membres SELECT * FROM cotisations_membres_old;

-- Suppression des anciennes tables
DROP TABLE cotisations_membres_old;




|






1
2
3
4
5
6
7
8
9
10
11
-- Ajout d'une clause ON DELETE SET NULL sur la table cotisations
ALTER TABLE cotisations_membres RENAME TO cotisations_membres_old;

-- Création des tables mises à jour (et de leurs index)
.read 0.8.3_schema.sql

-- Copie des données
INSERT INTO cotisations_membres SELECT * FROM cotisations_membres_old;

-- Suppression des anciennes tables
DROP TABLE cotisations_membres_old;

Added archives/0.8.3_schema.sql version [80be656e5d].









































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);

-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
-- compta_categorie_cotisations => id_categorie
-- compta_categorie_dons => id_categorie

CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,
    description TEXT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) ON DELETE SET NULL
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY NOT NULL,
    id_categorie_compta INTEGER NULL REFERENCES compta_categories (id) ON DELETE SET NULL, -- NULL si le type n'est pas associé automatiquement à la compta

    intitule TEXT NOT NULL,
    description TEXT NULL,
    montant REAL NOT NULL,

    duree INTEGER NULL, -- En jours
    debut TEXT NULL, -- timestamp
    fin TEXT NULL
);

CREATE TABLE IF NOT EXISTS cotisations_membres
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)
);

CREATE UNIQUE INDEX IF NOT EXISTS cm_unique ON cotisations_membres (id_membre, id_cotisation, date);

CREATE TABLE IF NOT EXISTS membres_operations
-- Liaison des enregistrement des paiements en compta
(
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_operation INTEGER NOT NULL REFERENCES compta_journal (id) ON DELETE CASCADE,
    id_cotisation INTEGER NULL REFERENCES cotisations_membres (id) ON DELETE SET NULL,

    PRIMARY KEY (id_membre, id_operation)
);

CREATE TABLE IF NOT EXISTS rappels
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    sujet TEXT NOT NULL,
    texte TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,
    id_rappel INTEGER NULL REFERENCES rappels (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),

    media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);

--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki
(
    id INTEGER PRIMARY KEY NOT NULL,
    uri TEXT NOT NULL, -- URI unique (équivalent NomPageWiki)
    titre TEXT NOT NULL,
    date_creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_creation) IS NOT NULL AND datetime(date_creation) = date_creation),
    date_modification TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_modification) IS NOT NULL AND datetime(date_modification) = date_modification),
    parent INTEGER NOT NULL DEFAULT 0, -- ID de la page parent
    revision INTEGER NOT NULL DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte)
    droit_lecture INTEGER NOT NULL DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe)
    droit_ecriture INTEGER NOT NULL DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe)
);

CREATE UNIQUE INDEX IF NOT EXISTS wiki_uri ON wiki_pages (uri);

CREATE VIRTUAL TABLE IF NOT EXISTS wiki_recherche USING fts4
-- Table dupliquée pour chercher une page
(
    id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire
    titre TEXT NOT NULL,
    contenu TEXT NULL, -- Contenu de la dernière révision
    FOREIGN KEY (id) REFERENCES wiki_pages(id)
);

CREATE TABLE IF NOT EXISTS wiki_revisions
-- Révisions du contenu des pages
(
    id_page INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE,
    revision INTEGER NULL,

    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL,

    contenu TEXT NOT NULL,
    modification TEXT NULL, -- Description des modifications effectuées
    chiffrement INTEGER NOT NULL DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),

    PRIMARY KEY(id_page, revision)
);

CREATE INDEX IF NOT EXISTS wiki_revisions_id_page ON wiki_revisions (id_page);
CREATE INDEX IF NOT EXISTS wiki_revisions_id_auteur ON wiki_revisions (id_auteur);

-- Triggers pour synchro avec table wiki_pages
CREATE TRIGGER IF NOT EXISTS wiki_recherche_delete AFTER DELETE ON wiki_pages
    BEGIN
        DELETE FROM wiki_recherche WHERE id = old.id;
    END;

CREATE TRIGGER IF NOT EXISTS wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages
    BEGIN
        UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id;
    END;

-- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1
    BEGIN
        UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page;
    END;

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;

/*
CREATE TABLE wiki_suivi
-- Suivi des pages
(
    id_membre INTEGER NOT NULL,
    id_page INTEGER NOT NULL,

    PRIMARY KEY (id_membre, id_page),

    FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
    FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire
);
*/

--
-- COMPTA
--

CREATE TABLE IF NOT EXISTS compta_exercices
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    libelle TEXT NOT NULL,

    debut TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(debut) IS NOT NULL AND date(debut) = debut),
    fin TEXT NULL DEFAULT NULL CHECK (fin IS NULL OR (date(fin) IS NOT NULL AND date(fin) = fin)),

    cloture INTEGER NOT NULL DEFAULT 0
);


CREATE TABLE IF NOT EXISTS compta_comptes
-- Plan comptable
(
    id TEXT NOT NULL PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
    parent TEXT NOT NULL DEFAULT 0,

    libelle TEXT NOT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
    desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
);

CREATE INDEX IF NOT EXISTS compta_comptes_parent ON compta_comptes (parent);

CREATE TABLE IF NOT EXISTS compta_comptes_bancaires
-- Comptes bancaires
(
    id TEXT NOT NULL PRIMARY KEY,

    banque TEXT NOT NULL,

    iban TEXT NULL,
    bic TEXT NULL,

    FOREIGN KEY(id) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS compta_projets
-- Projets (compta analytique)
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS compta_journal
-- Journal des opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL,
    remarques TEXT NULL,
    numero_piece TEXT NULL, -- N° de pièce comptable

    montant REAL NOT NULL,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    moyen_paiement TEXT NULL,
    numero_cheque TEXT NULL,

    compte_debit TEXT NULL, -- N° du compte dans le plan, NULL est utilisé pour une opération qui vient d'un exercice précédent
    compte_credit TEXT NULL, -- N° du compte dans le plan

    id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
    id_auteur INTEGER NULL,
    id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
    id_projet INTEGER NULL,

    FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
    FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
    FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
    FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
    FOREIGN KEY(id_auteur) REFERENCES membres(id) ON DELETE SET NULL,
    FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) ON DELETE SET NULL,
    FOREIGN KEY(id_projet) REFERENCES compta_projets(id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX IF NOT EXISTS compta_operations_date ON compta_journal (date);
CREATE INDEX IF NOT EXISTS compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX IF NOT EXISTS compta_operations_auteur ON compta_journal (id_auteur);

CREATE TABLE IF NOT EXISTS compta_moyens_paiement
-- Moyens de paiement
(
    code TEXT NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL
);

--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');

CREATE TABLE IF NOT EXISTS compta_categories
-- Catégories pour simplifier le plan comptable
(
    id INTEGER NOT NULL PRIMARY KEY,
    type INTEGER NOT NULL DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)

    intitule TEXT NOT NULL,
    description TEXT NULL,

    compte TEXT NOT NULL, -- Compte affecté par cette catégorie

    FOREIGN KEY(compte) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,
    auteur TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
    id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    id_auteur INTEGER NULL REFERENCES membres (id)
);

CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
    datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(datetime) IS NOT NULL AND datetime(datetime) = datetime), -- Date d'ajout ou mise à jour du fichier
    id_contenu INTEGER NOT NULL REFERENCES fichiers_contenu (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS fichiers_date ON fichiers (datetime);

CREATE TABLE IF NOT EXISTS fichiers_contenu
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier
    taille INTEGER NOT NULL, -- Taille en octets
    contenu BLOB NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES membres (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES wiki_pages (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

Name change from src/include/data/0.8.4.sql to archives/0.8.4_migration.sql.

Modified archives/0.9.0_migration.sql from [9f23495ff4] to [90ab72f7f4].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- Désactivation de l'accès aux membres, pour les groupes qui n'avaient que le droit de lecture
-- car maintenant ce droit permet de voir les fiches de membres complètes
UPDATE membres_categories SET droit_membres = 0 WHERE droit_membres = 1;

-- Suppression de la colonne description des catégories
ALTER TABLE membres_categories RENAME TO membres_categories_old;

-- Mise à jour table compta_rapprochement: la foreign key sur membres est passée
-- à ON DELETE SET NULL
ALTER TABLE compta_rapprochement RENAME TO compta_rapprochement_old;

-- Re-créer la table
-- Créer également les nouvelles tables email
.read schema.sql

-- Copie des données, sauf la colonne description
INSERT INTO membres_categories SELECT id, nom, droit_wiki,
	droit_membres, droit_compta, droit_inscription,
	droit_connexion, droit_config, cacher,
	id_cotisation_obligatoire FROM membres_categories_old;














|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- Désactivation de l'accès aux membres, pour les groupes qui n'avaient que le droit de lecture
-- car maintenant ce droit permet de voir les fiches de membres complètes
UPDATE membres_categories SET droit_membres = 0 WHERE droit_membres = 1;

-- Suppression de la colonne description des catégories
ALTER TABLE membres_categories RENAME TO membres_categories_old;

-- Mise à jour table compta_rapprochement: la foreign key sur membres est passée
-- à ON DELETE SET NULL
ALTER TABLE compta_rapprochement RENAME TO compta_rapprochement_old;

-- Re-créer la table
-- Créer également les nouvelles tables email
.read 0.9.0_schema.sql

-- Copie des données, sauf la colonne description
INSERT INTO membres_categories SELECT id, nom, droit_wiki,
	droit_membres, droit_compta, droit_inscription,
	droit_connexion, droit_config, cacher,
	id_cotisation_obligatoire FROM membres_categories_old;

Added archives/0.9.0_schema.sql version [9f23495ff4].







































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-- Désactivation de l'accès aux membres, pour les groupes qui n'avaient que le droit de lecture
-- car maintenant ce droit permet de voir les fiches de membres complètes
UPDATE membres_categories SET droit_membres = 0 WHERE droit_membres = 1;

-- Suppression de la colonne description des catégories
ALTER TABLE membres_categories RENAME TO membres_categories_old;

-- Mise à jour table compta_rapprochement: la foreign key sur membres est passée
-- à ON DELETE SET NULL
ALTER TABLE compta_rapprochement RENAME TO compta_rapprochement_old;

-- Re-créer la table
-- Créer également les nouvelles tables email
.read schema.sql

-- Copie des données, sauf la colonne description
INSERT INTO membres_categories SELECT id, nom, droit_wiki,
	droit_membres, droit_compta, droit_inscription,
	droit_connexion, droit_config, cacher,
	id_cotisation_obligatoire FROM membres_categories_old;

-- Suppression des anciennes tables
DROP TABLE membres_categories_old;

-- Migration des données
INSERT INTO compta_rapprochement SELECT * FROM compta_rapprochement_old;
DROP TABLE compta_rapprochement_old;

-- Cette variable n'est plus utilisée
DELETE FROM config WHERE cle = 'email_envoi_automatique';

ALTER TABLE plugins ADD COLUMN menu_condition TEXT NULL;

-- Supprimer le début dans le nom des plugins
UPDATE plugins_signaux SET callback = replace(callback, 'Garradin\Plugin\', '');

Modified archives/0.9.1_migration.sql from [8d911832a5] to [6227606b97].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Il manquait une clause ON DELETE SET NULL sur la foreign key
-- de cotisations quand on faisait une mise à jour depuis une
-- ancienne version
ALTER TABLE cotisations RENAME TO cotisations_old;

.read schema.sql

INSERT INTO cotisations SELECT * FROM cotisations_old;

DROP TABLE cotisations_old;

-- Changer le compte des reports automatiques
UPDATE compta_journal SET compte_debit = '890' WHERE compte_debit IS NULL;
UPDATE compta_journal SET compte_credit = '890' WHERE compte_credit IS NULL;





|








1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Il manquait une clause ON DELETE SET NULL sur la foreign key
-- de cotisations quand on faisait une mise à jour depuis une
-- ancienne version
ALTER TABLE cotisations RENAME TO cotisations_old;

.read 0.9.1_schema.sql

INSERT INTO cotisations SELECT * FROM cotisations_old;

DROP TABLE cotisations_old;

-- Changer le compte des reports automatiques
UPDATE compta_journal SET compte_debit = '890' WHERE compte_debit IS NULL;
UPDATE compta_journal SET compte_credit = '890' WHERE compte_credit IS NULL;

Added archives/0.9.1_schema.sql version [efe750515a].

































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);

-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
-- compta_categorie_cotisations => id_categorie
-- compta_categorie_dons => id_categorie

CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) ON DELETE SET NULL
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY NOT NULL,
    id_categorie_compta INTEGER NULL REFERENCES compta_categories (id) ON DELETE SET NULL, -- NULL si le type n'est pas associé automatiquement à la compta

    intitule TEXT NOT NULL,
    description TEXT NULL,
    montant REAL NOT NULL,

    duree INTEGER NULL, -- En jours
    debut TEXT NULL, -- timestamp
    fin TEXT NULL
);

CREATE TABLE IF NOT EXISTS cotisations_membres
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)
);

CREATE UNIQUE INDEX IF NOT EXISTS cm_unique ON cotisations_membres (id_membre, id_cotisation, date);

CREATE TABLE IF NOT EXISTS membres_operations
-- Liaison des enregistrement des paiements en compta
(
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_operation INTEGER NOT NULL REFERENCES compta_journal (id) ON DELETE CASCADE,
    id_cotisation INTEGER NULL REFERENCES cotisations_membres (id) ON DELETE SET NULL,

    PRIMARY KEY (id_membre, id_operation)
);

CREATE TABLE IF NOT EXISTS rappels
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    sujet TEXT NOT NULL,
    texte TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,
    id_rappel INTEGER NULL REFERENCES rappels (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),

    media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);

--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki
(
    id INTEGER PRIMARY KEY NOT NULL,
    uri TEXT NOT NULL, -- URI unique (équivalent NomPageWiki)
    titre TEXT NOT NULL,
    date_creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_creation) IS NOT NULL AND datetime(date_creation) = date_creation),
    date_modification TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_modification) IS NOT NULL AND datetime(date_modification) = date_modification),
    parent INTEGER NOT NULL DEFAULT 0, -- ID de la page parent
    revision INTEGER NOT NULL DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte)
    droit_lecture INTEGER NOT NULL DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe)
    droit_ecriture INTEGER NOT NULL DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe)
);

CREATE UNIQUE INDEX IF NOT EXISTS wiki_uri ON wiki_pages (uri);

CREATE VIRTUAL TABLE IF NOT EXISTS wiki_recherche USING fts4
-- Table dupliquée pour chercher une page
(
    id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire
    titre TEXT NOT NULL,
    contenu TEXT NULL, -- Contenu de la dernière révision
    FOREIGN KEY (id) REFERENCES wiki_pages(id)
);

CREATE TABLE IF NOT EXISTS wiki_revisions
-- Révisions du contenu des pages
(
    id_page INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE,
    revision INTEGER NULL,

    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL,

    contenu TEXT NOT NULL,
    modification TEXT NULL, -- Description des modifications effectuées
    chiffrement INTEGER NOT NULL DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),

    PRIMARY KEY(id_page, revision)
);

CREATE INDEX IF NOT EXISTS wiki_revisions_id_page ON wiki_revisions (id_page);
CREATE INDEX IF NOT EXISTS wiki_revisions_id_auteur ON wiki_revisions (id_auteur);

-- Triggers pour synchro avec table wiki_pages
CREATE TRIGGER IF NOT EXISTS wiki_recherche_delete AFTER DELETE ON wiki_pages
    BEGIN
        DELETE FROM wiki_recherche WHERE id = old.id;
    END;

CREATE TRIGGER IF NOT EXISTS wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages
    BEGIN
        UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id;
    END;

-- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1
    BEGIN
        UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page;
    END;

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;

/*
CREATE TABLE wiki_suivi
-- Suivi des pages
(
    id_membre INTEGER NOT NULL,
    id_page INTEGER NOT NULL,

    PRIMARY KEY (id_membre, id_page),

    FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
    FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire
);
*/

--
-- COMPTA
--

CREATE TABLE IF NOT EXISTS compta_exercices
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    libelle TEXT NOT NULL,

    debut TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(debut) IS NOT NULL AND date(debut) = debut),
    fin TEXT NULL DEFAULT NULL CHECK (fin IS NULL OR (date(fin) IS NOT NULL AND date(fin) = fin)),

    cloture INTEGER NOT NULL DEFAULT 0
);


CREATE TABLE IF NOT EXISTS compta_comptes
-- Plan comptable
(
    id TEXT NOT NULL PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
    parent TEXT NOT NULL DEFAULT 0,

    libelle TEXT NOT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
    desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
);

CREATE INDEX IF NOT EXISTS compta_comptes_parent ON compta_comptes (parent);

CREATE TABLE IF NOT EXISTS compta_comptes_bancaires
-- Comptes bancaires
(
    id TEXT NOT NULL PRIMARY KEY,

    banque TEXT NOT NULL,

    iban TEXT NULL,
    bic TEXT NULL,

    FOREIGN KEY(id) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS compta_projets
-- Projets (compta analytique)
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS compta_journal
-- Journal des opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL,
    remarques TEXT NULL,
    numero_piece TEXT NULL, -- N° de pièce comptable

    montant REAL NOT NULL,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    moyen_paiement TEXT NULL,
    numero_cheque TEXT NULL,

    compte_debit TEXT NULL, -- N° du compte dans le plan, NULL est utilisé pour une opération qui vient d'un exercice précédent
    compte_credit TEXT NULL, -- N° du compte dans le plan

    id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
    id_auteur INTEGER NULL,
    id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
    id_projet INTEGER NULL,

    FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
    FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
    FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
    FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
    FOREIGN KEY(id_auteur) REFERENCES membres(id) ON DELETE SET NULL,
    FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) ON DELETE SET NULL,
    FOREIGN KEY(id_projet) REFERENCES compta_projets(id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX IF NOT EXISTS compta_operations_date ON compta_journal (date);
CREATE INDEX IF NOT EXISTS compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX IF NOT EXISTS compta_operations_auteur ON compta_journal (id_auteur);

CREATE TABLE IF NOT EXISTS compta_moyens_paiement
-- Moyens de paiement
(
    code TEXT NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL
);

--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');

CREATE TABLE IF NOT EXISTS compta_categories
-- Catégories pour simplifier le plan comptable
(
    id INTEGER NOT NULL PRIMARY KEY,
    type INTEGER NOT NULL DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)

    intitule TEXT NOT NULL,
    description TEXT NULL,

    compte TEXT NOT NULL, -- Compte affecté par cette catégorie

    FOREIGN KEY(compte) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,
    auteur TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    menu_condition TEXT NULL,
    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
    id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
    datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(datetime) IS NOT NULL AND datetime(datetime) = datetime), -- Date d'ajout ou mise à jour du fichier
    id_contenu INTEGER NOT NULL REFERENCES fichiers_contenu (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS fichiers_date ON fichiers (datetime);

CREATE TABLE IF NOT EXISTS fichiers_contenu
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier
    taille INTEGER NOT NULL, -- Taille en octets
    contenu BLOB NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES membres (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES wiki_pages (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta_journal"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);

Added archives/0.9.5_schema.sql version [c8df01c2dd].





























































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);

-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
-- compta_categorie_cotisations => id_categorie
-- compta_categorie_dons => id_categorie

CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) ON DELETE SET NULL
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY NOT NULL,
    id_categorie_compta INTEGER NULL REFERENCES compta_categories (id) ON DELETE SET NULL, -- NULL si le type n'est pas associé automatiquement à la compta

    intitule TEXT NOT NULL,
    description TEXT NULL,
    montant REAL NOT NULL,

    duree INTEGER NULL, -- En jours
    debut TEXT NULL, -- timestamp
    fin TEXT NULL
);

CREATE TABLE IF NOT EXISTS cotisations_membres
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)
);

CREATE UNIQUE INDEX IF NOT EXISTS cm_unique ON cotisations_membres (id_membre, id_cotisation, date);

CREATE TABLE IF NOT EXISTS membres_operations
-- Liaison des enregistrement des paiements en compta
(
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_operation INTEGER NOT NULL REFERENCES compta_journal (id) ON DELETE CASCADE,
    id_cotisation INTEGER NULL REFERENCES cotisations_membres (id) ON DELETE SET NULL,

    PRIMARY KEY (id_membre, id_operation)
);

CREATE TABLE IF NOT EXISTS rappels
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    sujet TEXT NOT NULL,
    texte TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,
    id_rappel INTEGER NULL REFERENCES rappels (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),

    media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);

--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki
(
    id INTEGER PRIMARY KEY NOT NULL,
    uri TEXT NOT NULL, -- URI unique (équivalent NomPageWiki)
    titre TEXT NOT NULL,
    date_creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_creation) IS NOT NULL AND datetime(date_creation) = date_creation),
    date_modification TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_modification) IS NOT NULL AND datetime(date_modification) = date_modification),
    parent INTEGER NOT NULL DEFAULT 0, -- ID de la page parent
    revision INTEGER NOT NULL DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte)
    droit_lecture INTEGER NOT NULL DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe)
    droit_ecriture INTEGER NOT NULL DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe)
);

CREATE UNIQUE INDEX IF NOT EXISTS wiki_uri ON wiki_pages (uri);

CREATE VIRTUAL TABLE IF NOT EXISTS wiki_recherche USING fts4
-- Table dupliquée pour chercher une page
(
    id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire
    titre TEXT NOT NULL,
    contenu TEXT NULL, -- Contenu de la dernière révision
    FOREIGN KEY (id) REFERENCES wiki_pages(id)
);

CREATE TABLE IF NOT EXISTS wiki_revisions
-- Révisions du contenu des pages
(
    id_page INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE,
    revision INTEGER NULL,

    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL,

    contenu TEXT NOT NULL,
    modification TEXT NULL, -- Description des modifications effectuées
    chiffrement INTEGER NOT NULL DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),

    PRIMARY KEY(id_page, revision)
);

CREATE INDEX IF NOT EXISTS wiki_revisions_id_page ON wiki_revisions (id_page);
CREATE INDEX IF NOT EXISTS wiki_revisions_id_auteur ON wiki_revisions (id_auteur);

-- Triggers pour synchro avec table wiki_pages
CREATE TRIGGER IF NOT EXISTS wiki_recherche_delete AFTER DELETE ON wiki_pages
    BEGIN
        DELETE FROM wiki_recherche WHERE id = old.id;
    END;

CREATE TRIGGER IF NOT EXISTS wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages
    BEGIN
        UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id;
    END;

-- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1
    BEGIN
        UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page;
    END;

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;

/*
CREATE TABLE wiki_suivi
-- Suivi des pages
(
    id_membre INTEGER NOT NULL,
    id_page INTEGER NOT NULL,

    PRIMARY KEY (id_membre, id_page),

    FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
    FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire
);
*/

--
-- COMPTA
--

CREATE TABLE IF NOT EXISTS compta_exercices
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    libelle TEXT NOT NULL,

    debut TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(debut) IS NOT NULL AND date(debut) = debut),
    fin TEXT NULL DEFAULT NULL CHECK (fin IS NULL OR (date(fin) IS NOT NULL AND date(fin) = fin)),

    cloture INTEGER NOT NULL DEFAULT 0
);


CREATE TABLE IF NOT EXISTS compta_comptes
-- Plan comptable
(
    id TEXT NOT NULL PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
    parent TEXT NOT NULL DEFAULT 0,

    libelle TEXT NOT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
    desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
);

CREATE INDEX IF NOT EXISTS compta_comptes_parent ON compta_comptes (parent);

CREATE TABLE IF NOT EXISTS compta_comptes_bancaires
-- Comptes bancaires
(
    id TEXT NOT NULL PRIMARY KEY,

    banque TEXT NOT NULL,

    iban TEXT NULL,
    bic TEXT NULL,

    FOREIGN KEY(id) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS compta_projets
-- Projets (compta analytique)
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS compta_journal
-- Journal des opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL,
    remarques TEXT NULL,
    numero_piece TEXT NULL, -- N° de pièce comptable

    montant REAL NOT NULL,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    moyen_paiement TEXT NULL,
    numero_cheque TEXT NULL,

    compte_debit TEXT NULL, -- N° du compte dans le plan, NULL est utilisé pour une opération qui vient d'un exercice précédent
    compte_credit TEXT NULL, -- N° du compte dans le plan

    id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
    id_auteur INTEGER NULL,
    id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
    id_projet INTEGER NULL,

    FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
    FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
    FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
    FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
    FOREIGN KEY(id_auteur) REFERENCES membres(id) ON DELETE SET NULL,
    FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) ON DELETE SET NULL,
    FOREIGN KEY(id_projet) REFERENCES compta_projets(id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX IF NOT EXISTS compta_operations_date ON compta_journal (date);
CREATE INDEX IF NOT EXISTS compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX IF NOT EXISTS compta_operations_auteur ON compta_journal (id_auteur);

CREATE TABLE IF NOT EXISTS compta_moyens_paiement
-- Moyens de paiement
(
    code TEXT NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL
);

--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');

CREATE TABLE IF NOT EXISTS compta_categories
-- Catégories pour simplifier le plan comptable
(
    id INTEGER NOT NULL PRIMARY KEY,
    type INTEGER NOT NULL DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)

    intitule TEXT NOT NULL,
    description TEXT NULL,

    compte TEXT NOT NULL, -- Compte affecté par cette catégorie

    FOREIGN KEY(compte) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,
    auteur TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    menu_condition TEXT NULL,
    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
    id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
    datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(datetime) IS NOT NULL AND datetime(datetime) = datetime), -- Date d'ajout ou mise à jour du fichier
    id_contenu INTEGER NOT NULL REFERENCES fichiers_contenu (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS fichiers_date ON fichiers (datetime);

CREATE TABLE IF NOT EXISTS fichiers_contenu
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier
    taille INTEGER NOT NULL, -- Taille en octets
    contenu BLOB NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES membres (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES wiki_pages (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta_journal"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);


CREATE TABLE IF NOT EXISTS compromised_passwords_cache
-- Cache des hash de mots de passe compromis
(
    hash TEXT NOT NULL PRIMARY KEY
);

CREATE TABLE IF NOT EXISTS compromised_passwords_cache_ranges
-- Cache des préfixes de mots de passe compromis
(
    prefix TEXT NOT NULL PRIMARY KEY,
    date INTEGER NOT NULL
);

Modified archives/plan_comptable.json from [4d7e33a414] to [c496246466].

1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
    },
    "890": {
        "code": 890,
        "nom": "Bilan d'ouverture",
        "parent": 89,
        "position": 3
    },
    "890": {
        "code": 891,
        "nom": "Bilan de clôture",
        "parent": 89,
        "position": 3
    },
    "9": {
        "code": 9,
        "nom": "Classe 9 \u2014 Comptes analytiques",
        "parent": 0,
        "position": 12
    }
}







|












1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
    },
    "890": {
        "code": 890,
        "nom": "Bilan d'ouverture",
        "parent": 89,
        "position": 3
    },
    "891": {
        "code": 891,
        "nom": "Bilan de clôture",
        "parent": 89,
        "position": 3
    },
    "9": {
        "code": 9,
        "nom": "Classe 9 \u2014 Comptes analytiques",
        "parent": 0,
        "position": 12
    }
}

Modified debian/config.debian.php from [2a98e1b5ea] to [1cd45bbd87].

12
13
14
15
16
17
18




19
20
21
22
23
24
25
26
27
28
29
30

31

32
33
34
35
36
37
38
39
40
41
42
43

44
45


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66





67


68


69
70


71
72



73
74
75

76


77
		$_ENV['XDG_CONFIG_HOME'] = $home . '/.config';
	}

	if (!file_exists($_ENV['XDG_CONFIG_HOME'] . '/garradin'))
	{
		mkdir($_ENV['XDG_CONFIG_HOME'] . '/garradin', 0700, true);
	}





	// Data directory: where the data will go
	if (empty($_ENV['XDG_DATA_HOME']))
	{
		$_ENV['XDG_DATA_HOME'] = $home . '/.local/share';
	}

	if (!file_exists($_ENV['XDG_DATA_HOME'] . '/garradin'))
	{
		mkdir($_ENV['XDG_DATA_HOME'] . '/garradin', 0700, true);
	}


	define('Garradin\DATA_ROOT', $_ENV['XDG_DATA_HOME'] . '/garradin');


	// Cache directory: temporary stuff
	if (empty($_ENV['XDG_CACHE_HOME']))
	{
		$_ENV['XDG_CACHE_HOME'] = $home . '/.cache';
	}

	if (!file_exists($_ENV['XDG_CACHE_HOME'] . '/garradin'))
	{
		mkdir($_ENV['XDG_CACHE_HOME'] . '/garradin', 0700, true);
	}


	define('Garradin\CACHE_ROOT', $_ENV['XDG_CACHE_HOME'] . '/garradin');



	$last_file = $_ENV['XDG_CONFIG_HOME'] . '/garradin/last';

	if ($_ENV['GARRADIN_STANDALONE'] != 1)
	{
		$last_sqlite = trim($_ENV['GARRADIN_STANDALONE']);
	}
	else if (file_exists($last_file))
	{
		$last_sqlite = trim(file_get_contents($last_file));
	}
	else
	{
		$last_sqlite = $_ENV['XDG_DATA_HOME'] . '/garradin/association.sqlite';
	}

	file_put_contents($last_file, $last_sqlite);

	$secret_file = $_ENV['XDG_CONFIG_HOME'] . '/garradin/secret';

	if (!file_exists($secret_file))
	{





		$random = function_exists('random_bytes') ? random_bytes(64) : mt_rand();


		$random = sha1($random . $secret_file);



		file_put_contents($secret_file, $random);


	}




	define('Garradin\SECRET_KEY', trim(file_get_contents($secret_file)));

	define('Garradin\DB_FILE', $last_sqlite);

	define('Garradin\LOCAL_LOGIN', 1);


}







>
>
>
>












>
|
>












>
|
|
>
>
|

|
|
|
|
|
|
|
|
|
|
|
|

|

|
|
<
|
>
>
>
>
>
|
>
>
|
>
>
|
|
>
>

|
>
>
>
|
|
<
>
|
>
>

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

97
98
99
100
101
		$_ENV['XDG_CONFIG_HOME'] = $home . '/.config';
	}

	if (!file_exists($_ENV['XDG_CONFIG_HOME'] . '/garradin'))
	{
		mkdir($_ENV['XDG_CONFIG_HOME'] . '/garradin', 0700, true);
	}

	if (file_exists($_ENV['XDG_CONFIG_HOME'] . '/garradin/config.local.php')) {
		require_once $_ENV['XDG_CONFIG_HOME'] . '/garradin/config.local.php';
	}

	// Data directory: where the data will go
	if (empty($_ENV['XDG_DATA_HOME']))
	{
		$_ENV['XDG_DATA_HOME'] = $home . '/.local/share';
	}

	if (!file_exists($_ENV['XDG_DATA_HOME'] . '/garradin'))
	{
		mkdir($_ENV['XDG_DATA_HOME'] . '/garradin', 0700, true);
	}

	if (!defined('Garradin\DATA_ROOT')) {
		define('Garradin\DATA_ROOT', $_ENV['XDG_DATA_HOME'] . '/garradin');
	}

	// Cache directory: temporary stuff
	if (empty($_ENV['XDG_CACHE_HOME']))
	{
		$_ENV['XDG_CACHE_HOME'] = $home . '/.cache';
	}

	if (!file_exists($_ENV['XDG_CACHE_HOME'] . '/garradin'))
	{
		mkdir($_ENV['XDG_CACHE_HOME'] . '/garradin', 0700, true);
	}

	if (!defined('Garradin\CACHE_ROOT')) {
		define('Garradin\CACHE_ROOT', $_ENV['XDG_CACHE_HOME'] . '/garradin');
	}

	if (!defined('Garradin\DB_FILE')) {
		$last_file = $_ENV['XDG_CONFIG_HOME'] . '/garradin/last';

		if ($_ENV['GARRADIN_STANDALONE'] != 1)
		{
			$last_sqlite = trim($_ENV['GARRADIN_STANDALONE']);
		}
		else if (file_exists($last_file))
		{
			$last_sqlite = trim(file_get_contents($last_file));
		}
		else
		{
			$last_sqlite = $_ENV['XDG_DATA_HOME'] . '/garradin/association.sqlite';
		}

		file_put_contents($last_file, $last_sqlite);

		define('Garradin\DB_FILE', $last_sqlite);
	}


	if (!defined('Garradin\LOCAL_LOGIN')) {
		define('Garradin\LOCAL_LOGIN', true);
	}
}
elseif (isset($_SERVER['SERVER_NAME'])) {
	if (file_exists('/etc/garradin/config.php')) {
		require_once '/etc/garradin/config.php';
	}

	if (!defined('Garradin\DATA_ROOT')) {
		define('Garradin\DATA_ROOT', '/var/lib/garradin');
	}

	if (!defined('Garradin\CACHE_ROOT')) {
		define('Garradin\CACHE_ROOT', '/var/cache/garradin');
	}
}

if (!defined('Garradin\SECRET_KEY')) {
	if (file_exists(CACHE_ROOT . '/key')) {
		define('Garradin\SECRET_KEY', trim(file_get_contents(CACHE_ROOT . '/key')));
	}

	else {
		define('Garradin\SECRET_KEY', base64_encode(random_bytes(64)));
		file_put_contents(CACHE_ROOT . '/key', SECRET_KEY);
	}
}

Modified debian/makedeb.sh from [242f467b69] to [e75e803576].

34
35
36
37
38
39
40
41
42
43




44
45
46
47
48
49
50
51
52
mkdir -p "${DEBLOCALPREFIX}/share/applications"
cp ${THISDIR}/garradin.desktop "${DEBLOCALPREFIX}/share/applications/"

CODEDIR=${DEBLOCALPREFIX}/share/${PACKAGE_DEBNAME}
mkdir -p ${CODEDIR}
cp -r ${SRCDIR}/* ${CODEDIR}
cp ${THISDIR}/config.debian.php ${CODEDIR}/config.local.php
rm -rf ${CODEDIR}/*.sqlite ${CODEDIR}/cache ${CODEDIR}/www/squelettes
cp ${THISDIR}/garradin.png "${CODEDIR}"





# Cleaning files that will be copied to /usr/share/doc
rm -f ${CODEDIR}/{README,COPYING}

cd $DEBROOT || {
    echo "Debian dest dir [$DEBROOT] not found. :("
    exit 2
}

rm -fr DEBIAN







|


>
>
>
>

|







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
mkdir -p "${DEBLOCALPREFIX}/share/applications"
cp ${THISDIR}/garradin.desktop "${DEBLOCALPREFIX}/share/applications/"

CODEDIR=${DEBLOCALPREFIX}/share/${PACKAGE_DEBNAME}
mkdir -p ${CODEDIR}
cp -r ${SRCDIR}/* ${CODEDIR}
cp ${THISDIR}/config.debian.php ${CODEDIR}/config.local.php
rm -rf ${CODEDIR}/*.sqlite ${CODEDIR}/cache ${CODEDIR}/www/squelettes ${CODEDIR}/www/plugins/*
cp ${THISDIR}/garradin.png "${CODEDIR}"

mkdir -p "${DEBROOT}/var/lib/${PACKAGE_DEBNAME}"
mkdir -p "${DEBROOT}/var/cache/${PACKAGE_DEBNAME}"
mkdir -p "${DEBROOT}/etc/${PACKAGE_DEBNAME}"

# Cleaning files that will be copied to /usr/share/doc
#rm -f ${CODEDIR}/../{README.md,COPYING}

cd $DEBROOT || {
    echo "Debian dest dir [$DEBROOT] not found. :("
    exit 2
}

rm -fr DEBIAN
60
61
62
63
64
65
66
67
68
69
70














71
72
73
74
75
76
77
echo "Creating .deb package [${DEBFILE}]..."

echo "Generating md5 sums..."
find ${DEBLOCALPREFIX} -type f -exec md5sum {} \; > DEBIAN/md5sums

true && {
    echo "Generating Debian-specific files..."
    cp ${SRCDIR}/COPYING ${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}/copyright
} || {
	echo "Fail."
	exit 1














}

true && {
    CHANGELOG=${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}/changelog.gz
    cat <<EOF | gzip -c > ${CHANGELOG}
${PACKAGE_DEBNAME} ${PACKAGE_DEB_VERSION}; urgency=low








|



>
>
>
>
>
>
>
>
>
>
>
>
>
>







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
echo "Creating .deb package [${DEBFILE}]..."

echo "Generating md5 sums..."
find ${DEBLOCALPREFIX} -type f -exec md5sum {} \; > DEBIAN/md5sums

true && {
    echo "Generating Debian-specific files..."
    cp ${THISDIR}/../COPYING ${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}/copyright
} || {
	echo "Fail."
	exit 1
}

true && {
    cat <<EOF > DEBIAN/postinst
#!/bin/sh

chown www-data:www-data /var/lib/garradin /var/cache/garradin
chown root:www-data /etc/garradin
chmod g=rX,o= /etc/garradin
chmod ug=rwX,o= /var/lib/garradin /var/cache/garradin
EOF

    chmod +x DEBIAN/postinst

}

true && {
    CHANGELOG=${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}/changelog.gz
    cat <<EOF | gzip -c > ${CHANGELOG}
${PACKAGE_DEBNAME} ${PACKAGE_DEB_VERSION}; urgency=low

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
}

# doc.
DOCDIR=${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}

true && {
    echo "Generating doc..."
    cp ${SRCDIR}/README ${DOCDIR}
    a2x --doctype manpage --format manpage ${THISDIR}/manpage.txt
    mkdir -p ${DEBLOCALPREFIX}/share/man/man1
    gzip -c ${THISDIR}/garradin.1 > ${DEBLOCALPREFIX}/share/man/man1/${PACKAGE_DEBNAME}.1.gz
    rm -f ${THISDIR}/garradin.1
} || {
    echo "Fail."
    exit 1
}

true && {
    CONTROL=DEBIAN/control
    echo "Generating ${CONTROL}..."
    cat <<EOF > ${CONTROL}
Package: ${PACKAGE_DEBNAME}
Section: web
Priority: optional
Maintainer: Garradin <garradin@kd2.org>
Architecture: ${DEB_ARCH_NAME}
Depends: dash | bash, php5-cli (>=5.6) | php-cli (>=7.0), php5-sqlite | php-sqlite3
Version: ${PACKAGE_DEB_VERSION}
Suggests: www-browser, php-gd
Homepage: http://dev.kd2.org/garradin/
Description: Garradin is a tool to manage non-profit organizations.
 It's only available in french.
Description-fr: Gestionnaire d'association en interface web ou CLI.
 Garradin est un gestionnaire d'association à but non lucratif.







|


















|







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
}

# doc.
DOCDIR=${DEBLOCALPREFIX}/share/doc/${PACKAGE_DEBNAME}

true && {
    echo "Generating doc..."
    cp ${THISDIR}/../README.md ${DOCDIR}
    a2x --doctype manpage --format manpage ${THISDIR}/manpage.txt
    mkdir -p ${DEBLOCALPREFIX}/share/man/man1
    gzip -c ${THISDIR}/garradin.1 > ${DEBLOCALPREFIX}/share/man/man1/${PACKAGE_DEBNAME}.1.gz
    rm -f ${THISDIR}/garradin.1
} || {
    echo "Fail."
    exit 1
}

true && {
    CONTROL=DEBIAN/control
    echo "Generating ${CONTROL}..."
    cat <<EOF > ${CONTROL}
Package: ${PACKAGE_DEBNAME}
Section: web
Priority: optional
Maintainer: Garradin <garradin@kd2.org>
Architecture: ${DEB_ARCH_NAME}
Depends: dash | bash, php-cli (>=7.2), php-sqlite3
Version: ${PACKAGE_DEB_VERSION}
Suggests: www-browser, php-gd
Homepage: http://dev.kd2.org/garradin/
Description: Garradin is a tool to manage non-profit organizations.
 It's only available in french.
Description-fr: Gestionnaire d'association en interface web ou CLI.
 Garradin est un gestionnaire d'association à but non lucratif.

Added doc/dev/odoo_accounts.sql version [6c994995cd].















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
-- Schéma Odoo (PgSQL) pour info sur la compta
-- Pour les écritures, deux tables : move et move_line
-- Utilisation de deux colonnes "debit" et "credit"

-- https://github.com/odoo/odoo/blob/11.0/addons/account/data/data_account_type.xml
-- https://www.odoo.com/documentation/11.0/webservices/localization.html
CREATE TABLE public.account_account_type (
    id integer NOT NULL,
    name character varying NOT NULL,
    include_initial_balance boolean,
    type character varying NOT NULL,
    note text,
    create_uid integer,
    create_date timestamp without time zone,
    write_uid integer,
    write_date timestamp without time zone
);

COPY public.account_account_type (id, name, include_initial_balance, type, note, create_uid, create_date, write_uid, write_date) FROM stdin;
1	Receivable	t	receivable	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
2	Payable	t	payable	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
3	Bank and Cash	t	liquidity	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
4	Credit Card	t	liquidity	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
5	Current Assets	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
6	Non-current Assets	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
7	Prepayments	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
8	Fixed Assets	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
9	Current Liabilities	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
10	Non-current Liabilities	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
11	Equity	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
12	Current Year Earnings	t	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
13	Other Income	f	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
14	Income	f	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
15	Depreciation	f	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
16	Expenses	f	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
17	Cost of Revenue	f	other	\N	1	2019-02-14 15:05:35.697512	1	2019-02-14 15:05:35.697512
\.


COPY public.account_account (id, name, currency_id, code, deprecated, user_type_id, internal_type, last_time_entries_checked, reconcile, note, company_id, group_id, create_uid, create_date, write_uid, write_date) FROM stdin;
1	Virements Internes	\N	580000	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
2	Capital souscrit - non appelé	\N	101100	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
3	Capital souscrit - appelé non versé	\N	101200	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
4	Capital non amorti	\N	101310	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
5	Capital amorti	\N	101320	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
6	Capital souscrit soumis à des réglementations particulières	\N	101800	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
7	Primes d'émission	\N	104100	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
8	Primes de fusion	\N	104200	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
10	Primes de conversion d'obligations en actions	\N	104400	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
11	Bons de souscription d'actions	\N	104500	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
12	Réserve spéciale de réévaluation	\N	105100	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
13	Écart de réévaluation libre	\N	105200	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
15	Écarts de réévaluation (autres opérations légales)	\N	105500	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
16	Autres écarts de réévaluation en France	\N	105700	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
17	Autres écarts de réévaluation à l'étranger	\N	105800	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
18	Réserve légale proprement dite	\N	106110	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
19	Plus-values nettes à long terme	\N	106120	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
20	Réserves indisponibles	\N	106200	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
22	Plus-values nettes à long terme	\N	106410	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
23	Réserves consécutives à l'octroi de subventions d'investissement	\N	106430	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
24	Autres réserves réglementées	\N	106480	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
25	Réserve de propre assureur	\N	106810	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
26	Réserves diverses	\N	106880	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
27	Écarts d'équivalence	\N	107000	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
28	Compte de l'exploitant	\N	108000	f	11	other	\N	f	Capital pour une Entreprise Individuelle	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
29	Actionnaires : capital souscrit - non appelé	\N	109000	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
31	Report à nouveau (solde débiteur)	\N	119000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
30	Report à nouveau (solde créditeur)	\N	110000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
9	Primes d'apport	\N	104300	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
14	Réserve de réévaluation	\N	105300	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
21	Réserves statutaires ou contractuelles	\N	106300	f	11	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
32	Résultat de l'exercice (bénéfice)	\N	120000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
33	Résultat de l'exercice (perte)	\N	129000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
34	Subventions d'équipement - État	\N	131100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
35	Subventions d'équipement - Régions	\N	131200	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
37	Subventions d'équipement - Communes	\N	131400	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
38	Subventions d'équipement - Collectivités publiques	\N	131500	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
39	Subventions d'équipement - Entreprises publiques	\N	131600	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
40	Subventions d'équipement - Entreprises et organismes privés	\N	131700	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
41	Subventions d'équipement - Autres	\N	131800	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
42	Autres subventions d'investissement (même ventilation que celle du compte 131)	\N	138000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
43	Subventions d'équipement inscrites au compte de résultat - État	\N	139110	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
44	Subventions d'équipement inscrites au compte de résultat - Régions	\N	139120	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
45	Subventions d'équipement inscrites au compte de résultat - Départements	\N	139130	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
46	Subventions d'équipement inscrites au compte de résultat - Communes	\N	139140	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
47	Subventions d'équipement inscrites au compte de résultat - Collectivités publiques	\N	139150	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
48	Subventions d'équipement inscrites au compte de résultat - Entreprises publiques	\N	139160	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
49	Subventions d'équipement inscrites au compte de résultat - Entreprises et organismes privés	\N	139170	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
50	Subventions d'équipement inscrites au compte de résultat - Autres	\N	139180	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
51	Autres subventions d'investissement (même ventilation que celle du compte 1391)	\N	139800	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
53	Provisions pour investissement (participation des salariés)	\N	142400	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
54	Provisions réglementées relatives aux stocks - Hausse de prix	\N	143100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
55	Provisions réglementées relatives aux stocks - Fluctuation des cours	\N	143200	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
56	Provisions réglementées relatives aux autres éléments de l'actif	\N	144000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
57	Amortissements dérogatoires	\N	145000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
58	Provision spéciale de réévaluation	\N	146000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
36	Subventions d'équipement - Départements	\N	131300	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
52	Provisions reconstitution des gisements miniers et pétroliers	\N	142300	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
719	Bla bla	\N	512002	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:11:18.063543	1	2019-02-14 15:11:18.063543
59	Plus-values réinvesties	\N	147000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
60	Autres provisions réglementées	\N	148000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
61	Provisions pour litiges	\N	151100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
62	Provisions pour garanties données aux clients	\N	151200	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
64	Provisions pour amendes et pénalités	\N	151400	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
65	Provisions pour pertes de change	\N	151500	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
66	Provisions pour pertes sur contrats	\N	151600	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
67	Autres provisions pour risques	\N	151800	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
69	Provisions pour restructurations	\N	154000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
70	Provisions pour impôts	\N	155000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
71	Provisions pour renouvellement des immobilisations (entreprises concessionnaires)	\N	156000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
72	Provisions pour charges à répartir sur plusieurs exercices - Gros entretien ou grandes révisions	\N	157200	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
73	Provisions pour remises en état	\N	158100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
76	Emprunts auprès des établissements de crédit	\N	164000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
77	Dépôts	\N	165100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
78	Cautionnements	\N	165500	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
79	Participation des salariés aux résultats - Comptes bloqués	\N	166100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
80	Participation des salariés aux résultats - Fonds de participation	\N	166200	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
81	Emprunts et dettes assortis de conditions particulières - Emissions de titres participatifs	\N	167100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
82	Emprunts et dettes assortis de conditions particulières - Avances conditionnées de l'État	\N	167400	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
83	Emprunts et dettes assortis de conditions particulières - Emprunts participatifs	\N	167500	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
84	Autres emprunts et dettes assimilées - Autres emprunts	\N	168100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
85	Autres emprunts et dettes assimilées - Rentes viagères capitalisées	\N	168500	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
86	Autres emprunts et dettes assimilées - Autres dettes	\N	168700	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
74	Emprunts obligataires convertibles	\N	161000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
63	Provisions pour pertes sur marchés à terme	\N	151300	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
68	Provisions pour pensions et obligations similaires	\N	153000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
75	Autres emprunts obligataires	\N	163000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
87	Intérêts courus sur emprunts obligataires convertibles	\N	168810	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
88	Intérêts courus sur autres emprunts obligataires	\N	168830	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
89	Intérêts courus sur emprunts auprès des établissements de crédit	\N	168840	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
90	Intérêts courus sur dépôts et cautionnements reçus	\N	168850	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
91	Intérêts courus sur participation des salariés aux résultats	\N	168860	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
92	Intérêts courus sur emprunts et dettes assortis de conditions particulières	\N	168870	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
93	Intérêts courus sur autres emprunts et dettes assimilées	\N	168880	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
94	Primes de remboursement des obligations	\N	169000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
96	Dettes rattachées à des participations (hors groupe)	\N	174000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
97	Dettes rattachées à des sociétés en participation - Principal	\N	178100	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
98	Dettes rattachées à des sociétés en participation - Intérêts courus	\N	178800	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
100	Biens et prestations de services échangés entre établissements (charges)	\N	186000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
101	Biens et prestations de services échangés entre établissements (produits)	\N	187000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
102	Comptes de liaison des sociétés en participation	\N	188000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
103	Immobilisations incorporelles - Frais d'établissement - Frais de constitution	\N	201100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
104	Immobilisations incorporelles - Frais d'établissement - Frais de prospection	\N	201210	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
105	Immobilisations incorporelles - Frais d'établissement - Frais de publicité	\N	201220	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
108	Immobilisations incorporelles - Concessions et droits similaires, brevets, licences, marques, procédés, logiciels, droits et valeurs similaires	\N	205000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
109	Immobilisations incorporelles - Droit au bail	\N	206000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
110	Immobilisations incorporelles - Fonds commercial	\N	207000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
111	Autres immobilisations incorporelles	\N	208000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
95	Dettes rattachées à des participations (groupe)	\N	171000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
237	Études en cours E 1	\N	341100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
99	Comptes de liaison des établissements	\N	181000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
106	Immobilisations incorporelles - Frais d'augmentation de capital et d'opérations diverses (fusions, scissions, transformations)	\N	201300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
107	Immobilisations incorporelles - Frais de recherche et de développement	\N	203000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
112	Immobilisations corporelles - Terrains nus	\N	211100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
113	Immobilisations corporelles - Terrains aménagés	\N	211200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
115	Immobilisations corporelles - Carrières	\N	211410	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
116	Immobilisations corporelles - Terrains bâtis - Ensembles immobiliers industriels	\N	211510	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
117	Immobilisations corporelles - Terrains bâtis - Ensembles immobiliers administratifs et commerciaux	\N	211550	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
118	Immobilisations corporelles - Terrains bâtis affectés aux opérations professionnelles	\N	211581	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
119	Immobilisations corporelles - Terrains bâtis affectés aux opérations non professionnelles	\N	211588	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
120	Immobilisations corporelles - Compte d'ordre sur immobilisations	\N	211600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
121	Immobilisations corporelles - Agencements et aménagements de terrains (même ventilation que celle du compte 211)	\N	212000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
122	Immobilisations corporelles - Bâtiments - Ensembles immobiliers industriels	\N	213110	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
123	Immobilisations corporelles - Bâtiments - Ensembles immobiliers administratifs et commerciaux	\N	213150	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
124	Immobilisations corporelles - Bâtiments affectés aux opérations professionnelles	\N	213181	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
125	Immobilisations corporelles - Bâtiments affectés aux opérations non professionnelles	\N	213188	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
126	Immobilisations corporelles - Installations générales, agencements, aménagements des constructions (même ventilation que celle du compte 2131)	\N	213500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
127	Immobilisations corporelles - Ouvrages d'infrastructure - Voies de terre	\N	213810	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
128	Immobilisations corporelles - Ouvrages d'infrastructure - Voies de fer	\N	213820	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
129	Immobilisations corporelles - Ouvrages d'infrastructure - Voies d'eau	\N	213830	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
130	Immobilisations corporelles - Ouvrages d'infrastructure - Barrages	\N	213840	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
131	Immobilisations corporelles - Ouvrages d'infrastructure - Pistes d'aérodromes	\N	213850	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
132	Immobilisations corporelles - Constructions sur sol d'autrui (même ventilation que celle du compte 213)	\N	214000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
133	Immobilisations corporelles - Installations complexes spécialisées sur sol propre	\N	215110	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
134	Immobilisations corporelles - Installations complexes spécialisées sur sol d'autrui	\N	215140	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
114	Immobilisations corporelles - Sous-sols et sur-sols	\N	211300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
135	Immobilisations corporelles - Installations à caractère spécifique sur sol propre	\N	215310	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
136	Immobilisations corporelles - Installations à caractère spécifique sur sol d'autrui	\N	215340	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
137	Immobilisations corporelles - Matériels industriels	\N	215400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
138	Immobilisations corporelles - Outillage industriel	\N	215500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
139	Immobilisations corporelles - Agencements et aménagements des matériels et outillage industriels	\N	215700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
140	Immobilisations corporelles - Installations générales agencements aménagements divers	\N	218100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
141	Immobilisations corporelles - Matériel de transport	\N	218200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
143	Immobilisations corporelles - Mobilier	\N	218400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
144	Immobilisations corporelles - Cheptel	\N	218500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
145	Immobilisations corporelles - Emballages récupérables	\N	218600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
146	Immobilisations mises en concession	\N	220000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
147	Immobilisations corporelles en cours - Terrains	\N	231200	f	5	other	\N	f	Pas d'amortissement sur les terrains	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
149	Immobilisations corporelles en cours - Installations techniques matériel et outillage industriels	\N	231500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
150	Autres immobilisations corporelles en cours	\N	231800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
151	Immobilisations incorporelles en cours	\N	232000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
152	Avances et acomptes versés sur commandes d'immobilisations incorporelles	\N	237000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
153	Avances et acomptes versés sur commandes d'immobilisations corporelles - Terrains	\N	238200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
155	Avances et acomptes versés sur commandes d'immobilisations corporelles - Installations techniques matériel et outillage industriels	\N	238500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
156	Avances et acomptes versés sur commandes d'immobilisations corporelles - Autres immobilisations corporelles	\N	238800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
157	Parts dans des entreprises liées et créances sur des entreprises liées	\N	250000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
158	Titres de participation - Actions	\N	261100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
159	Autres titres de participation	\N	261800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
142	Immobilisations corporelles - Matériel de bureau et matériel informatique	\N	218300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
148	Immobilisations corporelles en cours - Constructions	\N	231300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
154	Avances et acomptes versés sur commandes d'immobilisations corporelles - Constructions	\N	238300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
160	Titres évalués par équivalence	\N	262000	f	5	other	\N	f	Pas d'amortissement sur les titres évalués par équivalence	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
161	Autres formes de participation	\N	266000	f	5	other	\N	f	Pas d'amortissement sur les titres évalués par équivalence	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
162	Créances rattachées à des participations (groupe)	\N	267100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
163	Créances rattachées à des participations (hors groupe)	\N	267400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
164	Versements représentatifs d'apports non capitalisés (appel de fonds)	\N	267500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
166	Autres créances rattachées à des participations	\N	267700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
167	Créances rattachées à des participations - Intérêts courus	\N	267800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
168	Créances rattachées à des sociétés en participation - Principal	\N	268100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
169	Créances rattachées à des sociétés en participation - Intérêts courus	\N	268800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
170	Versements restant à effectuer sur titres de participation non libérés	\N	269000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
171	Titres immobilisés autres que les titres immobilisés de l'activité de portefeuille - Actions	\N	271100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
172	Titres immobilisés autres que les titres immobilisés de l'activité de portefeuille - Autres titres	\N	271800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
173	Titres immobilisés - Obligations	\N	272100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
174	Titres immobilisés - Bons	\N	272200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
176	Prêts participatifs	\N	274100	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
177	Prêts aux associés	\N	274200	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
179	Autres prêts	\N	274800	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
180	Dépôts	\N	275100	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
181	Cautionnements	\N	275500	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
182	Autres créances immobilisées - Créances diverses	\N	276100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
183	Autres créances immobilisées - Intérêts courus sur titres immobilisés (droits de créance)	\N	276820	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
184	Autres créances immobilisées - Intérêts courus sur prêts	\N	276840	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
185	Autres créances immobilisées - Intérêts courus sur dépôts et cautionnements	\N	276850	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
186	Autres créances immobilisées - Intérêts courus sur créances diverses	\N	276880	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
187	Actions propres ou parts propres	\N	277100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
175	Titres immobilisés de l'activité de portefeuille	\N	273000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
178	Prêts au personnel	\N	274300	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
165	Avances consolidables	\N	267600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
188	Actions propres ou parts propres en voie d'annulation	\N	277200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
189	Versements restant à effectuer sur titres immobilisés non libérés	\N	279000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
190	Amortissements des immobilisations incorporelles - Frais d'établissement (même ventilation que celle du compte 201)	\N	280100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
192	Amortissements des immobilisations incorporelles - Concessions et droits similaires, brevets, licences, logiciels, droits et valeurs similaires	\N	280500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
193	Amortissements des immobilisations incorporelles - Fonds commercial	\N	280700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
194	Amortissements des autres immobilisations incorporelles	\N	280800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
195	Amortissements des immobilisations corporelles - Terrains de gisement	\N	281100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
196	Amortissements des immobilisations corporelles - Agencements aménagements de terrains (même ventilation que celle du compte 212)	\N	281200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
198	Amortissements des immobilisations corporelles - Constructions sur sol d'autrui (même ventilation que celle du compte 214)	\N	281400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
199	Amortissements des immobilisations corporelles - Installations matériel et outillage industriels (même ventilation que celle du compte 215)	\N	281500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
200	Amortissements des autres immobilisations corporelles (même ventilation que celle du compte 218)	\N	281800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
201	Amortissements des immobilisations mises en concession	\N	282000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
202	Dépréciations des immobilisations incorporelles - Marques, procédés, droits et valeurs similaires	\N	290500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
203	Dépréciations des immobilisations incorporelles - Droit au bail	\N	290600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
204	Dépréciations des immobilisations incorporelles - Fonds commercial	\N	290700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
205	Dépréciations des autres immobilisations incorporelles	\N	290800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
206	Dépréciations des immobilisations corporelles - Terrains (autres que terrains de gisement)	\N	291100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
207	Dépréciations des immobilisations mises en concession	\N	292000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
208	Dépréciations des immobilisations corporelles en cours	\N	293100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
209	Dépréciations des immobilisations incorporelles en cours	\N	293200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
191	Amortissements des immobilisations incorporelles - Frais de recherche et de développement	\N	280300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
197	Amortissements des immobilisations corporelles - Constructions (même ventilation que celle du compte 213)	\N	281300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
210	Provisions pour dépréciation des titres de participation	\N	296100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
211	Provisions pour dépréciation des autres formes de participation	\N	296600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
212	Provisions pour dépréciation des créances rattachées à des participations (même ventilation que celle du compte 267)	\N	296700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
213	Provisions pour dépréciation des créances rattachées à des sociétés en participation (même ventilation que celle du compte 268)	\N	296800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
214	Provisions pour dépréciation des titres immobilisés autres que les titres immobilisés de l'activité de portefeuille - droit de propriété (ventilation : 271)	\N	297100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
215	Provisions pour dépréciation des titres immobilisés - droit de créance (même ventilation que celle du compte 272)	\N	297200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
217	Provisions pour dépréciation des prêts (même ventilation que celle du compte 274)	\N	297400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
218	Provisions pour dépréciation des dépôts et cautionnements versés (même ventilation que celle du compte 275)	\N	297500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
221	Matières premières (ou groupe) B	\N	312000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
222	Fournitures A, B, C, ..	\N	317000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
223	Matières consommables (ou groupe) C	\N	321100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
224	Matières consommables (ou groupe) D	\N	321200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
225	Combustibles	\N	322100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
226	Produits d'entretien	\N	322200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
228	Fournitures de magasin	\N	322400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
229	Fournitures de bureau	\N	322500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
230	Emballages perdus	\N	326100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
231	Emballages récupérables non identifiables	\N	326500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
232	Emballages à usage mixte	\N	326700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
233	Produit en cours P 1	\N	331100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
234	Produit en cours P 2	\N	331200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
235	Travaux en cours T 1	\N	335100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
236	Travaux en cours T 2	\N	335200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
220	Matières premières (ou groupe) A	\N	311000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
216	Provisions pour dépréciation des titres immobilisés de l'activité de portefeuille	\N	297300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
227	Fournitures d'atelier et d usine	\N	322300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
219	Provisions pour dépréciation des autres créances immobilisées (même ventilation que celle du compte 276)	\N	297600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
238	Études en cours E 2	\N	341200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
239	Prestations de services en cours S 1	\N	345100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
240	Prestations de services en cours S 2	\N	345200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
241	Stocks produits intermédiaires (ou groupe) A	\N	351100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
242	Stocks produits intermédiaires (ou groupe) B	\N	351200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
243	Stocks produits finis (ou groupe) A	\N	355100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
244	Stocks produits finis (ou groupe) B	\N	355200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
245	Stocks produits résiduels - Déchets	\N	358100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
246	Stocks produits résiduels - Rebuts	\N	358500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
247	Stocks produits résiduels - Matières de récupération	\N	358600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
248	Stocks provenant d'immobilisations	\N	360000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
250	Stocks de marchandises (ou groupe) B	\N	372000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
251	Stocks en voie d'acheminement	\N	380000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
252	Provisions pour dépréciation des matières premières (ou groupe) A	\N	391100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
253	Provisions pour dépréciation des matières premières (ou groupe) B	\N	391200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
254	Provisions pour dépréciation des fournitures A, B, C, ..	\N	391700	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
255	Provisions pour dépréciation des matières consommables (même ventilation que celle du compte 321)	\N	392100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
256	Provisions pour dépréciation des fournitures consommables (même ventilation que celle du compte 322)	\N	392200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
257	Provisions pour dépréciation des emballages (même ventilation que celle du compte 326)	\N	392600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
258	Provisions pour dépréciation des produits en cours (même ventilation que celle du compte 331)	\N	393100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
259	Provisions pour dépréciation des travaux en cours (même ventilation que celle du compte 335)	\N	393500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
260	Provisions pour dépréciation des études en cours (même ventilation que celle du compte 341)	\N	394100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
261	Provisions pour dépréciation des prestations de services en cours (même ventilation que celle du compte 345)	\N	394500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
262	Provisions pour dépréciation des produits intermédiaires (même ventilation que celle du compte 351)	\N	395100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
263	Provisions pour dépréciation des produits finis (même ventilation que celle du compte 355)	\N	395500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
249	Stocks de marchandises (ou groupe) A	\N	371000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
264	Provisions pour dépréciation des stocks de marchandises (ou groupe) A	\N	397100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
265	Provisions pour dépréciation des stocks de marchandises (ou groupe) B	\N	397200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
266	Fournisseurs et comptes rattachés	\N	400000	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
267	Fournisseurs - Achats de biens et prestations de services	\N	401100	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
268	Fournisseurs - Retenues de garantie	\N	401700	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
270	Fournisseurs - Achats d'immobilisations	\N	404100	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
271	Fournisseurs d'immobilisations - Retenues de garantie	\N	404700	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
272	Fournisseurs d'immobilisations - Effets à payer	\N	405000	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
273	Factures non parvenues - Fournisseurs	\N	408100	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
274	Factures non parvenues - Fournisseurs d'immobilisations	\N	408400	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
275	Factures non parvenues - Fournisseurs - Intérêts courus	\N	408800	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
276	Fournisseurs débiteurs - Créances pour emballages et matériel à rendre	\N	409600	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
277	Fournisseurs débiteurs - Autres avoirs des fournisseurs d'exploitation	\N	409710	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
278	Fournisseurs débiteurs - Autres avoirs des fournisseurs d'immobilisations	\N	409740	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
279	Fournisseurs débiteurs - Rabais, remises, ristournes à obtenir et autres avoirs non encore reçus	\N	409800	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
281	Clients - Ventes de biens ou de prestations de services	\N	411100	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
282	Clients - Retenues de garantie	\N	411700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
284	Clients douteux ou litigieux	\N	416000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
285	Clients - Factures à établir	\N	418100	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
286	Clients - Intérêts courus non encore facturés	\N	418800	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
287	Clients créditeurs - Avances et acomptes reçus sur commandes	\N	419100	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
288	Clients créditeurs - Dettes pour emballages et matériels consignés	\N	419600	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
289	Clients créditeurs - Autres avoirs	\N	419700	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
290	Clients créditeurs - Rabais, remises, ristournes à accorder et autres avoirs à établir	\N	419800	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
280	Clients et comptes rattachés	\N	410000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
269	Fournisseurs - Effets à payer	\N	403000	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
283	Clients - Effets à recevoir	\N	413000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
292	Comités d'entreprise, d'établissement	\N	422000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
293	Participation des salariés aux résultats - Réserve spéciale	\N	424600	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
294	Participation des salariés aux résultats - Comptes courants	\N	424800	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
295	Personnel - Avances et acomptes	\N	425000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
296	Personnel - Dépôts	\N	426000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
297	Personnel - Oppositions	\N	427000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
298	Personnel - Dettes provisionnées pour congés à payer	\N	428200	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
299	Personnel - Dettes provisionnées pour participation des salariés aux résultats	\N	428400	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
300	Personnel - Autres charges à payer	\N	428600	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
301	Personnel - Produits à recevoir	\N	428700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
303	Autres organismes sociaux	\N	437000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
304	Charges sociales sur congés à payer	\N	438200	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
305	Organismes sociaux - Autres charges à payer	\N	438600	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
306	Organismes sociaux - Produits à recevoir	\N	438700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
307	État - Subventions à recevoir - Subventions d'investissement	\N	441100	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
308	État - Subventions à recevoir - Subventions d'exploitation	\N	441700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
309	État - Subventions à recevoir - Subventions d'équilibre	\N	441800	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
310	État - Subventions à recevoir - Avances sur subventions	\N	441900	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
311	État - Impôts et taxes recouvrables sur des tiers - Obligataires	\N	442400	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
312	État - Impôts et taxes recouvrables sur des tiers - Associés	\N	442500	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
313	Créances sur l'État résultant de la suppression de la règle du décalage d'un mois en matière de TVA	\N	443100	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
314	État - Intérêts courus sur créances figurant au compte 4431	\N	443800	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
315	État - Impôts sur les bénéfices	\N	444000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
316	TVA due intracommunautaire (Taux Normal)	\N	445201	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
317	TVA due intracommunautaire (Taux Intermédiaire)	\N	445202	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
318	TVA due intracommunautaire (Autre taux)	\N	445203	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
319	TVA due imports	\N	445204	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
291	Personnel - Rémunérations dues	\N	421000	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
302	Sécurité Sociale	\N	431000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
320	TVA à décaisser	\N	445510	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
321	Taxes assimilées à la TVA	\N	445580	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
322	TVA déductible sur immobilisations	\N	445620	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
323	TVA déductible transférée par d'autres entreprises	\N	445630	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
324	TVA déductible sur autres biens et services	\N	445660	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
325	TVA déductible intracommunautaire	\N	445662	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
326	TVA déductible imports	\N	445663	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
327	Crédit de TVA à reporter	\N	445670	f	5	other	\N	t	Si le remboursement n'a pas été demandé	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
328	Taxes déductibles assimilées à la TVA	\N	445680	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
329	TVA collectée (Taux Normal)	\N	445711	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
330	TVA collectée (Taux Intermédiaire)	\N	445712	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
331	TVA collectée (Autre taux)	\N	445713	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
332	Taxes collectées assimilées à la TVA	\N	445780	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
333	Taxes sur le chiffre d'affaires à régulariser ou en attente	\N	445800	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
334	Acomptes - Régime simplifié d'imposition	\N	445810	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
335	Acomptes - Régime du forfait	\N	445820	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
336	Remboursement de taxes sur le chiffre d'affaires demandé	\N	445830	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
337	TVA récupérée d'avance	\N	445840	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
338	Taxes sur le chiffre d'affaires sur factures non parvenues	\N	445860	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
339	Taxes sur le chiffre d'affaires sur factures à établir	\N	445870	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
340	Obligations cautionnées	\N	446000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
341	Autres impôts, taxes et versements assimilés	\N	447000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
342	État - Charges fiscales sur congés à payer	\N	448200	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
343	État - Charges à payer	\N	448600	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
344	État - Produits à recevoir	\N	448700	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
345	Quotas d'émission à restituer à l'État	\N	449000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
347	Associés - Comptes courants - Principal	\N	455100	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
348	Associés - Comptes courants - Intérêts courus	\N	455800	f	2	payable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
349	Associés - Comptes d'apport en société - Apports en nature	\N	456110	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
346	Groupe	\N	451000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
350	Associés - Comptes d'apport en société - Apports en numéraire	\N	456150	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
351	Actionnaires - Capital souscrit et appelé, non versé	\N	456210	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
352	Associés - Capital appelé, non versé	\N	456250	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
354	Associés - Versements anticipés	\N	456400	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
355	Actionnaires défaillants	\N	456600	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
356	Associés - Capital à rembourser	\N	456700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
357	Associés - Dividendes à payer	\N	457000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
358	Associés - Opérations faites en commun et en GIE - Opérations courantes	\N	458100	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
359	Associés - Opérations faites en commun et en GIE - Intérêts courus	\N	458800	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
360	Créances sur cessions d'immobilisations	\N	462000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
361	Dettes sur acquisitions de valeurs mobilières de placement	\N	464000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
362	Créances sur cessions de valeurs mobilières de placement	\N	465000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
363	Autres comptes débiteurs ou créditeurs	\N	467000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
364	Charges à payer	\N	468600	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
365	Produits à recevoir	\N	468700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
367	Différence de conversion - Actif - Diminution des créances	\N	476100	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
368	Différence de conversion - Actif - Augmentation des dettes	\N	476200	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
369	Différence de conversion - Actif - Différences compensées par couverture de change	\N	476800	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
370	Différences de conversion - Passif - Augmentation des créances	\N	477100	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
371	Différences de conversion - Passif - Diminution des dettes	\N	477200	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
372	Différences de conversion - Passif - Différences compensées par couverture de change	\N	477800	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
373	Autres comptes transitoires	\N	478000	f	9	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
374	Charges à répartir sur plusieurs exercices - Frais d'émission des emprunts	\N	481600	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
375	Charges constatées d'avance	\N	486000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
376	Produits constatés d'avance	\N	487000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
377	Comptes de répartition périodique des charges	\N	488600	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
366	Compte d'attente	\N	471000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
353	Associés - Versements reçus sur augmentation de capital	\N	456300	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
378	Comptes de répartition périodique des produits	\N	488700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
379	Quotas d'émission alloués par l'État	\N	489000	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
381	Provisions pour dépréciation des comptes du groupe	\N	495100	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
382	Provisions pour dépréciation des comptes courants des associés	\N	495500	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
383	Provisions pour dépréciation des opérations faites en commun et en GIE	\N	495800	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
384	Provisions pour dépréciation des créances sur cessions d'immobilisations	\N	496200	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
385	Provisions pour dépréciation des créances sur cessions de valeurs mobilières de placement	\N	496500	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
386	Provisions pour dépréciation - Autres comptes débiteurs	\N	496700	f	1	receivable	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
388	Valeurs mobilières de placement - Actions propres	\N	502000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
389	Valeurs mobilières de placement - Titres cotés	\N	503100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
390	Valeurs mobilières de placement - Titres non cotés	\N	503500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
391	Valeurs mobilières de placement - Autres titres conférant un droit de propriété	\N	504000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
392	Obligations et bons émis par la société et rachetés par elle	\N	505000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
393	Obligations cotés	\N	506100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
394	Obligations non cotés	\N	506500	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
395	Bons du Trésor et bons de caisse à court terme	\N	507000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
396	Autres valeurs mobilières de placement	\N	508100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
397	Bons de souscription	\N	508200	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
398	Intérêts courus sur obligations, bons et valeurs assimilées	\N	508800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
399	Versements restant à effectuer sur valeurs mobilières de placement non libérées	\N	509000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
400	Coupons échus à l'encaissement	\N	511100	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
401	Chèques à encaisser	\N	511200	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
403	Effets à l'escompte	\N	511400	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
404	Banques - Comptes en devises	\N	512400	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
405	Chèques postaux	\N	514000	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
380	Provisions pour dépréciation des comptes de clients	\N	491000	f	9	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
387	Valeurs mobilières de placement - Parts dans entreprises liées	\N	501000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
402	Effets à l'encaissement	\N	511300	f	5	other	\N	t	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
406	Caisses du Trésor et des établissements publics	\N	515000	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
407	Sociétés de bourse	\N	516000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
408	Autres organismes financiers	\N	517000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
409	Intérêts courus à payer	\N	518100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
410	Intérêts courus à recevoir	\N	518800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
411	Concours bancaires courants - Crédit de mobilisation de créances commerciales (CMCC)	\N	519100	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
413	Concours bancaires courants - Intérêts courus sur concours bancaires courants	\N	519800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
414	Instruments de trésorerie	\N	520000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
415	Caisse en monnaie nationale	\N	531100	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
416	Caisse en devises	\N	531400	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
417	Caisse succursale (ou usine) A	\N	532000	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
419	Régies d'avances et accréditifs	\N	540000	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
421	Provisions pour dépréciation des autres titres conférant un droit de propriété	\N	590400	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
422	Provisions pour dépréciation des obligations	\N	590600	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
423	Provisions pour dépréciation des autres valeurs mobilières de placement et créances assimilées (provisions)	\N	590800	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
424	Achats stockés - Matières premières (ou groupe) A	\N	601100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
425	Achats stockés - matières premières (ou groupe) B	\N	601200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
426	Achats stockés - Fournitures A, B, C, ..	\N	601700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
427	Achats stockés - Matières consommables (ou groupe) C	\N	602110	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
428	Achats stockés - Matières consommables (ou groupe) D	\N	602120	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
429	Achats stockés - Combustibles	\N	602210	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
430	Achats stockés - Produits d'entretien	\N	602220	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
431	Achats stockés - Fournitures d'atelier et d'usine	\N	602230	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
432	Achats stockés - Fournitures de magasin	\N	602240	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
433	Achats stockés - Fournitures de bureau	\N	602250	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
434	Achats stockés - Emballages perdus	\N	602610	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
412	Concours bancaires courants - Mobilisation de créances nées à l'étranger	\N	519300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
418	Caisse succursale (ou usine) B	\N	533000	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
420	Provisions pour dépréciation des actions	\N	590300	f	5	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
435	Achats stockés - Emballages récupérables non identifiables	\N	602650	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
436	Achats stockés - Emballages à usage mixte	\N	602670	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
437	Variation des stocks de matières premières (et fournitures)	\N	603100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
438	Variation des stocks des autres approvisionnements	\N	603200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
439	Variation des stocks de marchandises	\N	603700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
440	Achats d'études et prestations de services	\N	604000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
441	Achats de matériel équipements et travaux	\N	605000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
442	Fournitures non stockables (eau, énergie...)	\N	606100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
444	Fournitures administratives	\N	606400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
445	Achats autres matières et fournitures	\N	606800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
446	Achats de marchandises (ou groupe) A	\N	607100	f	16	other	\N	f	Pour achats France	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
447	Achats de marchandises (ou groupe) B	\N	607200	f	16	other	\N	f	Pour déclaration TVA intracommunautaire	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
448	Frais accessoires incorporés aux achats	\N	608000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
449	Rabais, remises et ristournes obtenus sur achats de matières premières (et fournitures)	\N	609100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
450	Rabais, remises et ristournes obtenus sur achats d'autres approvisionnements stockés	\N	609200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
451	Rabais, remises et ristournes obtenus sur achats d'études et prestations de services	\N	609400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
452	Rabais, remises et ristournes obtenus sur achats de matériel, équipements et travaux	\N	609500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
453	Rabais, remises et ristournes obtenus sur achats d'approvisionnements non stockés	\N	609600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
454	Rabais, remises et ristournes obtenus sur achats de marchandises	\N	609700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
455	Rabais, remises et ristournes non affectés	\N	609800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
457	Redevances de crédit-bail mobilier	\N	612200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
458	Redevances de crédit-bail immobilier	\N	612500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
459	Locations immobilières	\N	613200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
460	Locations mobilières	\N	613500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
461	Locations malis sur emballages	\N	613600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
462	Charges locatives et de copropriété	\N	614000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
456	Sous-traitance générale	\N	611000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
443	Fournitures d'entretien et de petit équipement	\N	606300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
463	Entretien et réparations sur biens immobiliers	\N	615200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
464	Entretien et réparations sur biens mobiliers	\N	615500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
465	Maintenance	\N	615600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
466	Assurance multirisques	\N	616100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
467	Assurance obligatoire dommage construction	\N	616200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
468	Assurance transport sur achats	\N	616360	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
469	Assurance transport sur ventes	\N	616370	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
470	Assurance transport sur autres biens	\N	616380	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
471	Assurance risques d'exploitation	\N	616400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
472	Assurance insolvabilité clients	\N	616500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
473	Études et recherches	\N	617000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
474	Documentation générale	\N	618100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
476	Frais de colloques, séminaires, conférences	\N	618500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
477	Rabais, remises et ristournes obtenus sur services extérieurs	\N	619000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
478	Personnel intérimaire	\N	621100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
479	Personnel détaché ou prêté à l'entreprise	\N	621400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
480	Commissions et courtages sur achats	\N	622100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
481	Commissions et courtages sur ventes	\N	622200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
482	Rémunérations des transitaires	\N	622400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
483	Rémunérations d'affacturage	\N	622500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
484	Honoraires	\N	622600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
485	Frais d'actes et de contentieux	\N	622700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
486	Rémunérations d'intermédiaires et honoraires - Divers	\N	622800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
487	Annonces et insertions	\N	623100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
488	Échantillons	\N	623200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
490	Cadeaux à la clientèle	\N	623400	f	16	other	\N	f	Attention déclaration DAS fin d'année	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
491	Primes	\N	623500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
492	Catalogues et imprimés	\N	623600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
493	Publications	\N	623700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
494	Publicité, publications, relations publiques - Divers (pourboires, dons courants...)	\N	623800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
475	Documentation technique	\N	618300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
489	Foires et expositions	\N	623300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
495	Transports sur achats	\N	624100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
496	Transports sur ventes	\N	624200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
498	Transports administratifs	\N	624400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
499	Transports collectifs du personnel	\N	624700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
500	Transports divers	\N	624800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
501	Voyages et déplacements	\N	625100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
502	Frais de déménagement	\N	625500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
503	Missions	\N	625600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
504	Réceptions	\N	625700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
505	Frais postaux et frais de télécommunications	\N	626000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
506	Frais sur titres (achat, vente, garde)	\N	627100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
507	Commissions et frais sur émission d'emprunts	\N	627200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
508	Frais sur effets	\N	627500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
510	Autres frais et commissions sur prestations de services	\N	627800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
511	Concours divers (cotisations...)	\N	628100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
512	Frais de recrutement de personnel	\N	628400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
513	Rabais, remises et ristournes obtenus sur autres services extérieurs	\N	629000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
514	Taxe sur les salaires	\N	631100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
515	Taxe d'apprentissage	\N	631200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
517	Cotisation pour défaut d'investissement obligatoire dans la construction	\N	631400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
518	Autres impôts, taxes et versements assimilés sur rémunérations (administrations des impôts)	\N	631800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
519	Versement de transport	\N	633100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
520	Allocation logement	\N	633200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
522	Participation des employeurs à l'effort de construction	\N	633400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
523	Versements libératoires ouvrant droit à l'exonération de la taxe d'apprentissage	\N	633500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
524	Autres impôts, taxes et versements assimilés sur rémunérations (autres organismes)	\N	633800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
497	Transports entre établissements ou chantiers	\N	624300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
516	Participation des employeurs à la formation professionnelle continue	\N	631300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
521	Participation des employeurs à la formation professionnelle continue	\N	633300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
509	Location de coffres	\N	627600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
525	Cotisation foncière des entreprises	\N	635111	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
526	Cotisation sur la valeur ajoutée des entreprises	\N	635112	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
527	Taxes foncières	\N	635120	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
528	Autres impôts locaux	\N	635130	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
529	Taxe sur les véhicules des sociétés	\N	635140	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
530	Taxes sur le chiffre d'affaires non récupérables	\N	635200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
532	Droits de mutation	\N	635410	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
533	Autres droits	\N	635800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
534	Contribution sociale de solidarité à la charge des sociétés	\N	637100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
535	Taxes perçues par les organismes publics internationaux	\N	637200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
536	Impôts et taxes exigibles à l'étranger	\N	637400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
537	Taxes diverses (autres organismes)	\N	637800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
538	Salaires et appointements	\N	641100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
539	Congés payés	\N	641200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
541	Indemnités et avantages divers	\N	641400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
542	Supplément familial	\N	641500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
543	Rémunération du travail de l'exploitant	\N	644000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
544	Cotisations à l'URSSAF	\N	645100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
545	Cotisations aux mutuelles	\N	645200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
547	Cotisations aux ASSEDIC	\N	645400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
548	Cotisations aux autres organismes sociaux	\N	645800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
549	Cotisations sociales personnelles de l'exploitant	\N	646000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
550	Prestations directes	\N	647100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
551	Versements aux comités d'entreprise et d'établissement	\N	647200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
553	Versements aux autres oeuvres sociales	\N	647400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
554	Médecine du travail, pharmacie	\N	647500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
555	Autres charges de personnel	\N	648000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
556	Crédit d’Impôt Compétitivité Emploi (CICE)	\N	649000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
531	Impôts indirects	\N	635300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
540	Primes et gratifications	\N	641300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
546	Cotisations aux caisses de retraites	\N	645300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
552	Versements aux comités d'hygiène et de sécurité	\N	647300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
557	Redevances pour concessions brevets, licences, marques, procédés, logiciels	\N	651100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
558	Droits d'auteur et de reproduction	\N	651600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
559	Autres droits et valeurs similaires	\N	651800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
561	Créances de l'exercice	\N	654100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
562	Créances des exercices antérieurs	\N	654400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
563	Quote-part de bénéfice transférée (comptabilité du gérant)	\N	655100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
564	Quote-part de perte supportée (comptabilité des associés non gérants)	\N	655500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
565	Charges diverses de gestion courante	\N	658000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
566	Intérêts des emprunts et dettes assimilées	\N	661160	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
567	Intérêts des dettes rattachées à des participations	\N	661170	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
568	Intérêts des comptes courants et des dépôts créditeurs	\N	661500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
569	Intérêts bancaires et sur opérations de financement (escompte, ...)	\N	661600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
570	Intérêts des obligations cautionnées	\N	661700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
571	Intérêts des dettes commerciales	\N	661810	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
572	Intérêts des dettes diverses	\N	661880	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
573	Pertes sur créances liées à des participations	\N	664000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
574	Escomptes accordés	\N	665000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
575	Pertes de change	\N	666000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
576	Charges nettes sur cessions de valeurs mobilières de placement	\N	667000	f	16	other	\N	f	(contrepartie 767)	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
577	Autres charges financières	\N	668000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
578	Charges exceptionnelles - Pénalités sur marchés (et dédits payés sur achats et ventes)	\N	671100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
579	Charges exceptionnelles - Pénalités, amendes fiscales et pénales	\N	671200	f	16	other	\N	f	PV code de la route non déductibles	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
581	Charges exceptionnelles - Créances devenues irrécouvrables dans l'exercice	\N	671400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
582	Charges exceptionnelles - Subventions accordées	\N	671500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
583	Charges exceptionnelles - Rappels d'impôts (autres qu'impôts sur les bénéfices)	\N	671700	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
584	Autres charges exceptionnelles sur opération de gestion	\N	671800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
560	Jetons de présence	\N	653000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
580	Charges exceptionnelles - Dons, libéralités	\N	671300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
585	Charges exceptionnelles sur exercices antérieurs (en cours d'exercice seulement)	\N	672000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
586	Valeurs comptables des éléments d'actif cédés - Immobilisations incorporelles	\N	675100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
587	Valeurs comptables des éléments d'actif cédés - Immobilisations corporelles	\N	675200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
588	Valeurs comptables des éléments d'actif cédés - Immobilisations financières	\N	675600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
589	Valeurs comptables des éléments d'actif cédés - Autres éléments d'actif	\N	675800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
590	Charges exceptionnelles - Malis provenant de clauses d'indexation	\N	678100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
591	Charges exceptionnelles - Lots	\N	678200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
593	Charges exceptionnelles diverses	\N	678800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
594	Dotations aux amortissements sur immobilisations incorporelles	\N	681110	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
595	Dotations aux amortissements sur immobilisations corporelles	\N	681120	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
596	Dotations aux amortissements des charges d'exploitation à répartir	\N	681200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
597	Dotations aux provisions pour risques et charges d'exploitation	\N	681500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
598	Dotations aux dépréciations des immobilisations incorporelles	\N	681610	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
599	Dotations aux dépréciations des immobilisations corporelles	\N	681620	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
600	Dotations aux dépréciations des stocks et en-cours	\N	681730	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
601	Dotations aux dépréciations des créances	\N	681740	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
602	Dotations aux amortissements des primes de remboursement des obligations	\N	686100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
603	Dotations aux provisions pour risques et charges financiers	\N	686500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
604	Dotations aux dépréciations des immobilisations financières	\N	686620	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
605	Dotations aux dépréciations des valeurs mobilières de placement	\N	686650	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
606	Autres dotations aux amortissements, dépréciations et provisions - Charges financières	\N	686800	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
607	Dotations aux amortissements exceptionnels des immobilisations	\N	687100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
608	Dotations aux provisions réglementées exceptionnelles (immobilisations) - Amortissements dérogatoires	\N	687250	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
592	Charges exceptionnelles - Malis provenant du rachat par l'entreprise d'actions et obligations émises par elle-même	\N	678300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
609	Dotations aux provisions réglementées exceptionnelles (stocks)	\N	687300	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
610	Dotations aux autres provisions réglementées exceptionnelles	\N	687400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
611	Dotations aux provisions exceptionnelles	\N	687500	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
614	Impôts sur les bénéfices dus en France	\N	695100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
615	Contribution additionnelle à l'impôt sur les bénéfices	\N	695200	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
616	Impôts sur les bénéfices dus à l'étranger	\N	695400	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
617	Supplément d'impôt sur les sociétés lié aux distributions	\N	696000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
618	Imposition forfaitaire annuelle des sociétés	\N	697000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
619	Intégration fiscale - Charges	\N	698100	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
620	Intégration fiscale - Produits	\N	698900	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
621	Produits, Reports en arrière des déficits	\N	699000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
622	Ventes de produits finis (ou groupe) A	\N	701100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
623	Ventes de produits finis (ou groupe) B	\N	701200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
624	Ventes de produits intermédiaires	\N	702000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
626	Ventes de travaux de catégorie (ou activité) A	\N	704100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
627	Ventes de travaux de catégorie (ou activité) B	\N	704200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
628	Ventes d'études	\N	705000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
629	Ventes de prestations de services	\N	706000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
630	Ventes de marchandises (ou groupe) A	\N	707100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
631	Ventes de marchandises (ou groupe) B	\N	707200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
633	Produits des services exploités dans l'intérêt du personnel	\N	708100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
634	Commissions et courtages	\N	708200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
636	Mise à disposition de personnel facturée	\N	708400	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
637	Ports et frais accessoires facturés	\N	708500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
638	Bonis sur reprises d'emballages consignés	\N	708600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
639	Bonifications obtenues des clients et primes sur ventes	\N	708700	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
613	Participation des salariés aux résultats	\N	691000	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
625	Ventes de produits résiduels	\N	703000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
632	Ventes de marchandises à l'exportation	\N	707300	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
635	Locations diverses	\N	708300	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
612	Dotations aux dépréciations exceptionnelles	\N	687600	f	16	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
640	Autres produits d'activités annexes (cessions d'approvisionnements...)	\N	708800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
641	Rabais, remises et ristournes sur ventes de produits finis	\N	709100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
642	Rabais, remises et ristournes sur ventes de produits intermédiaires	\N	709200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
643	Rabais, remises et ristournes sur travaux	\N	709400	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
644	Rabais, remises et ristournes sur études	\N	709500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
645	Rabais, remises et ristournes sur prestations de services	\N	709600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
646	Rabais, remises et ristournes sur ventes de marchandises	\N	709700	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
647	Rabais, remises et ristournes sur produits des activités annexes	\N	709800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
648	Variation des en-cours de production de biens - Produits en cours	\N	713310	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
649	Variation des en-cours de production de biens - Travaux en cours	\N	713350	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
650	Variation des en-cours de production de services - Études en cours	\N	713410	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
651	Variation des en-cours de production de services - Prestations de services en cours	\N	713450	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
652	Variation des stocks de produits intermédiaires	\N	713510	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
653	Variation des stocks de produits finis	\N	713550	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
654	Variation des stocks de produits résiduels	\N	713580	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
656	Production immobilisée - Immobilisations corporelles	\N	722000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
657	Subventions d'exploitation	\N	740000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
658	Redevances pour concessions, brevets, licences, marques, procédés, logiciels	\N	751100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
659	Droits d'auteur et de reproduction	\N	751600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
660	Redevances pour autres droits et valeurs similaires	\N	751800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
661	Revenus des immeubles non affectés aux activités professionnelles	\N	752000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
663	Ristournes perçues des coopératives (provenant des excédents)	\N	754000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
664	Quote-part de perte transférée (comptabilité du gérant)	\N	755100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
665	Quote-part de bénéfice attribuée (comptabilité des associés non-gérants)	\N	755500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
666	Produits divers de gestion courante	\N	758000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
655	Production immobilisée - Immobilisations incorporelles	\N	721000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
662	Jetons de présence et rémunérations d'administrateurs, gérants..	\N	753000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
667	Revenus des titres de participation	\N	761100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
668	Revenus sur autres formes de participation	\N	761600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
669	Revenus des titres immobilisés	\N	762100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
670	Revenus des prêts	\N	762600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
671	Revenus des créances immobilisées	\N	762700	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
672	Revenus des créances commerciales	\N	763100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
673	Revenus des créances diverses	\N	763800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
674	Revenus des valeurs mobilières de placement	\N	764000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
675	Escomptes obtenus	\N	765000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
676	Gains de change	\N	766000	f	13	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
677	Produits nets sur cessions de valeurs mobilières de placement	\N	767000	f	14	other	\N	f	(contrepartie 667)	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
678	Autres produits financiers	\N	768000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
679	Produits exceptionnels - Dédits et pénalités perçus sur achats et sur ventes	\N	771100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
681	Produits exceptionnels - Rentrées sur créances amorties	\N	771400	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
682	Produits exceptionnels - Subventions d'équilibre	\N	771500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
683	Produits exceptionnels - Dégrèvements d'impôts autres qu'impôts sur les bénéfices	\N	771700	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
684	Autres produits exceptionnels sur opérations de gestion	\N	771800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
685	Produits exceptionnels sur exercices antérieurs (en cours d'exercice seulement)	\N	772000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
686	Produits exceptionnels des cessions d'éléments d'actif - Immobilisations incorporelles	\N	775100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
687	Produits exceptionnels des cessions d'éléments d'actif - Immobilisations corporelles	\N	775200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
688	Produits exceptionnels des cessions d'éléments d'actif - Immobilisations financières	\N	775600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
689	Produits exceptionnels des cessions d'éléments d'actif - Autres éléments d'actif	\N	775800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
690	Produits exceptionnels - Quote-part des subventions d'investissement virée au résultat de l'exercice	\N	777000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
691	Produits exceptionnels - Bonis provenant de clauses d'indexation	\N	778100	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
692	Produits exceptionnels - Lots	\N	778200	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
680	Produits exceptionnels - Libéralités reçues	\N	771300	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
693	Produits exceptionnels - Bonis provenant du rachat par l'entreprise d'actions et d'obligations émises par elle-même	\N	778300	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
694	Produits exceptionnels divers	\N	778800	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
695	Reprises sur amortissements des immobilisations incorporelles	\N	781110	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
696	Reprises sur amortissements des immobilisations corporelles	\N	781120	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
697	Reprises sur provisions d'exploitation	\N	781500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
698	Reprises sur dépréciations des immobilisations incorporelles	\N	781610	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
699	Reprises sur dépréciations des immobilisations corporelles	\N	781620	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
700	Reprises sur dépréciations des actifs circulants - Stocks et en-cours	\N	781730	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
701	Reprises sur dépréciations des actifs circulants - Créances	\N	781740	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
702	Reprises sur provisions financières	\N	786500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
703	Reprises sur dépréciations des immobilisations financières	\N	786620	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
704	Reprises sur dépréciations des valeurs mobilières de placement	\N	786650	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
705	Reprises sur provisions réglementées (immobilisations) - Amortissements dérogatoires	\N	787250	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
706	Reprises sur provisions réglementées (immobilisations) - Provision spéciale de réévaluation	\N	787260	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
707	Reprises sur provisions réglementées (immobilisations) - Plus-values réinvesties	\N	787270	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
709	Reprises sur autres provisions réglementées	\N	787400	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
710	Reprises sur provisions exceptionnelles	\N	787500	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
713	Transferts de charges financières	\N	796000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
714	Transferts de charges exceptionnelles	\N	797000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
716	Banque	\N	512001	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
717	Profits/pertes non distribués	\N	999999	f	12	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:06:53.166234
712	Transferts de charges d'exploitation	\N	791000	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
708	Reprises sur provisions réglementées (stocks)	\N	787300	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
715	Espèces	\N	530001	f	3	liquidity	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
711	Reprises sur dépréciations exceptionnelles	\N	787600	f	14	other	\N	f	\N	1	\N	1	2019-02-14 15:06:53.166234	1	2019-02-14 15:09:48.936423
\.

Added doc/manuel/Comptabilité avancée (partie double).md version [4caaa3768c].















>
>
>
>
>
>
>
1
2
3
4
5
6
7
# Comptabilité avancée (partie double)

## Fonctionnement général

Par défaut les opérations sont enregistrées en tant que brouillon (dans le *brouillard*) et peuvent être modifiées ou supprimées.

Il faut valider les opérations pour les rendre définitivées. Une opération validée ne peut plus être modifiée ou supprimée. En cas d'erreur il faudra créer une nouvelle écriture corrective.

Added doc/manuel/Sauvegarde et restauration.md version [a2332341db].

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Sauvegarde et restauration



## Messages d'erreur

### Le fichier fourni est corrompu. Certaines clés étrangères référencent des lignes qui n'existent pas.

Ce message indiquent que certaines lignes dans une table font référence à des lignes d'une autre table qui n'existent pas.

Cette erreur se produit lorsque des modifications manuelles ont été apportées à une base de données.

Pour trouver les lignes qui sont invalides, utiliser un outil de gestion de base de données SQLite et lancer la commande suivante :

	PRAGMA schema.integrity_check;

Modified src/.htaccess.www from [0f46a6ebba] to [34b16e1e96].

1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Désactiver le multiviews (conflit avec /admin/plugin.php) et les index (sécurité)
Options -MultiViews -Indexes
DirectoryIndex disabled
DirectoryIndex index.php index.html

# Au cas où
<IfModule mod_alias.c>
	RedirectMatch 403 /include/
	RedirectMatch 403 /cache/
	RedirectMatch 403 /plugins/
	RedirectMatch 403 /templates/

	RedirectMatch 403 /.*\.sqlite
	RedirectMatch 403 /.*\.log
	RedirectMatch 403 /(README|VERSION|COPYING|Makefile|cron\.php)
	RedirectMatch 403 /config\.(.*)\.php
	RedirectMatch 403 /sous-domaine\.html
	RedirectMatch 403 _inc\.php
</IfModule>

# Redirection dynamique, pour les installations sans vhost dédié
# Objectif: supprimer le /www/ de l'URL
# Dé-commenter les lignes suivantes pour une installation sans VHost
# Note: il est probable qu'il soit nécessaire d'adapter la configuration
# à votre hébergeur !

#<IfModule mod_rewrite.c>
	#RewriteEngine on
	## Remplacer dans les lignes suivantes
	## /garradin/ par le nom du sous-répertoire où est installé Garradin
 	#RewriteBase /garradin/
	#FallbackResource /garradin/www/_route.php

	## Ne pas modifier les lignes suivantes, les décommenter simplement !
	#RewriteCond %{REQUEST_URI} !www/
	#RewriteRule ^(.*)$ www/$1 [QSA,L]
#</IfModule>

# Une fois ces lignes décommentées, supprimer le fichier .htaccess dans le répertoire www !











>










<



|
|


|
|


|
|
|
<
<
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35
36


# Désactiver le multiviews (conflit avec /admin/plugin.php) et les index (sécurité)
Options -MultiViews -Indexes
DirectoryIndex disabled
DirectoryIndex index.php index.html

# Au cas où
<IfModule mod_alias.c>
	RedirectMatch 403 /include/
	RedirectMatch 403 /cache/
	RedirectMatch 403 /plugins/
	RedirectMatch 403 /templates/
	RedirectMatch 403 ^/scripts/
	RedirectMatch 403 /.*\.sqlite
	RedirectMatch 403 /.*\.log
	RedirectMatch 403 /(README|VERSION|COPYING|Makefile|cron\.php)
	RedirectMatch 403 /config\.(.*)\.php
	RedirectMatch 403 /sous-domaine\.html
	RedirectMatch 403 _inc\.php
</IfModule>

# Redirection dynamique, pour les installations sans vhost dédié
# Objectif: supprimer le /www/ de l'URL

# Note: il est probable qu'il soit nécessaire d'adapter la configuration
# à votre hébergeur !

<IfModule mod_rewrite.c>
	RewriteEngine on
	## Remplacer dans les lignes suivantes
	## /garradin/ par le nom du sous-répertoire où est installé Garradin
 	RewriteBase /garradin/
	FallbackResource /garradin/www/_route.php

	## Ne pas modifier les lignes suivantes, les décommenter simplement !
	RewriteCond %{REQUEST_URI} !www/
	RewriteRule ^(.*)$ www/$1 [QSA,L]
</IfModule>


Modified src/Makefile from [cf4731ac96] to [c8d389d4f3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

27








28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45




.PHONY: dev-server release deps publish check-dependencies test
KD2_FILE := https://fossil.kd2.org/kd2fw/uv/KD2-5.6.zip

deps:
	$(eval TMP_KD2=$(shell mktemp -d))
	#cd ${TMP_KD2}

	wget ${KD2_FILE} -O ${TMP_KD2}/kd2.zip

	rm -rf "include/lib/KD2"
	unzip "${TMP_KD2}/kd2.zip" -d include/lib

	rm -rf ${TMP_KD2}

dev-server:
	php -S localhost:8082 -t www www/_route.php

test:
	find . -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null

release: test
	$(eval VERSION=$(shell cat VERSION))
	rm -rf /tmp/garradin-build
	mkdir -p /tmp/garradin-build
	fossil zip ${VERSION} /tmp/garradin-build/src.zip --name garradin
	unzip -d /tmp/garradin-build /tmp/garradin-build/src.zip

	cd include/lib; rsync --files-from=dependencies.list -r ./ /tmp/garradin-build/garradin/src/include/lib/








	mv /tmp/garradin-build/garradin/src /tmp/garradin-build/garradin-${VERSION}
	@#cd /tmp/garradin-build/; zip -r -9 garradin-${VERSION}.zip garradin-${VERSION};
	@#mv -f /tmp/garradin-build/garradin-${VERSION}.zip ./
	tar cjvfh garradin-${VERSION}.tar.bz2 -C /tmp/garradin-build garradin-${VERSION}

deb:
	cd ../debian; ./makedeb.sh

publish: release deb
	$(eval VERSION=$(shell cat VERSION))
	fossil uv sync
	#fossil uv ls | fgrep -v 'garradin-0.8.5' | grep '^garradin-.*\.(tar\.bz2|deb)' | xargs fossil uv rm
	fossil uv add garradin-${VERSION}.tar.bz2
	cd ../debian && fossil uv add garradin-${VERSION}-*.deb
	fossil uv sync

check-dependencies:
	grep -hEo '^use \\?KD2\\\w+|\\KD2\\\w+' -R include/lib/Garradin www | sed -r 's/^use \\?KD2\\|^\\KD2\\//' | sort | uniq




|
|


















|





>
|
>
>
>
>
>
>
>
>

















|
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
.PHONY: dev-server release deps publish check-dependencies test minify
KD2_FILE := https://fossil.kd2.org/kd2fw/uv/KD2-7.2.zip

deps:
	$(eval TMP_KD2=$(shell mktemp -d))
	#cd ${TMP_KD2}

	wget ${KD2_FILE} -O ${TMP_KD2}/kd2.zip

	rm -rf "include/lib/KD2"
	unzip "${TMP_KD2}/kd2.zip" -d include/lib

	rm -rf ${TMP_KD2}

dev-server:
	php -S localhost:8082 -t www www/_route.php

test:
	find . -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null

release: test minify
	$(eval VERSION=$(shell cat VERSION))
	rm -rf /tmp/garradin-build
	mkdir -p /tmp/garradin-build
	fossil zip ${VERSION} /tmp/garradin-build/src.zip --name garradin
	unzip -d /tmp/garradin-build /tmp/garradin-build/src.zip
	cd include/lib; \
		rsync --files-from=dependencies.list -r ./ /tmp/garradin-build/garradin/src/include/lib/
	mv www/admin/static/mini.css /tmp/garradin-build/garradin/src/www/admin/static/admin.css
	cd /tmp/garradin-build/garradin/src/www/admin/static; \
		rm -f styles/[0-9]*.css; \
		rm -f font/*.css font/*.json
	cd /tmp/garradin-build/garradin/src; \
		rm -f Makefile include/lib/KD2/data/countries.en.json
	cd /tmp/garradin-build/garradin/src/plugins; \
		wget https://fossil.kd2.org/garradin-plugins/uv/welcome.tar.gz
	mv /tmp/garradin-build/garradin/src /tmp/garradin-build/garradin-${VERSION}
	@#cd /tmp/garradin-build/; zip -r -9 garradin-${VERSION}.zip garradin-${VERSION};
	@#mv -f /tmp/garradin-build/garradin-${VERSION}.zip ./
	tar cjvfh garradin-${VERSION}.tar.bz2 -C /tmp/garradin-build garradin-${VERSION}

deb:
	cd ../debian; ./makedeb.sh

publish: release deb
	$(eval VERSION=$(shell cat VERSION))
	fossil uv sync
	#fossil uv ls | fgrep -v 'garradin-0.8.5' | grep '^garradin-.*\.(tar\.bz2|deb)' | xargs fossil uv rm
	fossil uv add garradin-${VERSION}.tar.bz2
	cd ../debian && fossil uv add garradin-${VERSION}-*.deb
	fossil uv sync

check-dependencies:
	grep -hEo '^use \\?KD2\\[^; ]+|\\KD2\\[^\(:; ]+' -R include/lib/Garradin www | sed -r 's/^use \\?KD2\\|^\\KD2\\//' | sort | uniq

minify:
	cat `ls www/admin/static/styles/[0-9]*.css` | sed 's/\.\.\///' > www/admin/static/mini.css
	yui-compressor --nomunge www/admin/static/mini.css -o www/admin/static/mini.css

Modified src/VERSION from [bef1ccc9e8] to [d0cb76d66a].

1
0.9.8.1
|
1
1.0.0-rc7

Modified src/config.dist.php from [7d9b8d58eb] to [9561d21e0d].

15
16
17
18
19
20
21



22
23
24

25
26
27
28
29
30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

53
54
55
56
57
58
59
60
61
62
63
64
65

66
67
68
69
70
71
72

73
74
75
76
77
78
79
80

81








82
83
84
85
86
87

88
89
90
91
92
93
94

95
96
97
98
99
100
101
102
103
104
105
106
107

108
109
110
111
112
113
114
115

/**
 * Clé secrète, doit être unique à chaque instance de Garradin
 *
 * Ceci est utilisé afin de sécuriser l'envoi de formulaires
 * (protection anti-CSRF).
 *



 * Si aucune valeur n'est définie, Garradin ajoutera automatiquement
 * une valeur au hasard dans le fichier config.local.php.
 */

const SECRET_KEY = '3xUhIgGwuovRKOjVsVPQ5yUMfXUSIOX2GKzcebsz5OINrYC50r';

/**
 * Se connecter automatiquement avec l'ID de membre indiqué
 * Exemple: LOCAL_LOGIN = 42 connectera automatiquement le membre n°42
 * Attention à ne pas utiliser en production !
 *
 * Il est aussi possible de mettre "LOCAL_LOGIN = -1" pour se connecter
 * avec le premier membre trouvé qui peut gérer la configuration (et donc
 * modifier les droits des membres).
 *
 * Défault : false (connexion automatique désactivée)
 */

const LOCAL_LOGIN = false;

/**
 * Autoriser (ou non) l'import de sauvegarde qui a été modifiée ?
 * 
 * Si mis à true, un avertissement et une confirmation seront demandés
 * Si mis à false, tout fichier SQLite importé qui ne comporte pas une signature
 * valide (hash SHA1) sera refusé.
 * 
 * Ceci ne s'applique qu'à la page "Sauvegarde et restauration" de l'admin,
 * il est toujours possible de restaurer une base de données non signée en
 * la recopiant à la place du fichier association.sqlite
 *
 * Défaut : true
 */

const ALLOW_MODIFIED_IMPORT = true;

/**
 * Doit-on suggérer à l'utilisateur d'utiliser la version chiffrée du site ?
 * 
 * 1 ou true = affiche un message de suggestion sur l'écran de connexion invitant à utiliser le site chiffré
 * (conseillé si vous avez un certificat auto-signé ou peu connu type CACert)
 * 2 = rediriger automatiquement sur la version chiffrée pour l'administration (mais pas le site public)
 * 3 = rediriger automatiquement sur la version chiffrée pour administration et site public
 * false ou 0 = aucune version chiffrée disponible, donc ne rien proposer ni rediriger
 *
 * Défaut : false
 */

const PREFER_HTTPS = false;

/**
 * Répertoire où se situe le code source de Garradin
 *
 * Défaut : répertoire racine de Garradin (__DIR__)
 */

const ROOT = __DIR__;

/**
 * Répertoire où sont situées les données de Garradin
 * (incluant la base de données SQLite, le cache et les fichiers locaux)
 *
 * Défaut : identique à ROOT
 */

const DATA_ROOT = ROOT;









/**
 * Emplacement du fichier de base de données de Garradin
 *
 * Défaut : ROOT . '/association.sqlite'
 */

const DB_FILE = ROOT . '/association.sqlite';

/**
 * Emplacement de stockage des plugins
 *
 * Défaut : DATA_ROOT . '/plugins'
 */

const PLUGINS_ROOT = DATA_ROOT . '/plugins';

/**
 * Plugins fixes qui ne peuvent être désinstallés par l'utilisateur
 * (séparés par une virgule)
 *
 * Ils seront aussi réinstallés en cas de restauration de sauvegarde,
 * s'ils ne sont pas dans la sauvegarde.
 *
 * Exemple : PLUGINS_SYSTEM = 'gestion_emails,factures'
 *
 * Défaut : aucun (chaîne vide)
 */

const PLUGINS_SYSTEM = '';

/**
 * Adresse URI de la racine du site Garradin
 * (doit se terminer par un slash)
 *
 * Défaut : découverte automatique à partir de SCRIPT_NAME
 */







>
>
>
|
|

>
|












>
|



|



|






>
|



|








>
|






>
|



|



>
|
>
>
>
>
>
>
>
>




|

>
|






>
|












>
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

/**
 * Clé secrète, doit être unique à chaque instance de Garradin
 *
 * Ceci est utilisé afin de sécuriser l'envoi de formulaires
 * (protection anti-CSRF).
 *
 * Cette valeur peut être modifiée sans autre impact que la déconnexion des utilisateurs
 * actuellement connectés.
 *
 * Si cette constante n'est définie, Garradin ajoutera automatiquement
 * une valeur aatoire dans le fichier config.local.php.
 */

//const SECRET_KEY = '3xUhIgGwuovRKOjVsVPQ5yUMfXUSIOX2GKzcebsz5OINrYC50r';

/**
 * Se connecter automatiquement avec l'ID de membre indiqué
 * Exemple: LOCAL_LOGIN = 42 connectera automatiquement le membre n°42
 * Attention à ne pas utiliser en production !
 *
 * Il est aussi possible de mettre "LOCAL_LOGIN = -1" pour se connecter
 * avec le premier membre trouvé qui peut gérer la configuration (et donc
 * modifier les droits des membres).
 *
 * Défault : false (connexion automatique désactivée)
 */

//const LOCAL_LOGIN = false;

/**
 * Autoriser (ou non) l'import de sauvegarde qui a été modifiée ?
 *
 * Si mis à true, un avertissement et une confirmation seront demandés
 * Si mis à false, tout fichier SQLite importé qui ne comporte pas une signature
 * valide (hash SHA1) sera refusé.
 *
 * Ceci ne s'applique qu'à la page "Sauvegarde et restauration" de l'admin,
 * il est toujours possible de restaurer une base de données non signée en
 * la recopiant à la place du fichier association.sqlite
 *
 * Défaut : true
 */

//const ALLOW_MODIFIED_IMPORT = true;

/**
 * Doit-on suggérer à l'utilisateur d'utiliser la version chiffrée du site ?
 *
 * 1 ou true = affiche un message de suggestion sur l'écran de connexion invitant à utiliser le site chiffré
 * (conseillé si vous avez un certificat auto-signé ou peu connu type CACert)
 * 2 = rediriger automatiquement sur la version chiffrée pour l'administration (mais pas le site public)
 * 3 = rediriger automatiquement sur la version chiffrée pour administration et site public
 * false ou 0 = aucune version chiffrée disponible, donc ne rien proposer ni rediriger
 *
 * Défaut : false
 */

//const PREFER_HTTPS = false;

/**
 * Répertoire où se situe le code source de Garradin
 *
 * Défaut : répertoire racine de Garradin (__DIR__)
 */

//const ROOT = __DIR__;

/**
 * Répertoire où sont situées les données de Garradin
 * (incluant la base de données SQLite, les sauvegardes, le cache et les fichiers locaux)
 *
 * Défaut : identique à ROOT
 */

//const DATA_ROOT = ROOT;

/**
 * Répertoire où est situé le cache (fichiers temporaires utilisés pour accélérer le chargement des pages)
 *
 * Défaut : sous-répertoire 'cache' de DATA_ROOT
 */

//const CACHE_ROOT = ROOT . '/cache';

/**
 * Emplacement du fichier de base de données de Garradin
 *
 * Défaut : DATA_ROOT . '/association.sqlite'
 */

//const DB_FILE = DATA_ROOT . '/association.sqlite';

/**
 * Emplacement de stockage des plugins
 *
 * Défaut : DATA_ROOT . '/plugins'
 */

//const PLUGINS_ROOT = DATA_ROOT . '/plugins';

/**
 * Plugins fixes qui ne peuvent être désinstallés par l'utilisateur
 * (séparés par une virgule)
 *
 * Ils seront aussi réinstallés en cas de restauration de sauvegarde,
 * s'ils ne sont pas dans la sauvegarde.
 *
 * Exemple : PLUGINS_SYSTEM = 'gestion_emails,factures'
 *
 * Défaut : aucun (chaîne vide)
 */

//const PLUGINS_SYSTEM = '';

/**
 * Adresse URI de la racine du site Garradin
 * (doit se terminer par un slash)
 *
 * Défaut : découverte automatique à partir de SCRIPT_NAME
 */
125
126
127
128
129
130
131

132
133
134
135
136
137
138
139
140
141
142

143
144
145
146
147
148
149
150
151
152
153
154

155
156
157
158
159
160
161
162
163
164
165
166

167
168
169


170
171

172
173
174

175
176
177
178
179

180
181
182
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

217
218
219
220
221
222
223
224
225
226

227
228
229
230
231
232
233
234

235
236
237
238
239
240
241
242
243
244

245
246
247
248
249
250
251
252
//const WWW_URL = 'http://garradin.chezmoi.tld' . WWW_URI;

/**
 * Adresse URL HTTP(S) de l'admin Garradin
 *
 * Défaut : WWW_URL + 'admin/'
 */

//const ADMIN_URL = 'https://admin.garradin.chezmoi.tld/';

/**
 * Affichage des erreurs
 * Si "true" alors un message expliquant l'erreur et comment rapporter le bug s'affiche
 * en cas d'erreur. Sinon rien ne sera affiché.
 *
 * Défaut : false
 *
 * Il est fortement conseillé de mettre cette valeur à false en production !
 */

const SHOW_ERRORS = false;

/**
 * Envoi des erreurs par e-mail
 *
 * Si renseigné, un email sera envoyé à l'adresse indiquée à chaque fois qu'une erreur
 * d'exécution sera rencontrée.
 * Si "false" alors aucun email ne sera envoyé.
 * Note : les erreurs sont déjà toutes loguées dans error.log à la racine de DATA_ROOT
 *
 * Défaut : false
 */

const MAIL_ERRORS = false;

/**
 * Envoi des erreurs à une API compatible AirBrake/Errbit
 *
 * Si renseigné avec une URL HTTP(S) valide, chaque erreur système sera envoyée
 * automatiquement à cette URL.
 *
 * Si laissé à null, aucun rapport ne sera envoyé.
 *
 * Défaut : null
 */

const ERRORS_REPORT_URL = null;

/**


 * Activation de la page permettant de visualiser et rapporter les erreurs présentes
 * dans le error.log.

 *
 * Conseillé de mettre à false si vous ne voulez pas que les administrateurs de votre
 * instance puissent voir les erreurs système.

 *
 * Défaut : true
 * (Afin d'aider au rapport de bugs des instances auto-hébergées)
 */
const ERRORS_ENABLE_LOG_VIEW = true;


/**
 * Utilisation de cron pour les tâches automatiques
 *
 * Si "true" on s'attend à ce qu'une tâche automatisée appelle
 * le script cron.php à la racine toutes les 24 heures. Sinon Garradin
 * effectuera les actions automatiques quand quelqu'un se connecte à
 * l'administration ou visite le site.
 *
 * Défaut : false
 */

const USE_CRON = false;

/**
 * Activation de l'envoi de fichier directement par le serveur web.
 * (X-SendFile)
 *
 * Permet d'améliorer la rapidité d'envoi des fichiers.
 * Supporte les serveurs web suivants :
 * - Apache avec mod_xsendfile (paquet libapache2-mod-xsendfile)
 * - Lighttpd
 *
 * N'activer que si vous êtes sûr que le module est installé et activé (sinon 
 * les fichiers ne pourront être vus ou téléchargés).
 * Nginx n'est PAS supporté, car X-Accel-Redirect ne peut gérer que des fichiers
 * qui sont *dans* le document root du vhost, ce qui n'est pas le cas ici.
 *
 * Pour activer X-SendFile mettre dans la config du virtualhost de Garradin:
 * XSendFile On
 * XSendFilePath /var/www/garradin
 *
 * (remplacer le chemin par le répertoire racine de Garradin)
 *
 * Détails : https://tn123.org/mod_xsendfile/
 *
 * Défaut : false
 */

const ENABLE_XSENDFILE = false;

/**
 * Serveur NTP utilisé pour les connexions avec TOTP
 * (utilisé seulement si le code OTP fourni est faux)
 *
 * Désactiver (false) si vous êtes sûr que votre serveur est toujours à l'heure.
 *
 * Défaut : fr.pool.ntp.org
 */

const NTP_SERVER = 'fr.pool.ntp.org';

/**
 * Hôte du serveur SMTP, mettre à false (défaut) pour utiliser la fonction
 * mail() de PHP
 *
 * Défaut : false
 */

const SMTP_HOST = false;

/**
 * Port du serveur SMTP
 *
 * 25 = port standard pour connexion non chiffrée (465 pour Gmail)
 * 587 = port standard pour connexion SSL
 *
 * Défaut : 587
 */

const SMTP_PORT = 587;

/**
 * Login utilisateur pour le server SMTP
 *
 * mettre à null pour utiliser un serveur local ou anonyme
 *
 * Défaut : null







>











>
|











>
|











>
|


>
>
|
<
>

<
<
>




|
>





|
|
|



>
|










|














>
|









>
|







>
|









>
|







145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196

197
198


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
//const WWW_URL = 'http://garradin.chezmoi.tld' . WWW_URI;

/**
 * Adresse URL HTTP(S) de l'admin Garradin
 *
 * Défaut : WWW_URL + 'admin/'
 */

//const ADMIN_URL = 'https://admin.garradin.chezmoi.tld/';

/**
 * Affichage des erreurs
 * Si "true" alors un message expliquant l'erreur et comment rapporter le bug s'affiche
 * en cas d'erreur. Sinon rien ne sera affiché.
 *
 * Défaut : false
 *
 * Il est fortement conseillé de mettre cette valeur à false en production !
 */

//const SHOW_ERRORS = false;

/**
 * Envoi des erreurs par e-mail
 *
 * Si renseigné, un email sera envoyé à l'adresse indiquée à chaque fois qu'une erreur
 * d'exécution sera rencontrée.
 * Si "false" alors aucun email ne sera envoyé.
 * Note : les erreurs sont déjà toutes loguées dans error.log à la racine de DATA_ROOT
 *
 * Défaut : false
 */

//const MAIL_ERRORS = false;

/**
 * Envoi des erreurs à une API compatible AirBrake/Errbit
 *
 * Si renseigné avec une URL HTTP(S) valide, chaque erreur système sera envoyée
 * automatiquement à cette URL.
 *
 * Si laissé à null, aucun rapport ne sera envoyé.
 *
 * Défaut : null
 */

//const ERRORS_REPORT_URL = null;

/**
 * Activation des détails techniques (utile en auto-hébergement) :
 * - version de PHP
 * - page permettant de visualiser les erreurs présentes dans le error.log

 * - vérification de nouvelle version
 *


 * Ces infos ne sont visibles que par les membres ayant accès à la configuration.
 *
 * Défaut : true
 * (Afin d'aider au rapport de bugs des instances auto-hébergées)
 */

//const ENABLE_TECH_DETAILS = true;

/**
 * Utilisation de cron pour les tâches automatiques
 *
 * Si "true" on s'attend à ce qu'une tâche automatisée appelle
 * le script cron.php dans le répertoire "scripts" toutes les 24 heures.
 * Sinon Garradin effectuera les actions automatiques quand quelqu'un
 * se connecte à l'administration ou visite le site.
 *
 * Défaut : false
 */

//const USE_CRON = false;

/**
 * Activation de l'envoi de fichier directement par le serveur web.
 * (X-SendFile)
 *
 * Permet d'améliorer la rapidité d'envoi des fichiers.
 * Supporte les serveurs web suivants :
 * - Apache avec mod_xsendfile (paquet libapache2-mod-xsendfile)
 * - Lighttpd
 *
 * N'activer que si vous êtes sûr que le module est installé et activé (sinon
 * les fichiers ne pourront être vus ou téléchargés).
 * Nginx n'est PAS supporté, car X-Accel-Redirect ne peut gérer que des fichiers
 * qui sont *dans* le document root du vhost, ce qui n'est pas le cas ici.
 *
 * Pour activer X-SendFile mettre dans la config du virtualhost de Garradin:
 * XSendFile On
 * XSendFilePath /var/www/garradin
 *
 * (remplacer le chemin par le répertoire racine de Garradin)
 *
 * Détails : https://tn123.org/mod_xsendfile/
 *
 * Défaut : false
 */

//const ENABLE_XSENDFILE = false;

/**
 * Serveur NTP utilisé pour les connexions avec TOTP
 * (utilisé seulement si le code OTP fourni est faux)
 *
 * Désactiver (false) si vous êtes sûr que votre serveur est toujours à l'heure.
 *
 * Défaut : fr.pool.ntp.org
 */

//const NTP_SERVER = 'fr.pool.ntp.org';

/**
 * Hôte du serveur SMTP, mettre à false (défaut) pour utiliser la fonction
 * mail() de PHP
 *
 * Défaut : false
 */

//const SMTP_HOST = false;

/**
 * Port du serveur SMTP
 *
 * 25 = port standard pour connexion non chiffrée (465 pour Gmail)
 * 587 = port standard pour connexion SSL
 *
 * Défaut : 587
 */

//const SMTP_PORT = 587;

/**
 * Login utilisateur pour le server SMTP
 *
 * mettre à null pour utiliser un serveur local ou anonyme
 *
 * Défaut : null
270
271
272
273
274
275
276

277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

292
293
294
295
296
297
298
299
300

301
302
303
304
305
306

307
308
309
310
311
312
313
314
315
316
317
318
319
320

321
 * NONE = pas de chiffrement
 * SSL = connexion SSL native
 * TLS = connexion TLS native (le plus sécurisé)
 * STARTTLS = utilisation de STARTTLS (moyennement sécurisé)
 *
 * Défaut : STARTTLS
 */

const SMTP_SECURITY = 'STARTTLS';

/**
 * Activer les sauvegardes automatiques
 *
 * Utile à désactiver si vous avez déjà des sauvegardes effectuées
 * automatiquement au niveau du système.
 *
 * Sinon les sauvegardes seront effectuées soit par la tâche cron
 * soit à l'affichage de la page d'accueil (si nécessaire).
 *
 * Voir paramètre USE_CRON aussi
 *
 * Défaut : true
 */

const ENABLE_AUTOMATIC_BACKUPS = true;


/**
 * Couleur primaire de l'interface admin par défaut
 * (peut être personnalisée dans la configuration)
 *
 * Défaut : #9c4f15
 */

//const ADMIN_COLOR1 = '#20787a';

/**
 * Couleur secondaire de l'interface admin
 * Défaut : #d98628
 */

//const ADMIN_COLOR2 = '#85b9ba';

/**
 * Image de fond par défaut de l'interface admin
 *
 * Cette URL doit être absolue (http://...) ou relative à l'admin (/admin/static...)
 *
 * Attention si l'image est sur un domaine différent vous devrez activer l'entête CORS:
 * Access-Control-Allow-Origin "*"
 *
 * sinon la personnalisation des couleurs ne fonctionnera pas
 *
 * Défaut : [ADMIN_URL]static/gdin_bg.png
 */

//const ADMIN_BACKGROUND_IMAGE = 'http://mon-asso.fr/fond_garradin.png';







>
|














>
|








>






>














>

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
 * NONE = pas de chiffrement
 * SSL = connexion SSL native
 * TLS = connexion TLS native (le plus sécurisé)
 * STARTTLS = utilisation de STARTTLS (moyennement sécurisé)
 *
 * Défaut : STARTTLS
 */

//const SMTP_SECURITY = 'STARTTLS';

/**
 * Activer les sauvegardes automatiques
 *
 * Utile à désactiver si vous avez déjà des sauvegardes effectuées
 * automatiquement au niveau du système.
 *
 * Sinon les sauvegardes seront effectuées soit par la tâche cron
 * soit à l'affichage de la page d'accueil (si nécessaire).
 *
 * Voir paramètre USE_CRON aussi
 *
 * Défaut : true
 */

//const ENABLE_AUTOMATIC_BACKUPS = true;


/**
 * Couleur primaire de l'interface admin par défaut
 * (peut être personnalisée dans la configuration)
 *
 * Défaut : #9c4f15
 */

//const ADMIN_COLOR1 = '#20787a';

/**
 * Couleur secondaire de l'interface admin
 * Défaut : #d98628
 */

//const ADMIN_COLOR2 = '#85b9ba';

/**
 * Image de fond par défaut de l'interface admin
 *
 * Cette URL doit être absolue (http://...) ou relative à l'admin (/admin/static...)
 *
 * Attention si l'image est sur un domaine différent vous devrez activer l'entête CORS:
 * Access-Control-Allow-Origin "*"
 *
 * sinon la personnalisation des couleurs ne fonctionnera pas
 *
 * Défaut : [ADMIN_URL]static/gdin_bg.png
 */

//const ADMIN_BACKGROUND_IMAGE = 'http://mon-asso.fr/fond_garradin.png';

Added src/include/data/1.0.0-beta6_migration.sql version [13c95a32fe].



























>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
ALTER TABLE fichiers_membres RENAME TO fichiers_membres_old;
ALTER TABLE fichiers_wiki_pages RENAME TO fichiers_wiki_pages_old;
ALTER TABLE fichiers_acc_transactions RENAME TO fichiers_acc_transactions_old;

.read 1.0.0_schema.sql

INSERT INTO fichiers_membres SELECT * FROM fichiers_membres_old;
INSERT INTO fichiers_wiki_pages SELECT * FROM fichiers_wiki_pages_old;
INSERT INTO fichiers_acc_transactions SELECT * FROM fichiers_acc_transactions_old;

DROP TABLE fichiers_membres_old;
DROP TABLE fichiers_wiki_pages_old;
DROP TABLE fichiers_acc_transactions_old;

Added src/include/data/1.0.0-beta8_migration.sql version [ff1b70a076].





>
>
1
2
UPDATE acc_accounts SET type = 11 WHERE code = '120';
UPDATE acc_accounts SET type = 12 WHERE code = '129';

Added src/include/data/1.0.0-rc3_migration.sql version [46b4d521e5].



>
1
UPDATE acc_transactions SET type = 0 WHERE type = 6;

Added src/include/data/1.0.0_migration.sql version [ba81ccc806].

































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
ALTER TABLE membres_operations RENAME TO membres_operations_old;
ALTER TABLE membres_categories RENAME TO membres_categories_old;

DROP TABLE fichiers_compta_journal; -- Inutilisé à ce jour

-- Fix: comptes de clôture et fermeture
UPDATE compta_comptes SET libelle = 'Bilan d''ouverture' WHERE id = '890' AND libelle = 'Bilan de clôture';
INSERT OR REPLACE INTO compta_comptes (id, parent, libelle, position) VALUES ('891', '89', 'Bilan de clôture', 0);

-- N'est pas utilisé
DELETE FROM config WHERE cle = 'categorie_dons' OR cle = 'categorie_cotisations';

.read 1.0.0_schema.sql

-------- MIGRATION COMPTA ---------
INSERT INTO acc_charts (id, country, code, label) VALUES (1, 'FR', 'PCGA1999', 'Plan comptable associatif 1999');

-- Migration comptes de code comme identifiant à ID unique
-- Inversement valeurs actif/passif et produit/charge
INSERT INTO acc_accounts (id, id_chart, code, label, position, user)
	SELECT NULL, 1, id, libelle,
	CASE position
		WHEN 1 THEN 2
		WHEN 2 THEN 1
		WHEN 3 THEN 3
		WHEN 4 THEN 5
		WHEN 8 THEN 4
		-- Suppression de la position "charge ou produit" qui n'a aucun sens
		WHEN 12 THEN 0
		ELSE 0
	END,
	CASE WHEN plan_comptable = 1 THEN 0 ELSE 1 END
	FROM compta_comptes;

-- Migrations projets vers comptes analytiques
INSERT INTO acc_accounts (id_chart, code, label, position, user, type)
	VALUES (1, '99', 'Projets', 0, 1, 0);

INSERT INTO acc_accounts (id_chart, code, label, position, user, type)
	SELECT 1, '99' || substr('0000' || id, -4), libelle, 0, 1, 7 FROM compta_projets;

-- Mise à jour de la position pour les comptes de tiers qui peuvent varier actif ou passif
UPDATE acc_accounts SET position = 3 WHERE code IN (4010, 4110, 4210, 428, 438);

-- Mise à jour position comptes bancaires, qui peuvent être à découvert et donc changer de côté au bilan
UPDATE acc_accounts SET position = 3 WHERE code LIKE '512%';

-- Migration comptes bancaires
UPDATE acc_accounts SET type = 1 WHERE code IN (SELECT id FROM compta_comptes_bancaires);

-- Caisse
UPDATE acc_accounts SET type = 2 WHERE code = '530';

-- Chèques et carte à encaisser
UPDATE acc_accounts SET type = 3 WHERE code = '5112' OR code = '5113';

-- Comptes d'ouverture et de clôture
UPDATE acc_accounts SET type = 9, position = 0 WHERE code = '890';
UPDATE acc_accounts SET type = 10, position = 0 WHERE code = '891';

-- Comptes de tiers
UPDATE acc_accounts SET type = 4 WHERE code IN (SELECT id FROM compta_comptes WHERE id LIKE '4%' AND plan_comptable = 0 AND desactive = 0);

-- Recopie des mouvements
INSERT INTO acc_transactions (id, label, notes, reference, date, id_year, id_creator)
	SELECT id, libelle, remarques, numero_piece, date, id_exercice, id_auteur
	FROM compta_journal;

-- Recettes
UPDATE acc_transactions SET type = 1 WHERE id IN (SELECT id FROM compta_journal WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = 1));

-- Dépenses
UPDATE acc_transactions SET type = 2 WHERE id IN (SELECT id FROM compta_journal WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = -1));

-- Virements
UPDATE acc_transactions SET type = 3 WHERE id IN (SELECT id FROM compta_journal
	WHERE (compte_credit IN ('530', '5112', '5115') OR compte_credit LIKE '512%')
	AND (compte_debit IN ('530', '5112', '5115') OR compte_debit LIKE '512%'));

-- Dettes
UPDATE acc_transactions SET type = 4 WHERE id IN (SELECT id FROM compta_journal WHERE compte_debit LIKE '6%' AND compte_credit LIKE '4%');

-- Créances
UPDATE acc_transactions SET type = 5 WHERE id IN (SELECT id FROM compta_journal WHERE compte_credit LIKE '7%' AND compte_debit LIKE '4%');

-- Création des lignes associées aux mouvements
INSERT INTO acc_transactions_lines (id_transaction, id_account, debit, credit, reference, id_analytical)
	SELECT id, (SELECT id FROM acc_accounts WHERE code = compte_credit), 0, CAST(REPLACE(montant * 100, '.0', '') AS INT), numero_cheque,
	CASE WHEN id_projet IS NOT NULL THEN (SELECT id FROM acc_accounts WHERE code = '99' || substr('0000' || id_projet, -4)) ELSE NULL END
	FROM compta_journal;

INSERT INTO acc_transactions_lines (id_transaction, id_account, debit, credit, reference, id_analytical)
	SELECT id, (SELECT id FROM acc_accounts WHERE code = compte_debit), CAST(REPLACE(montant * 100, '.0', '') AS INT), 0, numero_cheque,
	CASE WHEN id_projet IS NOT NULL THEN (SELECT id FROM acc_accounts WHERE code = '99' || substr('0000' || id_projet, -4)) ELSE NULL END
	FROM compta_journal;

-- Recopie des descriptions de catégories dans la table des comptes, et mise des comptes en signets
-- +Fix éventuels types qui ne correspondent pas à leur type… (@Fred C.) (... a.position = X)
-- Revenus
UPDATE acc_accounts SET type = 6, description = (SELECT description FROM compta_categories WHERE compte = acc_accounts.code)
	WHERE id IN (SELECT a.id FROM acc_accounts a INNER JOIN compta_categories c ON c.compte = a.code AND c.type = 1 AND a.position = 5);

-- Dépenses
UPDATE acc_accounts SET type = 5, description = (SELECT description FROM compta_categories WHERE compte = acc_accounts.code)
	WHERE id IN (SELECT a.id FROM acc_accounts a INNER JOIN compta_categories c ON c.compte = a.code AND c.type = -1 AND c.compte NOT LIKE '4%' AND a.position = 4);

-- Tiers
UPDATE acc_accounts SET type = 4, description = (SELECT description FROM compta_categories WHERE compte = acc_accounts.code)
	WHERE id IN (SELECT a.id FROM acc_accounts a INNER JOIN compta_categories c ON c.compte = a.code AND c.type = -1 AND c.compte LIKE '4%');

-- Recopie des opérations, mais le nom a changé pour acc_transactions_users
INSERT INTO acc_transactions_users
	SELECT * FROM membres_operations_old;

-- Recopie des exercices, mais la date de fin ne peut être nulle
INSERT INTO acc_years (id, label, start_date, end_date, closed, id_chart)
	SELECT id, libelle, debut, CASE WHEN fin IS NULL THEN date(debut, '+1 year') ELSE fin END, cloture, 1 FROM compta_exercices;

-- Recopie des catégories, on supprime la colonne id_cotisation_obligatoire
INSERT INTO membres_categories
	SELECT id, nom, droit_wiki, droit_membres, droit_compta, droit_inscription, droit_connexion, droit_config, cacher FROM membres_categories_old;

DROP TABLE membres_categories_old;

-- Transfert des rapprochements
UPDATE acc_transactions_lines SET reconciled = 1 WHERE id_transaction IN (SELECT id_operation FROM compta_rapprochement);

--------- MIGRATION COTISATIONS ----------

-- A edge-case where the end date is after the start date, let's fix it…
UPDATE cotisations SET fin = debut WHERE fin < debut;

INSERT INTO services SELECT id, intitule, description, duree, debut, fin FROM cotisations;

INSERT INTO services_fees (id, label, amount, id_service, id_account, id_year)
	SELECT id, intitule, CASE WHEN montant IS NOT NULL THEN CAST(montant*100 AS integer) ELSE NULL END, id,
		(SELECT id FROM acc_accounts WHERE code = (SELECT compte FROM compta_categories WHERE id = id_categorie_compta)),
		(SELECT MAX(id) FROM acc_years WHERE closed = 0)
	FROM cotisations;

INSERT INTO services_users SELECT cm.id, cm.id_membre, cm.id_cotisation,
	cm.id_cotisation,
	1,
	NULL,
	cm.date,
	CASE
		WHEN c.duree IS NOT NULL THEN date(cm.date, '+'||c.duree||' days')
		WHEN c.fin IS NOT NULL THEN c.fin
		ELSE NULL
	END
	FROM cotisations_membres cm
	INNER JOIN cotisations c ON c.id = cm.id_cotisation;

INSERT INTO services_reminders SELECT * FROM rappels;
INSERT INTO services_reminders_sent SELECT id, id_membre, id_cotisation,
	CASE WHEN id_rappel IS NULL THEN (SELECT MAX(id) FROM rappels) ELSE id_rappel END, date
	FROM rappels_envoyes
	WHERE id_rappel IS NOT NULL
	GROUP BY id_membre, id_cotisation, id_rappel;

DROP TABLE cotisations;
DROP TABLE cotisations_membres;
DROP TABLE rappels;
DROP TABLE rappels_envoyes;

-- Suppression inutilisées
DROP TABLE compta_rapprochement;
DROP TABLE compta_journal;
DROP TABLE compta_categories;
DROP TABLE compta_comptes;
DROP TABLE compta_exercices;
DROP TABLE membres_operations_old;

DROP TABLE compta_projets;
DROP TABLE compta_comptes_bancaires;
DROP TABLE compta_moyens_paiement;

Modified src/include/data/1.0.0_schema.sql from [c8df01c2dd] to [2cb870482b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51




52


53
54

55







56
57
58
59
60
61
62
63

64



65

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

102

103
104


105
106
107
108
109
110
111
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);

-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
-- compta_categorie_cotisations => id_categorie
-- compta_categorie_dons => id_categorie

CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) ON DELETE SET NULL
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY NOT NULL,
    id_categorie_compta INTEGER NULL REFERENCES compta_categories (id) ON DELETE SET NULL, -- NULL si le type n'est pas associé automatiquement à la compta

    intitule TEXT NOT NULL,
    description TEXT NULL,
    montant REAL NOT NULL,







    duree INTEGER NULL, -- En jours
    debut TEXT NULL, -- timestamp

    fin TEXT NULL







);

CREATE TABLE IF NOT EXISTS cotisations_membres
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,





    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)

);

CREATE UNIQUE INDEX IF NOT EXISTS cm_unique ON cotisations_membres (id_membre, id_cotisation, date);

CREATE TABLE IF NOT EXISTS membres_operations
-- Liaison des enregistrement des paiements en compta
(
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_operation INTEGER NOT NULL REFERENCES compta_journal (id) ON DELETE CASCADE,
    id_cotisation INTEGER NULL REFERENCES cotisations_membres (id) ON DELETE SET NULL,

    PRIMARY KEY (id_membre, id_operation)
);

CREATE TABLE IF NOT EXISTS rappels
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    sujet TEXT NOT NULL,
    texte TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,
    id_rappel INTEGER NULL REFERENCES rappels (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),



    media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);



--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki






<
<
<
<












|
<
<

















|
|


<

|

|
>
>
>
>

>
>
|
|
>
|
>
>
>
>
>
>
>


|



|
|
>

>
>
>
|
>


|

|
<
<
<
<
<
|
|
|

|



|

|

|
|


|




|
|
|

|
>

>
|
<
>
>







1
2
3
4
5
6




7
8
9
10
11
12
13
14
15
16
17
18
19


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82





83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

113
114
115
116
117
118
119
120
121
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);





CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0


);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS services
-- Types de services (cotisations)
(
    id INTEGER PRIMARY KEY NOT NULL,


    label TEXT NOT NULL,
    description TEXT NULL,

    duration INTEGER NULL CHECK (duration IS NULL OR duration > 0), -- En jours
    start_date TEXT NULL CHECK (start_date IS NULL OR date(start_date) = start_date),
    end_date TEXT NULL CHECK (end_date IS NULL OR (date(end_date) = end_date AND date(end_date) >= date(start_date)))
);

CREATE TABLE IF NOT EXISTS services_fees
(
    id INTEGER PRIMARY KEY NOT NULL,

    label TEXT NOT NULL,
    description TEXT NULL,

    amount INTEGER NULL,
    formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01)

    id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE,
    id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL si le type n'est pas associé automatiquement à la compta
    id_year INTEGER NULL REFERENCES acc_years (id) ON DELETE SET NULL -- NULL si le type n'est pas associé automatiquement à la compta
);

CREATE TABLE IF NOT EXISTS services_users
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE,
    id_fee INTEGER NULL REFERENCES services_fees (id) ON DELETE CASCADE,

    paid INTEGER NOT NULL DEFAULT 0,
    expected_amount INTEGER NULL,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    expiry_date TEXT NULL CHECK (date(expiry_date) IS NULL OR date(expiry_date) = expiry_date)
);

CREATE UNIQUE INDEX IF NOT EXISTS su_unique ON services_users (id_user, id_service, date);

CREATE INDEX IF NOT EXISTS su_service ON services_users (id_service);





CREATE INDEX IF NOT EXISTS su_fee ON services_users (id_fee);
CREATE INDEX IF NOT EXISTS su_paid ON services_users (paid);
CREATE INDEX IF NOT EXISTS su_expiry ON services_users (expiry_date);

CREATE TABLE IF NOT EXISTS services_reminders
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE,

    delay INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    subject TEXT NOT NULL,
    body TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS services_reminders_sent
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE,
    id_reminder INTEGER NOT NULL REFERENCES services_reminders (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)
);

CREATE UNIQUE INDEX IF NOT EXISTS srs_index ON services_reminders_sent (id_user, id_service, id_reminder, date);


CREATE INDEX IF NOT EXISTS srs_reminder ON services_reminders_sent (id_reminder);
CREATE INDEX IF NOT EXISTS srs_user ON services_reminders_sent (id_user);

--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki
170
171
172
173
174
175
176



177
178
179

180
181


182


183



184

185

186


187



188
189
190
191
192
193



194
195
196
197
198
199
200
201
202
203
204
205


206
207

208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235

236
237
238

239
240

241
242


243
244

245
246
247
248
249
250
251
252
253
254

255
256
257
258
259

260

261
262
263
264
265
266
267
268

269
270
271
272
273
274

275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;




/*
CREATE TABLE wiki_suivi
-- Suivi des pages

(
    id_membre INTEGER NOT NULL,


    id_page INTEGER NOT NULL,






    PRIMARY KEY (id_membre, id_page),



    FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire


    FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire



);
*/

--
-- COMPTA
--




CREATE TABLE IF NOT EXISTS compta_exercices
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    libelle TEXT NOT NULL,

    debut TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(debut) IS NOT NULL AND date(debut) = debut),
    fin TEXT NULL DEFAULT NULL CHECK (fin IS NULL OR (date(fin) IS NOT NULL AND date(fin) = fin)),

    cloture INTEGER NOT NULL DEFAULT 0


);



CREATE TABLE IF NOT EXISTS compta_comptes
-- Plan comptable
(
    id TEXT NOT NULL PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
    parent TEXT NOT NULL DEFAULT 0,

    libelle TEXT NOT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a é ajouté par l'utilisateur
    desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
);

CREATE INDEX IF NOT EXISTS compta_comptes_parent ON compta_comptes (parent);

CREATE TABLE IF NOT EXISTS compta_comptes_bancaires
-- Comptes bancaires
(
    id TEXT NOT NULL PRIMARY KEY,

    banque TEXT NOT NULL,

    iban TEXT NULL,
    bic TEXT NULL,

    FOREIGN KEY(id) REFERENCES compta_comptes(id) ON DELETE CASCADE
);


CREATE TABLE IF NOT EXISTS compta_projets
-- Projets (compta analytique)

(
    id INTEGER PRIMARY KEY NOT NULL,


    libelle TEXT NOT NULL


);


CREATE TABLE IF NOT EXISTS compta_journal
-- Journal des opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL,
    remarques TEXT NULL,
    numero_piece TEXT NULL, -- N° de pièce comptable

    montant REAL NOT NULL,


    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    moyen_paiement TEXT NULL,
    numero_cheque TEXT NULL,


    compte_debit TEXT NULL, -- N° du compte dans le plan, NULL est utilisé pour une opération qui vient d'un exercice précédent

    compte_credit TEXT NULL, -- N° du compte dans le plan

    id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
    id_auteur INTEGER NULL,
    id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
    id_projet INTEGER NULL,

    FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),

    FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
    FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
    FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
    FOREIGN KEY(id_auteur) REFERENCES membres(id) ON DELETE SET NULL,
    FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) ON DELETE SET NULL,
    FOREIGN KEY(id_projet) REFERENCES compta_projets(id) ON DELETE SET NULL

);

CREATE INDEX IF NOT EXISTS compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX IF NOT EXISTS compta_operations_date ON compta_journal (date);
CREATE INDEX IF NOT EXISTS compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX IF NOT EXISTS compta_operations_auteur ON compta_journal (id_auteur);

CREATE TABLE IF NOT EXISTS compta_moyens_paiement
-- Moyens de paiement
(
    code TEXT NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL
);

--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');

CREATE TABLE IF NOT EXISTS compta_categories
-- Catégories pour simplifier le plan comptable
(
    id INTEGER NOT NULL PRIMARY KEY,
    type INTEGER NOT NULL DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)

    intitule TEXT NOT NULL,
    description TEXT NULL,

    compte TEXT NOT NULL, -- Compte affecté par cette catégorie

    FOREIGN KEY(compte) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,







>
>
>
|
|
<
>

|
>
>
|
>
>

>
>
>
|
>

>
|
>
>
|
>
>
>

<

<
<
<
>
>
>

|




|

|
|

|
>
>


>

|
|

<
<
|
<

<
|
|
<

<
<
<
<
<
<
<
|
<
|
|

<
<
>

<
<
>
|
|
>

|
>
>


>
|
<
<
<
|
<
<
<
|
<
>

|
|
<
|
>
|
>
|

<
|
<
|

|
>
|
|
|
|
|
|
>


|
|
<
|

|
|

|
<
<
|
<
<
<
<
<
<
<
|
<
<
|
|
<
|
<
<

<
|
<
<







180
181
182
183
184
185
186
187
188
189
190
191

192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215

216



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240


241

242

243
244

245







246

247
248
249


250
251


252
253
254
255
256
257
258
259
260
261
262
263



264



265

266
267
268
269

270
271
272
273
274
275

276

277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

292
293
294
295
296
297


298







299


300
301

302


303

304


305
306
307
308
309
310
311

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;

--
-- COMPTA
--

CREATE TABLE IF NOT EXISTS acc_charts

-- Plans comptables : il peut y en avoir plusieurs
(
    id INTEGER NOT NULL PRIMARY KEY,
    country TEXT NOT NULL,
    code TEXT NULL, -- NULL = plan comptable créé par l'utilisateur
    label TEXT NOT NULL,
    archived INTEGER NOT NULL DEFAULT 0 -- 1 = archivé, non-modifiable
);

CREATE TABLE IF NOT EXISTS acc_accounts
-- Comptes des plans comptables
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_chart INTEGER NOT NULL REFERENCES acc_charts ON DELETE CASCADE,

    code TEXT NOT NULL, -- peut contenir des lettres, eg. 53A, 53B, etc.

    label TEXT NOT NULL,
    description TEXT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    type INTEGER NOT NULL DEFAULT 0, -- Type de compte spécial : banque, caisse, en attente d'encaissement, etc.
    user INTEGER NOT NULL DEFAULT 1 -- 1 = fait partie du plan comptable original, 0 = a été ajouté par l'utilisateur
);





CREATE UNIQUE INDEX IF NOT EXISTS acc_accounts_codes ON acc_accounts (code, id_chart);
CREATE INDEX IF NOT EXISTS acc_accounts_type ON acc_accounts (type);
CREATE INDEX IF NOT EXISTS acc_accounts_position ON acc_accounts (position);

CREATE TABLE IF NOT EXISTS acc_years
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    label TEXT NOT NULL,

    start_date TEXT NOT NULL CHECK (date(start_date) IS NOT NULL AND date(start_date) = start_date),
    end_date TEXT NOT NULL CHECK (date(end_date) IS NOT NULL AND date(end_date) = end_date),

    closed INTEGER NOT NULL DEFAULT 0,

    id_chart INTEGER NOT NULL REFERENCES acc_charts (id)
);

CREATE INDEX IF NOT EXISTS acc_years_closed ON acc_years (closed);

CREATE TABLE IF NOT EXISTS acc_transactions
-- Opérations comptables
(


    id INTEGER PRIMARY KEY NOT NULL,



    type INTEGER NOT NULL DEFAULT 0, -- Type d'écriture, 0 = avancée (normale)
    status INTEGER NOT NULL DEFAULT 0, -- Statut (bitmask)









    label TEXT NOT NULL,

    notes TEXT NULL,
    reference TEXT NULL, -- N° de pièce comptable



    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),



    validated INTEGER NOT NULL DEFAULT 0, -- 1 = écriture validée, non modifiable

    hash TEXT NULL,
    prev_hash TEXT NULL,

    id_year INTEGER NOT NULL REFERENCES acc_years(id),
    id_creator INTEGER NULL REFERENCES membres(id) ON DELETE SET NULL,
    id_related INTEGER NULL REFERENCES acc_transactions(id) ON DELETE SET NULL -- écriture liée (par ex. remboursement d'une dette)
);

CREATE INDEX IF NOT EXISTS acc_transactions_year ON acc_transactions (id_year);
CREATE INDEX IF NOT EXISTS acc_transactions_date ON acc_transactions (date);



CREATE INDEX IF NOT EXISTS acc_transactions_related ON acc_transactions (id_related);



CREATE INDEX IF NOT EXISTS acc_transactions_type ON acc_transactions (type);

CREATE INDEX IF NOT EXISTS acc_transactions_status ON acc_transactions (status);

CREATE TABLE IF NOT EXISTS acc_transactions_lines
-- Lignes d'écritures d'une opération

(
    id INTEGER PRIMARY KEY NOT NULL,

    id_transaction INTEGER NOT NULL REFERENCES acc_transactions (id) ON DELETE CASCADE,
    id_account INTEGER NOT NULL REFERENCES acc_accounts (id), -- N° du compte dans le plan comptable


    credit INTEGER NOT NULL,

    debit INTEGER NOT NULL,

    reference TEXT NULL, -- Référence de paiement, eg. numéro de chèque
    label TEXT NULL,

    reconciled INTEGER NOT NULL DEFAULT 0,

    id_analytical INTEGER NULL REFERENCES acc_accounts(id) ON DELETE SET NULL,

    CONSTRAINT line_check1 CHECK ((credit * debit) = 0),
    CONSTRAINT line_check2 CHECK ((credit + debit) > 0)
);

CREATE INDEX IF NOT EXISTS acc_transactions_lines_account ON acc_transactions_lines (id_account);
CREATE INDEX IF NOT EXISTS acc_transactions_lines_analytical ON acc_transactions_lines (id_analytical);

CREATE INDEX IF NOT EXISTS acc_transactions_lines_reconciled ON acc_transactions_lines (reconciled);

CREATE TABLE IF NOT EXISTS acc_transactions_users
-- Liaison des écritures et des membres
(
    id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,


    id_transaction INTEGER NOT NULL REFERENCES acc_transactions (id) ON DELETE CASCADE,







    id_service_user INTEGER NULL REFERENCES services_users (id) ON DELETE SET NULL,



    PRIMARY KEY (id_user, id_transaction)

);




CREATE INDEX IF NOT EXISTS acc_transactions_users_service ON acc_transactions_users (id_service_user);



CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
    id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue







<
<
<
<
<
<
<
<







322
323
324
325
326
327
328








329
330
331
332
333
334
335
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);









CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES membres (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES wiki_pages (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta_journal"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);


CREATE TABLE IF NOT EXISTS compromised_passwords_cache
-- Cache des hash de mots de passe compromis







|
|






|
|



|


|
|










|







349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE,
    id INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE,
    id INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE,
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_acc_transactions
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id) ON DELETE CASCADE,
    id INTEGER NOT NULL REFERENCES acc_transactions (id) ON DELETE CASCADE,
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);


CREATE TABLE IF NOT EXISTS compromised_passwords_cache
-- Cache des hash de mots de passe compromis

Deleted src/include/data/categories_comptables.sql version [7430a75c4a].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INSERT INTO "compta_categories" VALUES(NULL,-1,'Prestations de service','','604');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Achat de marchandises à vendre','Marchandises destinées à être revendues en l''état.','607');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Achat de fournitures consommables','','6068');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Publicité et relations publiques','','623');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais de déplacement des membres','Billet SNCF, remboursement de frais kilométrique, etc.','625');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Locations','Locations versées pour un local ou du matériel.','613');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Fournitures non stockables : eau, électricité...','Facture d''eau, d''opérateur électrique, etc.','6061');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Fournitures administratives','Cartouches d''encre, papier, matériel bureautique, etc.','6064');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais d''actes et de contentieux','Insertion au Journal Officiel, frais de justice, etc.','6227');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais postaux et télécommunications','Facture d''accès à Internet, timbres, etc.','626');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Licences fédérales','Licences payées pour les adhérents (par exemple fédération sportive etc.)','652');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Prime d''assurance','','616');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Services bancaires','','627');
INSERT INTO "compta_categories" VALUES(NULL,-1,'Divers','','658');

INSERT INTO "compta_categories" VALUES(NULL,1,'Vente de produits finis','Vente de produits fabriqués par l''association.','701');
INSERT INTO "compta_categories" VALUES(NULL,1,'Prestation de service','','706');
INSERT INTO "compta_categories" VALUES(NULL,1,'Revente de marchandises','','707');
INSERT INTO "compta_categories" VALUES(NULL,1,'Manifestations diverses','Revenus provenant de manifestations au profit de l''association : droit d''entrée, location d''emplacement en vide grenier, ventes, etc.','7780');
INSERT INTO "compta_categories" VALUES(NULL,1,'Cotisations','','756');
INSERT INTO "compta_categories" VALUES(NULL,1,'Dons et collectes','','754');
INSERT INTO "compta_categories" VALUES(NULL,1,'Subventions','','740');
INSERT INTO "compta_categories" VALUES(NULL,1,'Divers','','758');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































Added src/include/data/charts/fr_1999.csv version [38d9f67e8d].



















































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
code,label,description,position,type
1,"Classe 1 — Comptes de capitaux (Fonds propres, emprunts et dettes assimilés)",,Passif,
10,FONDS ASSOCIATIFS ET RÉSERVES,,Passif,
102,Fonds associatif sans droit de reprise,,Passif,
1021,Valeur du patrimoine intégré,,Passif,
1022,Fonds statutaire,,Passif,
1024,Apports sans droit de reprise,,Passif,
103,Fonds associatif avec droit de reprise,,Passif,
1034,Apports avec droit de reprise,,Passif,
105,Écarts de réévaluation,,Passif,
106,Réserves,,Passif,
1063,Réserves statutaires ou contractuelles,,Passif,
1064,Réserves réglementées,,Passif,
1068,Autres réserves (dont réserves pour projet associatif),,Passif,
11,REPORT À NOUVEAU,,Passif,
110,Report à nouveau (Solde créditeur),,Passif,
119,Report à nouveau (Solde débiteur),,Passif,
12,RÉSULTAT NET DE L'EXERCICE,,Passif,
120,Résultat de l'exercice (excédent),,Passif,Résultat excédentaire
129,Résultat de l'exercice (déficit),,Passif,Résultat déficitaire
13,SUBVENTIONS D'INVESTISSEMENT AFFECTÉES A DES BIENS NON RENOUVELABLES,,Passif,
131,Subventions d'investissement (renouvelables),,Passif,
139,Subventions d'investissement inscrites au compte de résultat,,Passif,
14,PROVISIONS REGLEMENTÉES,,Passif,
15,PROVISIONS,,Passif,
151,Provisions pour risques,,Passif,
157,Provisions pour charges à répartir sur plusieurs exercices,,Passif,
158,Autres provisions pour charges,,Passif,
16,EMPRUNTS ET DETTES ASSIMILÉES,,Passif,
164,Emprunts auprès des établissements de crédits,,Passif,
165,Dépôts et cautionnements reçus,,Passif,
167,Emprunts et dettes assorties de conditions particulières,,Passif,
168,Autres emprunts et dettes assimilés,,Passif,
17,DETTES RATTACHÉES À DES PARTICIPATIONS,,Passif,
18,COMPTES DE LIAISON DES ÉTABLISSEMENTS,,Passif,
181,Apports permanents entre siège social et établissements,,Passif,
185,Biens et prestations de services échangés entre établissements et siège social,,Passif,
186,Biens et prestations de services échangés entre établissements (charges),,Passif,
187,Biens et prestations de services échangés entre établissements (produits),,Passif,
19,FONDS DÉDIÉS,,Passif,
194,Fonds dédiés sur subventions de fonctionnement,,Passif,
195,Fonds dédiés sur dons manuels affectés,,Passif,
197,Fonds dédiés sur legs et donations affectés,,Passif,
198,Excédent disponible après affectation au projet associatif,,Passif,
199,Reprise des fonds affectés au projet associatif,,Passif,
2,Classe 2 — Comptes d'immobilisations,,Actif,
20,IMMOBILISATIONS INCORPORELLES,,Actif,
200,Immobilisations incorporelles,,Actif,
21,IMMOBILISATIONS CORPORELLES,,Actif,
210,Investissements,,Actif,
22,IMMOBILISATIONS GREVÉES DE DROITS,,Actif,
228,Immobilisations grevées de droits,,Actif,
229,Droits des propriétaires,,Actif,
23,IMMOBILISATIONS EN COURS,,Actif,
231,Immobilisations corporelles en cours,,Actif,
238,Avances et acomptes versés sur commande d'immobilisations corporelles,,Actif,
26,PARTICIPATIONS ET CRÉANCES RATTACHÉES A DES PARTICIPATIONS,,Actif,
261,Titres de participation,,Actif,
27,AUTRES IMMOBILISATIONS FINANCIÈRES,,Actif,
270,Participations financières,,Actif,
275,Dépôts et cautionnements versés,,Actif,
28,AMORTISSEMENTS DES IMMOBILISATIONS,,Actif,
280,Amortissements des immobilisations incorporelles,,Actif,
281,Amortissements des immobilisations corporelles,,Actif,
29,DÉPRÉCIATION DES IMMOBILISATIONS,,Actif,
290,Dépréciation des immobilisations incorporelles,,Actif,
291,Dépréciation des immobilisations corporelles,,Actif,
3,Classe 3 — Comptes de stocks,,Actif,
31,MATIERES PREMIERES ET FOURNITURES,,Actif,
311,Matières,,Actif,
317,Fournitures,,Actif,
32,AUTRES APPROVISIONNEMENTS,,Actif,
321,Matières consommables,,Actif,
322,Fournitures consommables,,Actif,
33,EN-COURS DE PRODUCTION DE BIENS,,Actif,
331,Produits en cours,,Actif,
335,Travaux en cours,,Actif,
34,EN-COURS DE PRODUCTION DE SERVICES,,Actif,
35,STOCKS DE PRODUITS,,Actif,
351,Produits intermédiaires,,Actif,
355,Produits finis,,Actif,
358,Produits résiduels,,Actif,
3581,Déchets,,Actif,
3585,Rebuts,,Actif,
3586,Matière de récupération,,Actif,
37,STOCKS DE MARCHANDISES,,Actif,
370,Autres stocks de marchandises,,Actif,
39,PROVISIONS POUR DEPRECIATION DES STOCKS ET EN-COURS,,Actif,
391,Provisions pour dépréciation des matières premières et fournitures,,Actif,
4,Classe 4 — Comptes de tiers,,Actif ou passif,
40,FOURNISSEURS ET COMPTES RATTACHÉS,,Passif,
401,Fournisseurs,,Passif,
4010,Autres fournisseurs,,Actif ou passif,Tiers
408,Fournisseurs - Factures non parvenues,,Passif,
409,Avances aux fournisseurs,,Actif,
41,USAGERS ET COMPTES RATTACHÉS,,Actif,
411,Usagers,,Actif,
4110,Autres usagers,,Actif ou passif,Tiers
419,Avances aux usagers,,Passif,
42,PERSONNEL ET COMPTES RATTACHÉS,,Passif,
421,Personnel - Rémunérations dues,,Passif,
4210,Autres membres du personnel,,Actif ou passif,
425,Personnel - Avances et acomptes,,Actif,
428,Personnel - Charges à payer et produits à recevoir,,Actif ou passif,
43,SÉCURITÉ SOCIALE ET AUTRES ORGANISMES SOCIAUX,,Passif,
430,Dettes et crédits envers les organismes sociaux,,Passif,
431,Sécurité sociale,,Passif,
437,Autres organismes sociaux,,Passif,
4372,Mutuelles,,Passif,
4373,Caisse de retraite et de prévoyance,,Passif,
4374,Caisse d'allocations de chômage - Pôle emploi,,Passif,
4375,AGESSA,,Passif,
4378,Autres organismes sociaux - Divers,,Passif,
438,Organismes sociaux - Charges à payer et produits à recevoir,,Actif ou passif,
4382,Charges sociales sur congés à payer,,Passif,
4386,Autres charges à payer,,Passif,
4387,Produits à recevoir,,Actif,
439,Avances auprès des organismes sociaux,,Passif,
44,ÉTAT ET AUTRES COLLECTIVITÉS PUBLIQUES,,Actif,
441,État - Subventions à recevoir,,Actif,
4411,Subventions d'investissement,,Actif,
4417,Subventions d'exploitation,,Actif,
4418,Subventions d'équilibre,,Actif,
4419,Avances sur subventions,,Actif,
442,État - Impôts et taxes recouvrables sur des tiers,,Passif,
444,État - Impôts sur les bénéfices,,Actif,
445,État - Taxes sur le chiffre d'affaires,,Actif,
4455,Taxes sur le chiffre d'affaires à décaisser,,Actif,
44551,TVA à décaisser,,Actif,
44558,Taxes assimilées à la TVA,,Actif,
4456,Taxes sur le chiffre d'affaires déductibles,,Actif,
44562,TVA sur immobilisations,,Actif,
44566,TVA sur autres biens et services,,Actif,
4457,Taxes sur le chiffre d'affaires collectées par l'association,,Actif,
4458,Taxes sur le chiffre d'affaires à régulariser ou en attente,,Actif,
44581,Acomptes - Régime simplifié d'imposition,,Actif,
44582,Acomptes - Régime du forfait,,Actif,
44583,Remboursement de taxes sur le chiffre d'affaires demandé,,Actif,
44584,TVA récupérée d'avance,,Actif,
44586,Taxes sur le chiffre d'affaires sur factures non parvenues,,Actif,
44587,Taxes sur le chiffre d'affaires sur factures à établir,,Actif,
447,"Autres impôts, taxes et versements assimilés",,Passif,
4471,"Autres impôts, taxes et versements assimilés sur rémunérations (Administration des impôts)",,Passif,
44711,Taxe sur les salaires,,Passif,
44713,Participation des employeurs à la formation professionnelle continue,,Passif,
44714,Cotisation par défaut d'investissement obligatoire dans la construction,,Passif,
44718,"Autres impôts, taxes et versements assimilés",,Passif,
4473,"Autres impôts, taxes et versements assimilés sur rémunérations (Autres organismes)",,Passif,
44733,Participation des employeurs à la formation professionnelle continue,,Passif,
44734,Participation des employeurs à l'effort de construction (versements à fonds perdus),,Passif,
4475,"Autres impôts, taxes et versements assimilés (Administration des impôts)",,Passif,
4477,"Autres impôts, taxes et versements assimilés (Autres organismes)",,Passif,
448,État - Charges à payer et produits à recevoir,,Passif,
4482,Charges fiscales sur congés à payer,,Passif,
4486,Autres charges à payer,,Passif,
4487,Produits à recevoir,,Actif,
449,Avances auprès de l'état et des collectivités publiques,,Passif,
45,"CONFÉDÉRATION, FÉDÉRATION, UNIONS ET ASSOCIATIONS AFFILIÉES",,Actif ou passif,
451,"Confédération, fédération et associations affiliées - Compte courant",,Actif ou passif,
455,Sociétaires - Comptes courants,,Actif ou passif,
46,DÉBITEURS DIVERS ET CRÉDITEURS DIVERS,,Actif ou passif,
467,Autres comptes débiteurs et créditeurs,,Actif ou passif,
468,Divers - Charges à payer et produits à recevoir,,Actif ou passif,
4686,Charges à payer,,Passif,
4687,Produits à recevoir,,Actif,
47,COMPTES TRANSITOIRES OU D'ATTENTE,,Actif ou passif,
471,Recettes à classer,,Passif,
472,Dépenses à classer et à régulariser,,Actif,
48,COMPTES DE RÉGULARISATION,,Actif ou passif,
481,Charges à répartir sur plusieurs exercices,,Actif,
486,Charges constatées d'avance,,Actif,
487,Produits constatés d'avance,,Passif,
49,DEPRECIATION DES COMPTES DE TIERS,,Actif,
491,Dépréciation des comptes clients,,Actif,
496,Dépréciation des comptes débiteurs divers,,Actif,
5,Classe 5 — Comptes financiers,,Actif,
50,VALEURS MOBILIÈRES DE PLACEMENT,,Actif,
51,"BANQUES, ÉTABLISSEMENTS FINANCIERS ET ASSIMILÉS",,Actif,
511,Valeurs à l'encaissement,,Actif,
5112,Chèques à encaisser,,Actif,Attente d'encaissement
5115,Paiements par carte à encaisser,,Actif,
512,Banques,,Actif ou passif,
53,CAISSE,,Actif,
530,Caisse,,Actif,Caisse
54,RÉGIES D'AVANCES ET ACCRÉDITIFS,,Actif,
58,VIREMENTS INTERNES,,Actif,
59,PROVISIONS POUR DÉPRÉCIATION DES COMPTES FINANCIERS,,Actif,
6,Classe 6 — Comptes de charges,,Charge,
60,ACHATS,,Charge,
601,Achats stockés - Matières premières et fournitures,,Charge,
602,Achats stockés - Autres approvisionnements,,Charge,
604,Achat d'études et prestations de services,,Charge,Dépenses
606,Achats non stockés de matières et fournitures,,Charge,
6061,"Fournitures non stockables (eau, énergie...)","Facture d'eau, d'opérateur électrique, etc.",Charge,Dépenses
6063,Fournitures d'entretien et de petit équipement,,Charge,Dépenses
6064,Fournitures administratives,"Cartouches d'encre, papier, matériel bureautique, etc.",Charge,Dépenses
6068,Autres matières et fournitures,,Charge,Dépenses
607,Achats de marchandises,Marchandises destinées à être revendues en l'état.,Charge,Dépenses
61,SERVICES EXTÉRIEURS,,Charge,
611,Sous-traitance générale,,Charge,
612,Redevances de crédit-bail,,Charge,
613,Locations,Locations versées pour un local ou du matériel.,Charge,Dépenses
614,Charges locatives et de co-propriété,,Charge,
615,Entretiens et réparations,,Charge,
616,Primes d'assurance,,Charge,Dépenses
618,Divers,,Charge,
62,AUTRES SERVICES EXTÉRIEURS,,Charge,
621,Personnel extérieur à l'association,,Charge,
62141,Mises à disposition de personnel salarié,,Charge,Dépenses
622,Rémunérations d'intermédiaires et honoraires,,Charge,
6226,Honoraires,,Charge,
6227,Frais d'actes et de contentieux,"Insertion au Journal Officiel, frais de justice, etc.",Charge,Dépenses
6228,Divers,,Charge,
623,"Publicité, publications, relations publiques","Bulletins, affiches, communication, etc.",Charge,Dépenses
624,Transports de biens et transports collectifs du personnel,,Charge,
625,"Déplacements, missions et réceptions","Billet SNCF, remboursement de frais kilométrique, etc.",Charge,Dépenses
626,Frais postaux et de télécommunications,"Facture d'accès à Internet, timbres, etc.",Charge,Dépenses
627,Services bancaires et assimilés,Frais bancaires,Charge,Dépenses
628,Divers,,Charge,Dépenses
63,"IMPÔTS, TAXES ET VERSEMENTS ASSIMILÉS",,Charge,
631,"Impôts, taxes et versements assimilés sur rémunérations (Administration des impôts)",,Charge,
6311,Taxes sur les salaires,,Charge,
6313,Participations des employeurs à la formation professionnelle continue,,Charge,
635,"Autres impôts, taxes et versements assimilés (Administration des impôts)",,Charge,
6351,Impôts directs (sauf impôts sur les bénéfices),,Charge,
6353,Impôts indirects,,Charge,
637,"Autres impôts, taxes et versements assimilés (Autres organismes)",,Charge,
64,CHARGES DE PERSONNEL,,Charge,
641,Rémunérations du personnel,,Charge,
643,Rémunérations du personnel artistique et assimilés,,Charge,
645,Charges de sécurité sociale et de prévoyance,,Charge,
647,Autres charges sociales,,Charge,
648,Autres charges de personnel,,Charge,
65,AUTRES CHARGES DE GESTION COURANTE,,Charge,
652,Licences fédérales,Licences payées pour les adhérents (par exemple fédération sportive etc.),Charge,Dépenses
658,Charges diverses de gestion courante,,Charge,Dépenses
66,CHARGES FINANCIÈRES,,Charge,
661,Charges d'intérêts,,Charge,
67,CHARGES EXCEPTIONNELLES,,Charge,
670,Charges exceptionnelles,Autres dépenses exceptionnelles,Charge,Dépenses
671,Charges exceptionnelles sur opérations de gestion,,Charge,
6713,"Dons, libéralités",,Charge,
678,Autres charges exceptionnelles,,Charge,
6788,Charges exceptionnelles diverses,,Charge,
68,"DOTATIONS AUX AMORTISSEMENTS, DÉPRÉCIATIONS, PROVISIONS ET ENGAGEMENTS",,Charge,
681,"Dotations aux amortissements, dépréciations et provisions - Charges d'exploitation",,Charge,
6811,Dotations aux amortissements des immobilisations incorporelles et corporelles,,Charge,
68111,Immobilisations incorporelles,,Charge,
68112,Immobilisations corporelles,,Charge,
686,"Dotations aux amortissements, dépréciations et provisions - Charges financières",,Charge,
69,PARTICIPATION DES SALARIÉS - IMPÔTS SUR LES BÉNÉFICES ET ASSIMILÉS,,Charge,
695,Impôts sur les sociétés (y compris impôts sur les sociétés des personnes morales non lucratives),,Charge,
7,Classe 7 — Comptes de produits,,Produit,
70,"VENTES DE PRODUITS FINIS, PRESTATIONS DE SERVICES, MARCHANDISES",,Produit,
701,Ventes de produits finis,Vente de produits fabriqués par l'association.,Produit,Recettes
706,Prestations de services,,Produit,Recettes
707,Ventes de marchandises,,Produit,Recettes
708,Produits des activités annexes,,Produit,
71,PRODUCTION STOCKÉE (OU DÉSTOCKAGE),,Produit,
72,PRODUCTION IMMOBILISÉE,,Produit,
74,SUBVENTIONS D'EXPLOITATION,,Produit,
740,Subventions reçues,,Produit,Recettes
75,AUTRES PRODUITS DE GESTION COURANTE,,Produit,
754,Collectes,,Produit,Recettes
756,Cotisations,,Produit,Recettes
758,Produits divers de gestion courante,,Produit,
7587,Ventes de dons en nature,,Produit,
7588,Autres produits de la générosité du public,,Produit,
76,PRODUITS FINANCIERS,,Produit,
760,Produits financiers,,Produit,
77,PRODUITS EXCEPTIONNELS,,Produit,
771,Produits exceptionnels sur opérations de gestion,,Produit,
7713,Libéralités reçues,,Produit,
7715,Subventions d'équilibre,,Produit,
775,Produits des cessions d'éléments d'actifs,,Produit,
778,Autres produits exceptionnels,,Produit,
7780,Manifestations diverses,"Revenus provenant de manifestations au profit de l'association : droit d'entrée, location d'emplacement en vide grenier, ventes, etc.",Produit,Recettes
7788,Produits exceptionnels divers,,Produit,
78,REPRISES SUR AMORTISSEMENTS ET PROVISIONS,,Produit,
79,TRANSFERT DE CHARGES,,Produit,
791,Transferts de charges d'exploitation,,Produit,
796,Transferts de charges financières,,Produit,
797,Transferts de charges exceptionnels,,Produit,
8,Classe 8 ­— Comptes spéciaux,,,
86,RÉPARTITION PAR NATURE DE CHARGES,,Charge,
861,Mise à dispositions gratuites de biens,,Charge,
862,Prestations,,Charge,
864,Personnel bénévole,,Charge,
87,RÉPARTITION PAR NATURE DE RESSOURCES,,Produit,
870,Bénévolat,,Produit,
871,Prestations en nature,,Produit,
875,Dons en nature,,Produit,
89,BILAN,,,
890,Bilan d'ouverture,,,Ouverture
891,Bilan de clôture,,,Clôture
9,Classe 9 — Comptes analytiques,,,
99,Projets,,,Analytique

Added src/include/data/charts/fr_2018.csv version [7724166e91].

















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
code,label,description,position,type
1,"Classe 1 — Comptes de capitaux (Fonds propres, emprunts et dettes assimilés)",,Passif,
10,FONDS ASSOCIATIFS ET RÉSERVES,,Passif,
102,Fonds associatifs sans droit de reprise,,Passif,
1021,Première situation nette établie,,Passif,
1022,Fonds statutaires,,Passif,
1023,Dotations non consomptibles,,Passif,
10231,Dotations non consomptibles initiales,,Passif,
10232,Dotations non consomptibles complémentaires,,Passif,
1024,Autres fonds propres sans droit de reprise,,Passif,
103,Fonds associatif avec droit de reprise,,Passif,
1032,Fonds statutaires,,Passif,
1034,Autres fonds propres avec droit de reprise,,Passif,
105,Ecarts de réévaluation,,Passif,
1051,Ecarts réévaluation sur biens sans dt reprise,,Passif,
1052,Ecarts réévaluation sur biens avec dt reprise,,Passif,
106,Réserves,,Passif,
1062,Réserves indisponibles,,Passif,
1063,Réserves statutaires,,Passif,
1064,Réserves réglementées,,Passif,
1068,Réserves pour projet de l’entité,,Passif,
108,Dotations consomptibles,,Passif,
1081,Dotations consomptibles,,Passif,
1089,Dot. consomptibles inscrites au cpte de résul,,Passif,
11,REPORT à NOUVEAU,,Passif,
110,Report à nouveau (Solde créditeur),,Passif,
119,Report à nouveau (Solde débiteur),,Passif,
12,RÉSULTAT NET DE L'EXERCICE,,Passif,
120,Résultat de l'exercice (excédent),,Passif,Résultat excédentaire
129,Résultat de l'exercice (déficit),,Passif,Résultat déficitaire
13,SUBVENTIONS D'INVESTISSEMENTS,,Passif,
131,Subventions d'équipement,,Passif,
139,Subventions inscrites au compte de résultat,,Passif,
14,PROVISIONS RÉGLEMENTÉES,,Passif,
148,Autres provisions réglementées,,Passif,
15,PROVISIONS POUR RISQUES ET CHARGES,,Passif,
151,Provisions pour risques,,Passif,
152,Provisions pour charges sur legs ou donations,,Passif,
153,Provisions pour pensions et obligations simil,,Passif,
155,Provisions pour impôts,,Passif,
157,Provisions pour charges à répartir,,Passif,
158,Autres provisions pour charges,,Passif,
16,EMPRUNTS ET DETTES ASSIMILÉES,,Passif,
163,Autres emprunts obligataires,,Passif,
1631,Titres associatifs et assimilés,,Passif,
164,Emprunts auprès des établissements de crédit,,Passif,
165,Dépôts et cautionnements reçus,,Passif,
1651,Dépôts,,Passif,
1655,Cautionnements,,Passif,
167,Emprunts et dettes sous conditions particulières,,Passif,
168,Autres emprunts et dettes assimilées,,Passif,
18,COMPTES DE LIAISONS,,Passif,
181,Apports permanents siège-établissements,,Passif,
185,Biens & PS échangés siège-établissements,,Passif,
186,Biens & PS entre établissements - Charges,,Passif,
187,Biens & PS entre établissements - Produits,,Passif,
19,FONDS DÉDIÉS OU REPORTÉS,,Passif,
191,Fonds reportés liés aux legs ou donations,,Passif,
1911,Legs ou donations,,Passif,
1912,Donations temporaires d’usufruit,,Passif,
194,Fonds dédiés sur subventions fonctionnement,,Passif,
195,F.d. / contributions financières autres org.,,Passif,
196,F.d. / ressources liées à la générosité,,Passif,
2,Classe 2 — Comptes d'immobilisations,,Actif,
20,IMMOBILISATIONS INCORPORELLES,,Actif,
201,Frais d'établissement,,Actif,
203,Frais de recherche et de développement,,Actif,
204,Donations temporaires d’usufruit,,Actif,
205,"Brevets, licences, marques...",,Actif,
206,Droit au bail,,Actif,
208,Autres immobilisations incorporelles,,Actif,
21,IMMOBILISATIONS CORPORELLES,,Actif,
211,Terrains,,Actif,
212,Agencements / aménagements de terrains,,Actif,
2131,Bâtiments,,Actif,
2135,"Installations, agencements...de constructions",,Actif,
214,Constructions sur sol d'autrui,,Actif,
215,"Installations techniques, matériel, outillage",,Actif,
2154,Matériel industriel,,Actif,
2155,Outillage industriel,,Actif,
218,Autres immobilisations corporelles,,Actif,
2181,"Installations, agencements, aménagem. divers",,Actif,
2182,Matériel de transport,,Actif,
2183,Matériel bureau et informatique,,Actif,
2184,Mobilier,,Actif,
23,IMMOBILISATIONS EN COURS,,Actif,
231,Immobilisations corporelles en cours,,Actif,
238,"Avances, acomptes sur immobilis. corporelles",,Actif,
24,BIEN DESTINÉS À ÊTRE CÉDÉS,,Actif,
240,Biens reçus par legs ou donations à céder,,Actif,
26,PARTICIPATIONS ET CRÉANCES RATTACHÉES,,Actif,
261,Titres de participation,,Actif,
266,Autres formes de participation,,Actif,
267,Créances rattachées à des participations,,Actif,
269,Versements restants sur participations,,Actif,
27,IMMOBILISATIONS FINANCIERES,,Actif,
271,Titres immobilisés (droit de propriété),,Actif,
272,Titres immobilisés (droit de créance),,Actif,
274,Prêts,,Actif,
2742,Prêts aux partenaires,,Actif,
275,Dépôts et cautionnements versés,,Actif,
276,Autres créances immobilisées,,Actif,
28,AMORTISSEMENTS DES IMMOBILISAT,,Actif,
2801,Amt. frais d'établissement,,Actif,
2804,Amt. donations temporaires d’usufruit,,Actif,
2805,"Amt brevets, licences, marques...",,Actif,
2806,Amt. droit au bail,,Actif,
2808,Amt. autres immo.incorporelles,,Actif,
2812,"Amt.agencements, aménagements de terrains",,Actif,
28131,Amortissements bâtiments,,Actif,
28135,"Amt. installations, agencements...",,Actif,
2814,Amt.constructions sur sol d'autrui,,Actif,
2815,"Amt instal. techniques, matériel, outillage",,Actif,
28181,"Amt. intallations, agencements, aménagements",,Actif,
28182,Amt. matériel de transport,,Actif,
28183,"Amortiss.matériel de bureau, informatique",,Actif,
28184,Amortissements du mobilier,,Actif,
29,DÉPRÉCIATIONS DES IMMOBILISATIONS,,Actif,
2904,Donations temporaires d'usufruit,,Actif,
2905,"Brevets, licences, marques...",,Actif,
2906,Droit au bail,,Actif,
2908,Autres immobilisations incorporelles,,Actif,
2911,Terrains,,Actif,
2931,Immobilisations corporelles en cours,,Actif,
294,Biens reçus par legs ou donations à céder,,Actif,
2961,Titres de participations,,Actif,
2966,Autres formes de participations,,Actif,
2967,Créances rattachées à des participations,,Actif,
2971,Titres immobilisés (droit de propriété),,Actif,
2972,Titres immobilisés (droit de créance),,Actif,
2974,Prêts,,Actif,
2975,Dépôts et cautionnements versés,,Actif,
2976,Autres créances immobilisées,,Actif,
3,Classe 3 — Comptes de stocks,,Actif,
31,MATIÈRES PREMIÈRES ET FOURNITURES,,Actif,
318,Matières premières et fournitures,,Actif,
32,AUTRES APPROVISIONNEMENTS,,Actif,
321,Matières consommables,,Actif,
322,Fournitures consommables,,Actif,
326,Emballages,,Actif,
33,EN-COURS DE PRODUCTION DE BIENS,,Actif,
331,Produits en cours,,Actif,
335,Travaux en cours,,Actif,
34,EN-COURS DE PROUCTION DE SERVICES,,Actif,
341,Etudes en cours,,Actif,
345,Prestations de services en cours,,Actif,
35,STOCKS DE PRODUITS,,Actif,
351,Produits intermédiaires,,Actif,
355,Produits finis,,Actif,
358,Produits résiduels,,Actif,
37,STOCKS DE MARCHANDISES,,Actif,
370,Stocks de marchandises,,Actif,
39,PROVISIONS PR DÉPRECIATIONS STOCKS & EN-COURS,,Actif,
391,Matières premières et fournitures,,Actif,
392,Autres approvisionnements,,Actif,
393,En-cours de production de biens,,Actif,
394,En-cours de production de services,,Actif,
395,Stocks de produits,,Actif,
397,Stocks de marchandises,,Actif,
4,Classe 4 — Comptes de tiers,,Actif ou passif,
40,FOURNISSEURS ET COMPTES RATTACHÉS,,Actif ou passif,
401,Fournisseurs,,Actif ou passif,
4010,Autres fournisseurs,,Actif ou passif,Tiers
403,Fournisseurs - Effets à payer,,Passif,
404,Fournisseurs d'immobilisations,,Actif ou passif,
405,Fournisseurs d'immos - Effets à payer,,Passif,
408,Fournisseurs - Factures non parvenues,,Passif,
4091,Fournisseurs - Avances & acomptes,,Actif,
41,USAGERS ET COMPTES RATTACHÉS,,Actif ou passif,
411,Usagers,,Actif ou passif,
4110,Autres usagers,Pour les dettes ou créances des membres,Actif ou passif,Tiers
413,Usagers - Effets à recevoir,,Actif,
416,Usagers douteux ou litigieux,,Actif,
418,Usagers non encore facturés,,Actif,
4191,Usagers créditeurs : Avances et acomptes,,Passif,
42,PERSONNEL ET COMPTES RATTACHÉS,,Actif ou passif,
421,Personnel : Rémunérations dues,,Passif,
4210,Autres membres du personnel,Dettes dûes aux salarié⋅e⋅s,Actif ou passif,Tiers
422,"Comités d'entreprise, d'établissement",,Actif ou passif,
425,Personnel : Avances & acomptes,,Actif,
427,Personnel - Oppositions,,Passif,
4286,Personnel- Charges à payer,,Passif,
4287,Personnel- Produits à recevoir,,Actif,
43,SÉCURITÉ SOCIALE &  AUTRES ORGANISMES SOCIAUX,,Passif,
431,Sécurité sociale,,Passif,
4372,Mutuelles,,Passif,
4373,Caisses de retraites et de prévoyance,,Passif,
4378,Autres organismes sociaux,,Passif,
44,ETAT ET AUTRES COLLECTIVITÉS PUBLIQUES,,Actif,
441,Etat - Subventions à recevoir,,Actif,
4421,Prélèvements à la source- Impôt sur le revenu,,Actif ou passif,
444,Etat - Impôts sur les bénéfices,,Actif ou passif,
4452,TVA due intracommunautaire,,Actif ou passif,
4455,Taxes sur CA à décaisser,,Actif,
44562,TVA déductible sur immobilisations,,Actif,
44566,TVA déductible sur autres biens et services,,Actif,
44571,TVA normale collectée,,Actif,
445711,TVA réduite collectée,,Actif,
445712,TVA super-réduite collectée,,Actif,
445713,TVA intermédiaire collectée,,Actif,
4458,Taxe sur CA à régulariser ou en attente,,Actif,
447,"Autres impôts, taxes et versements assimilés",,Actif,
4486,Etat - Charges à payer,,Passif,
4487,Etat - Produits à recevoir,,Actif,
45,"CONFÉDÉRATION, FEDERATION, UNIONS...AFFILIÉES",,Actif ou passif,
451,"Confédération, fédération at assoc. affiliées",,Actif ou passif,
455,Partenaires - comptes courants,,Actif ou passif,
46,DEBITEURS DIVERS ET CREDIT.DVS,,Actif ou passif,
461,Créances reçues par legs ou donations,,Actif,
466,Dettes des legs ou donations,,Passif,
4671,Débiteurs divers,,Actif ou passif,
4672,Créditeurs divers,,Actif ou passif,
4681,Frais des bénévoles,,Actif ou passif,
4686,Divers - Charges à payer,,Passif,
4687,Divers - Produits à recevoir,,Actif,
47,COMPTES D'ATTENTE,,Actif ou passif,
4715,Compte de transit,,Actif ou passif,
4718,Compte d'attente,,Actif ou passif,
48,COMPTES DE REGULARISATION,,Actif ou passif,
481,Charges à répartir,,Passif,
486,Charges constatées d'avance,,Passif,
487,Produits constatés d'avance,,Actif,
49,DÉPRECIATIONS DES COMPTES DE TIERS,,Actif ou passif,
491,Prov. pour dépreciation des comptes d'usagers,,Passif,
496,Prov. pour dépreci. cptes débiteurs divers,,Passif,
5,Classe 5 — Comptes financiers,,Actif,
50,VALEURS MOBILIERES DE PLACEMENT,,Actif,
503,Actions,,Actif,
506,Obligations,,Actif,
508,Autres VMP et créances assimilées,,Actif,
51,"BANQUES, ETABLISSEMENTS FINANCIERS",,Actif,
5112,Chèques à encaisser,,Actif,Attente d'encaissement
5115,Paiements par carte à encaisser,,Actif,
512,Banques,,Actif ou passif,
5186,Intérêts courus à payer,,Passif,
5187,Intérêts courus à recevoir,,Actif,
53,Caisses,,Actif ou passif,
530,Caisse,,Actif,Caisse
58,VIREMENTS INTERNES,,Actif ou passif,
580,Virements internes,,Actif ou passif,
59,DEPRECIATIONS DES COMPTES FINANCIERS,,Actif ou passif,
5903,Actions,,Actif ou passif,
5906,Obligations,,Actif ou passif,
5908,Autres VMP et créances assimilées,,Actif ou passif,
6,Classe 6 — Comptes de charges,,Charge,
60,ACHATS (SAUF 603),,Charge,
601,Achats stockés - Matières premières et fournitures,,Charge,
6010,Achats stockés de matières et fournitures,,Charge,
6011,Achats stockés - Matières premières,,Charge,
6017,Achats stockés - Fournitures,,Charge,
602,Achats stockés - Autres approvisionnements,,Charge,
6021,Achats stockés - Matières consommables,,Charge,
60221,Achats stockés - Combustibles,,Charge,
60222,Achats stockés - Produits d'entretien,,Charge,
60223,Achats stockés - Fournitures d'atelier,,Charge,
60224,Achats stockés - Fournitures de magasin,,Charge,
60225,Fournitures de bureau,,Charge,
6026,Achats stockés - Emballages,,Charge,
603,Variations de stocks,,Charge,
6031,Variations de stocks matières & fournitures,,Charge,
6032,Variations stocks autres approvisionnements,,Charge,
6037,Variations de stocks de marchandises,,Charge,
604,Achats d'études et prestations de services,,Charge,
605,"Achats matériel, équipements & travaux",,Charge,
606,Achats non stockés de matières et fournitures,,Charge,
6061,"Fournitures non stockables (eau, énergie...)","Facture d'eau, d’électricité, etc.",Charge,Dépenses
60611,Eau,,Charge,
60612,Electricité,,Charge,
60613,Chauffage,,Charge,
6063,Fournitures d'entretien et petit équipement,"Vis, et matériel de bricolage (sauf outils) par exemple",Charge,Dépenses
6064,Fournitures administratives,"Cartouches d'encre, papier, matériel bureautique, etc.",Charge,Dépenses
6065,Petits logiciels,Par exemple contribution à un logiciel de gestion associative génial :-),Charge,Dépenses
6068,Autres fournitures & matières,,Charge,Dépenses
607,Achats de marchandises,Marchandises destinées à être revendues en l'état.,Charge,Dépenses
608,Frais accessoires d'achats,,Charge,
60811,Frais accessoires d'achats sur matières,,Charge,
60817,Frais accessoires d'achats sur fournitures,,Charge,
609,"Rabais, remises et ristournes sur achats",,Charge,
6091,RRR sur achats matières et fournitures,,Charge,
6092,RRR sur achats et autres approvisionnements,,Charge,
6097,RRR sur achats de marchandises,,Charge,
61,SERVICES EXTERIEURS,,Charge,
611,Sous-traitance générale,,Charge,
6122,Redevance crédit-bail mobilier,,Charge,
6125,Redevances crédit-bail immobilier,,Charge,
6132,Locations immobilières,Locations versées pour un local ou du matériel.,Charge,Dépenses
6135,Locations mobilières,,Charge,
6136,Malis sur emballages,,Charge,
614,Charges locatives et de copropriété,,Charge,
6152,Entretien sur biens immobiliers,,Charge,
6155,Entretien sur biens mobiliers,,Charge,
6156,Maintenance,,Charge,
616,Primes d'assurance,"Frais d’assurance local, activité, etc.",Charge,Dépenses
6161,Primes d'assurances mutirisques,,Charge,
6164,Primes d'assurances / risques d'exploitation,,Charge,
6165,Primes d'assurances / insolvabilité usagers,,Charge,
6168,Autres assurances,,Charge,
617,Etudes et recherches,,Charge,
6181,Documentation générale,,Charge,
6183,Documentation technique,,Charge,
6185,"Frais de colloques, séminaires, conférences",,Charge,
6187,Prestations administratives,,Charge,
62,AUTRES SERVICES EXTERIEURS,,Charge,
621,Personnel extérieur à l'association,,Charge,
6211,Personnel intérimaire,,Charge,
6214,Personnel détaché ou prêté à l'association,,Charge,
62141,Mises à disposition de personnel salarié,Frais de mise à disposition via un groupement d’employeurs,Charge,Dépenses
622,Rémunérations d'intermédiaires et honoraires,,Charge,
6221,Commissions ... sur achats,,Charge,
6222,Commissions... sur ventes,,Charge,
6226,Honoraires,,Charge,
62264,Honoraires sur legs ou donations à céder,,Charge,
6227,Frais d'actes et de contentieux,,Charge,
6228,Rémunérations div .intermédiaires & honor.,,Charge,
623,"Publicité, publications, relations publiques","Bulletins, affiches, communication, etc.",Charge,Dépenses
6231,Annonces et insertions,,Charge,
6232,Fêtes et cérémonies,,Charge,
6233,Foires et expositions,,Charge,
6234,Cadeaux,,Charge,
6236,Catalogues et imprimés,,Charge,
6237,Publications,,Charge,
6238,"Divers : pourboires, dons courants",,Charge,
624,Transports de biens...,,Charge,
6241,Transports sur achats,,Charge,
6242,Transports sur ventes,,Charge,
6243,Transports entre établissements,,Charge,
6244,Transports administratifs,,Charge,
6247,Transports collectifs du personnel,,Charge,
6248,Transports divers,,Charge,
625,"Déplacements, missions et réceptions","Billet de train, remboursement de frais kilométrique, etc.",Charge,Dépenses
6251,Voyages et déplacements,,Charge,
6255,Frais de déménagement,,Charge,
6256,Frais de missions,,Charge,
6257,"Frais de réceptions, représentations",,Charge,
626,Frais postaux et de télécommunications,"Facture d'accès à Internet, timbres, etc.",Charge,Dépenses
6261,Liaisons spécialisées,,Charge,
6263,"Affranchissements, frais postaux",,Charge,
6265,Téléphone,,Charge,
627,Services bancaires et assimilés,Frais bancaires,Charge,Dépenses
628,Divers,,Charge,Dépenses
6281,Cotisations (liées à l'activité économique),,Charge,
6284,Frais de recrutement du personnel,,Charge,
63,"IMPÔTS, TAXES ET VERSEMENTS ASSIMILÉS",,Charge,
631,Sur rémunérations - administration des impôts,,Charge,
6311,Taxe sur les salaires,,Charge,
633,Sur rémunérations - autres organismes,,Charge,
6331,Versement de transport,,Charge,
6332,Allocation logement,,Charge,
6333,Formation professionnelle continue,,Charge,
6334,Participations employeurs à l'effort de const,,Charge,
635,Autres - Administration des impôts,,Charge,
63512,Taxes foncières,,Charge,
63513,Autres impôts locaux,,Charge,
6354,Droits d'enregistrement et de timbre,,Charge,
637,Autres - Autres organismes,,Charge,
64,CHARGES DE PERSONNEL,,Charge,
641,Rémunérations du personnel,,Charge,
6411,"Salaires, appointements",,Charge,
6412,Congés payés,,Charge,
6413,Primes et gratifications,,Charge,
6414,Indemnités et avantages divers,,Charge,
6415,Supplément familial,,Charge,
645,Charges de sécurité sociale et de prévoyance,,Charge,
6451,Cotisations à l'URSSAF,,Charge,
6452,Cotisations aux mutuelles,,Charge,
6453,Cotisations caisses de retraites et de prévoy,,Charge,
6458,Cotisations aux autres organismes sociaux,,Charge,
647,Autres charges sociales,,Charge,
6472,Versements aux comités d'entreprise et d'étab,,Charge,
6473,Versement aux comités d'hygiène et de sécurit,,Charge,
6474,Versements aux autres oeuvres sociales,,Charge,
6475,"Médecine du travail, pharmacie",,Charge,
648,Autres charges de personnel,,Charge,
6481,Indemnités du personnel de culte,,Charge,
6485,Charges sociales sur indemnités de culte,,Charge,
6488,Autres charges de personnel,,Charge,
65,AUTRES CHARGES DE GESTION COURANTE,,Charge,
6511,"Redevances pour concessions, brevets, licenc.",,Charge,
6516,Droits d'auteur et de reproduction,,Charge,
6518,Autres droits et valeurs similaires,,Charge,
652,Licences fédérales,Licences payées pour les adhérents (par exemple fédération sportive etc.),Charge,Dépenses
653,Charges de la générosité du public,,Charge,
6531,Autres charges sur legs ou donations,,Charge,
654,Pertes sur créances irrécouvrables,,Charge,
655,Quotes-parts sur opérations faites en commun,,Charge,
657,Aides financières,,Charge,
6571,Aides financières octroyées,,Charge,
6572,Quotes-parts de générosité reversée,,Charge,
658,Charges diverses de gestion courante,,Charge,Dépenses
6586,Cotisations (vie statutaire),,Charge,
6588,Charges diverses de gestion courante,,Charge,
66,CHARGES FINANCIERES,,Charge,
661,Charges d'intérêts,,Charge,
665,Escomptes accordés,,Charge,
666,Pertes de changes,,Charge,
667,Charges nettes sur cessions de VMP,,Charge,
668,Autres charges financières,,Charge,
67,CHARGES EXEPTIONNELLES,,Charge,
670,Charges exceptionnelles,Autres dépenses exceptionnelles,Charge,Dépenses
6712,"Pénalités, amendes fiscales at pénales",,Charge,
6713,"Dons, libéralités",,Charge,
6714,Créances devenues irrécouvrables,,Charge,
6718,Autres charges exceptionnelles de gestion,,Charge,
673,Apports ou affectations en numéraire,,Charge,
675,Valeurs comptables des éléments d'actifs cédés,,Charge,
6750,Valeurs comptables des actifs cédés,,Charge,
6754,Immobilisations reçues par legs ou donations,,Charge,
678,Autres charges exceptionnelles sur opération en capital,,Charge,
68,"DOT. AUX AMORTISS., DÉPRÉC. ET ENGAGEMENTS",,Charge,
6811,Dot. aux amortissements des immobilisations,,Charge,
6812,Dot. aux amortissements charges à répartir,,Charge,
6815,Dot. aux provisions d'exploitation,,Charge,
6816,Dot. provisions pour dépréciations des immos,,Charge,
68164,Dot. pr dépréc. d’actifs reçus par legs ou donations,,Charge,
6817,Dot. aux dépréciations des actifs circulants,,Charge,
68173,Dotations dépréciations stocks et en-cours,,Charge,
68174,Dotations dépréciations créances,,Charge,
686,Dot. aux amort. & prov. - Charges financières,,Charge,
68662,Dot. aux amort. & prov immos financières,,Charge,
68665,Dot. aux amort. & prov VMP,,Charge,
687,Dot. aux amort. & prov. - Charges exceptionn.,,Charge,
689,Reports en fonds dédiés,,Charge,
6891,Reports en fonds reportés,,Charge,
6894,Reports en fonds dédiés / subventions d’exploitation,,Charge,
6895,Reports en fds dédiés / contributions financières d'autres organismes,,Charge,
6896,Reports en fds dédiés / ressources générosité,,Charge,
69,IMPÔTS SUR LES BENEFICES,,Charge,
695,IS sur personnes non lucratives,,Charge,
7,Classe 7 — Comptes de produits,,Produit,
70,"VENTES PROD.FINIS, MARCHANDISES, PRESTATIONS",,Produit,
701,Ventes de produits finis,Vente de produits fabriqués par l'association.,Produit,Recettes
702,Ventes de produits intermédiaires,,Produit,
703,Ventes de produits résiduels,,Produit,
704,Travaux,,Produit,
705,Etudes,,Produit,
706,Prestations de services,,Produit,Recettes
7063,Parrainages,,Produit,
707,Ventes de marchandises,Ventes de produits achetés et revendus en l’état,Produit,Recettes
7073,Ventes de dons en nature,,Produit,
708,Produits des activités annexes,,Produit,
7081,Produits des prestations fournies au personne,,Produit,
7083,Locations diverses,,Produit,
7085,Ports et frais accessoires facturés,,Produit,
7088,Autres produits d'activités annexes,,Produit,
709,RRR accordés,,Produit,
7091,RRR sur ventes de produits finis,,Produit,
7092,RRR sur ventes de produits intermédiaires,,Produit,
7094,RRR sur travaux,,Produit,
7095,RRR sur études,,Produit,
7096,RRR sur prest.de services,,Produit,
7097,RRR sur ventes marchandises,,Produit,
71,PRODUCTION STOCKEE,,Produit,
713,"Variation de stocks (en-cours, productions)",,Produit,
7133,Variation des en-cours de production de biens,,Produit,
7134,Variation des en-cours de production services,,Produit,
7135,Variations de stocks de produits,,Produit,
72,PRODUCTION IMMOBILISEE,,Produit,
721,Production immobilisée incorporelle,,Produit,
722,Production immobilisée corporelle,,Produit,
73,CONCOURS PUBLICS,,Produit,
730,Concours publics,,Produit,
74,SUBVENTION D'EXPLOITATION,,Produit,
740,Subventions reçues,,Produit,Recettes
748,Subventions d'exploitation diverses,,Produit,
75,AUTRES PRODUITS DE GESTION COURANTE,,Produit,
751,"Redevances pour concessions, licences...",,Produit,
753,Versements des fondateurs ou consommation dot,,Produit,
7531,Versements des fondateurs,,Produit,
7532,Quotes-parts de dotation consomptible virée a,,Produit,
754,Ressources liées à la générosité du public,Dons reçus,Produit,Recettes
7541,Dons manuels,,Produit,
75411,Dons manuels,,Produit,
75412,Abandons de frais par les bénévoles,,Produit,
7542,Mécénats,,Produit,
7543,"Legs, donations et assurances-vie",,Produit,
75431,Assurances-vie,,Produit,
75432,Legs ou donations,,Produit,
75433,Autres produits sur legs ou donations,,Produit,
755,Contributions financières,,Produit,
7551,Contributions financières d’autres organismes,,Produit,
7552,Quotes-parts de générosité reçues,,Produit,
756,Cotisations,Cotisations des adherent⋅e⋅s,Produit,Recettes
7561,Cotisations sans contrepartie,,Produit,
7562,Cotisations avec contrepartie,,Produit,
757,Gains de change / créances et dettes d’exploitation,,Produit,
758,Produits divers de gestion courante,,Produit,
7588,Autres produits divers de gestion courante,,Produit,
76,PRODUITS FINANCIERS,,Produit,
761,Produits des participations,,Produit,
762,Produits des autres immobilisat. financières,,Produit,
763,Revenus des autres créances,,Produit,
764,Revenus des VMP,,Produit,
765,Escomptes obtenus,,Produit,
766,Gains de change,,Produit,
767,Produits nets sur cession VMP,,Produit,
768,Autres produits financiers,,Produit,
77,PRODUITS EXCEPTIONNELS,,Produit,
771,Produits exception. sur opération de gestion,,Produit,
7713,Libéralités perçues,,Produit,
7718,Autres produits exception. sur op. de gestion,,Produit,
775,Produits des cessions d'actif,,Produit,
7754,Immos reçues en legs ou donations à céder,,Produit,
777,QP subvention d'investissement virée au résul,,Produit,
778,Autres produits exceptionnels,,Produit,
7780,Manifestations diverses,"Revenus provenant de manifestations au profit de l'association : droit d'entrée, location d'emplacement en vide grenier, ventes, etc.",Produit,Recettes
78,"REPRISES SUR AMORT., DÉPRÉC., ENGAGEMENTS",,Produit,
781,Reprises / amt & prov. d'exploitation,,Produit,
7811,Amt immos corporelles & incorporelles,,Produit,
7815,Reprises sur provisions d'exploitation,,Produit,
7816,Déprec. immos corporelles & incorporelles,,Produit,
78164,Reprises dépréc. d’actifs reçus par legs ou donations destinés à être cédés,,Produit,
7817,Dépréciations actifs circulant,,Produit,
786,Reprises / amt & prov. financiers,,Produit,
7865,Risques & charges financiers,,Produit,
7866,Déprec.des éléments financiers,,Produit,
787,Reprises sur amt & prov. exceptionnelles,,Produit,
7872,Provisions réglementées - Immobilisations,,Produit,
7873,Provisions réglementées - stocks,,Produit,
7874,Autres provisions réglementées,,Produit,
7875,Risques et charges,,Produit,
7876,Dépréciations exceptionnelles,,Produit,
789,Utilisations fds reportés et de fds dédiés,,Produit,
7891,Utilisations de fonds reportés,,Produit,
7894,Utilisations des fonds dédiés / subventions,,Produit,
7895,Utilisations des fds dédiés / contributions,,Produit,
7896,Utilisations des fds dédiés / générosité,,Produit,
79,TRANSFERT DE CHARGES,,Produit,
791,Transferts de charges d'exploitation,,Produit,
796,Transferts de charges financières,,Produit,
797,Transferts de charges exceptionnelles,,Produit,
8,Classe 8 ­— Comptes spéciaux,,,
86,EMPLOIS CONTRIBUTIONS VOLONTAIRES EN NATURE,,,
860,Secours en nature,,,
8601,Alimentaires,,,
8602,Vestimentaires,,,
861,Mise à dispositions gratuites de biens,,,
8611,Locaux,,,
8612,Matériels,,,
862,Prestations,,,
864,Personnel bénévole,,,
87,CONTRIBUTIONS VOLONTAIRES EN NATURE,,,
870,Dons en nature,,,
871,Prestations en nature,,,
875,Bénévolat,,,Bénévolat
89,COMPTES DE BILAN,,,
890,Bilan d'ouverture,,,Ouverture
891,Bilan de clôture,,,Clôture
9,Classe 9 — Comptes analytiques,,,
90,COMPTES RÉFLÉCHIS,,,
906,Charges réfléchies,,,
907,Produits réfléchis,,,
99,Projets,,,Analytique

Modified src/include/data/schema.sql from [c8df01c2dd] to [bdddcd8200].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
CREATE TABLE IF NOT EXISTS config (
-- Configuration de Garradin
    cle TEXT PRIMARY KEY NOT NULL,
    valeur TEXT
);

-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
-- compta_categorie_cotisations => id_categorie
-- compta_categorie_dons => id_categorie

CREATE TABLE IF NOT EXISTS membres_categories
-- Catégories de membres
(
    id INTEGER PRIMARY KEY NOT NULL,
    nom TEXT NOT NULL,

    droit_wiki INTEGER NOT NULL DEFAULT 1,
    droit_membres INTEGER NOT NULL DEFAULT 1,
    droit_compta INTEGER NOT NULL DEFAULT 1,
    droit_inscription INTEGER NOT NULL DEFAULT 0,
    droit_connexion INTEGER NOT NULL DEFAULT 1,
    droit_config INTEGER NOT NULL DEFAULT 0,
    cacher INTEGER NOT NULL DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id) ON DELETE SET NULL
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE IF NOT EXISTS membres_sessions
-- Sessions
(
    selecteur TEXT NOT NULL,
    hash TEXT NOT NULL,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    expire INT NOT NULL,

    PRIMARY KEY (selecteur, id_membre)
);

CREATE TABLE IF NOT EXISTS cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY NOT NULL,
    id_categorie_compta INTEGER NULL REFERENCES compta_categories (id) ON DELETE SET NULL, -- NULL si le type n'est pas associé automatiquement à la compta

    intitule TEXT NOT NULL,
    description TEXT NULL,
    montant REAL NOT NULL,

    duree INTEGER NULL, -- En jours
    debut TEXT NULL, -- timestamp
    fin TEXT NULL
);

CREATE TABLE IF NOT EXISTS cotisations_membres
-- Enregistrement des cotisations et activités
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date)
);

CREATE UNIQUE INDEX IF NOT EXISTS cm_unique ON cotisations_membres (id_membre, id_cotisation, date);

CREATE TABLE IF NOT EXISTS membres_operations
-- Liaison des enregistrement des paiements en compta
(
    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_operation INTEGER NOT NULL REFERENCES compta_journal (id) ON DELETE CASCADE,
    id_cotisation INTEGER NULL REFERENCES cotisations_membres (id) ON DELETE SET NULL,

    PRIMARY KEY (id_membre, id_operation)
);

CREATE TABLE IF NOT EXISTS rappels
-- Rappels de devoir renouveller une cotisation
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,

    delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel

    sujet TEXT NOT NULL,
    texte TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS rappels_envoyes
-- Enregistrement des rappels envoyés à qui et quand
(
    id INTEGER NOT NULL PRIMARY KEY,

    id_membre INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE,
    id_cotisation INTEGER NOT NULL REFERENCES cotisations (id) ON DELETE CASCADE,
    id_rappel INTEGER NULL REFERENCES rappels (id) ON DELETE CASCADE,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),

    media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
);

--
-- WIKI
--

CREATE TABLE IF NOT EXISTS wiki_pages
-- Pages du wiki
(
    id INTEGER PRIMARY KEY NOT NULL,
    uri TEXT NOT NULL, -- URI unique (équivalent NomPageWiki)
    titre TEXT NOT NULL,
    date_creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_creation) IS NOT NULL AND datetime(date_creation) = date_creation),
    date_modification TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date_modification) IS NOT NULL AND datetime(date_modification) = date_modification),
    parent INTEGER NOT NULL DEFAULT 0, -- ID de la page parent
    revision INTEGER NOT NULL DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte)
    droit_lecture INTEGER NOT NULL DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe)
    droit_ecriture INTEGER NOT NULL DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe)
);

CREATE UNIQUE INDEX IF NOT EXISTS wiki_uri ON wiki_pages (uri);

CREATE VIRTUAL TABLE IF NOT EXISTS wiki_recherche USING fts4
-- Table dupliquée pour chercher une page
(
    id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire
    titre TEXT NOT NULL,
    contenu TEXT NULL, -- Contenu de la dernière révision
    FOREIGN KEY (id) REFERENCES wiki_pages(id)
);

CREATE TABLE IF NOT EXISTS wiki_revisions
-- Révisions du contenu des pages
(
    id_page INTEGER NOT NULL REFERENCES wiki_pages (id) ON DELETE CASCADE,
    revision INTEGER NULL,

    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL,

    contenu TEXT NOT NULL,
    modification TEXT NULL, -- Description des modifications effectuées
    chiffrement INTEGER NOT NULL DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),

    PRIMARY KEY(id_page, revision)
);

CREATE INDEX IF NOT EXISTS wiki_revisions_id_page ON wiki_revisions (id_page);
CREATE INDEX IF NOT EXISTS wiki_revisions_id_auteur ON wiki_revisions (id_auteur);

-- Triggers pour synchro avec table wiki_pages
CREATE TRIGGER IF NOT EXISTS wiki_recherche_delete AFTER DELETE ON wiki_pages
    BEGIN
        DELETE FROM wiki_recherche WHERE id = old.id;
    END;

CREATE TRIGGER IF NOT EXISTS wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages
    BEGIN
        UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id;
    END;

-- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1
    BEGIN
        UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page;
    END;

-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
CREATE TRIGGER IF NOT EXISTS wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
    BEGIN
        UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
    END;

/*
CREATE TABLE wiki_suivi
-- Suivi des pages
(
    id_membre INTEGER NOT NULL,
    id_page INTEGER NOT NULL,

    PRIMARY KEY (id_membre, id_page),

    FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
    FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire
);
*/

--
-- COMPTA
--

CREATE TABLE IF NOT EXISTS compta_exercices
-- Exercices
(
    id INTEGER NOT NULL PRIMARY KEY,

    libelle TEXT NOT NULL,

    debut TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(debut) IS NOT NULL AND date(debut) = debut),
    fin TEXT NULL DEFAULT NULL CHECK (fin IS NULL OR (date(fin) IS NOT NULL AND date(fin) = fin)),

    cloture INTEGER NOT NULL DEFAULT 0
);


CREATE TABLE IF NOT EXISTS compta_comptes
-- Plan comptable
(
    id TEXT NOT NULL PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
    parent TEXT NOT NULL DEFAULT 0,

    libelle TEXT NOT NULL,

    position INTEGER NOT NULL, -- position actif/passif/charge/produit
    plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
    desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
);

CREATE INDEX IF NOT EXISTS compta_comptes_parent ON compta_comptes (parent);

CREATE TABLE IF NOT EXISTS compta_comptes_bancaires
-- Comptes bancaires
(
    id TEXT NOT NULL PRIMARY KEY,

    banque TEXT NOT NULL,

    iban TEXT NULL,
    bic TEXT NULL,

    FOREIGN KEY(id) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS compta_projets
-- Projets (compta analytique)
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS compta_journal
-- Journal des opérations comptables
(
    id INTEGER PRIMARY KEY NOT NULL,

    libelle TEXT NOT NULL,
    remarques TEXT NULL,
    numero_piece TEXT NULL, -- N° de pièce comptable

    montant REAL NOT NULL,

    date TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date) IS NOT NULL AND date(date) = date),
    moyen_paiement TEXT NULL,
    numero_cheque TEXT NULL,

    compte_debit TEXT NULL, -- N° du compte dans le plan, NULL est utilisé pour une opération qui vient d'un exercice précédent
    compte_credit TEXT NULL, -- N° du compte dans le plan

    id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
    id_auteur INTEGER NULL,
    id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
    id_projet INTEGER NULL,

    FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
    FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
    FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
    FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
    FOREIGN KEY(id_auteur) REFERENCES membres(id) ON DELETE SET NULL,
    FOREIGN KEY(id_categorie) REFERENCES compta_categories(id) ON DELETE SET NULL,
    FOREIGN KEY(id_projet) REFERENCES compta_projets(id) ON DELETE SET NULL
);

CREATE INDEX IF NOT EXISTS compta_operations_exercice ON compta_journal (id_exercice);
CREATE INDEX IF NOT EXISTS compta_operations_date ON compta_journal (date);
CREATE INDEX IF NOT EXISTS compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
CREATE INDEX IF NOT EXISTS compta_operations_auteur ON compta_journal (id_auteur);

CREATE TABLE IF NOT EXISTS compta_moyens_paiement
-- Moyens de paiement
(
    code TEXT NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL
);

--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
INSERT OR IGNORE INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');

CREATE TABLE IF NOT EXISTS compta_categories
-- Catégories pour simplifier le plan comptable
(
    id INTEGER NOT NULL PRIMARY KEY,
    type INTEGER NOT NULL DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)

    intitule TEXT NOT NULL,
    description TEXT NULL,

    compte TEXT NOT NULL, -- Compte affecté par cette catégorie

    FOREIGN KEY(compte) REFERENCES compta_comptes(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    officiel INTEGER NOT NULL DEFAULT 0,
    nom TEXT NOT NULL,
    description TEXT NULL,
    auteur TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,
    menu INTEGER NOT NULL DEFAULT 0,
    menu_condition TEXT NULL,
    config TEXT NULL
);

CREATE TABLE IF NOT EXISTS plugins_signaux
-- Association entre plugins et signaux (hooks)
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS compta_rapprochement
-- Rapprochement entre compta et relevés de comptes
(
    id_operation INTEGER NOT NULL PRIMARY KEY REFERENCES compta_journal (id) ON DELETE CASCADE,
    date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(date) IS NOT NULL AND datetime(date) = date),
    id_auteur INTEGER NULL REFERENCES membres (id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS fichiers
-- Données sur les fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    nom TEXT NOT NULL, -- nom de fichier (par exemple image1234.jpeg)
    type TEXT NULL, -- Type MIME
    image INTEGER NOT NULL DEFAULT 0, -- 1 = image reconnue
    datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(datetime) IS NOT NULL AND datetime(datetime) = datetime), -- Date d'ajout ou mise à jour du fichier
    id_contenu INTEGER NOT NULL REFERENCES fichiers_contenu (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS fichiers_date ON fichiers (datetime);

CREATE TABLE IF NOT EXISTS fichiers_contenu
-- Contenu des fichiers
(
    id INTEGER NOT NULL PRIMARY KEY,
    hash TEXT NOT NULL, -- Hash SHA1 du contenu du fichier
    taille INTEGER NOT NULL, -- Taille en octets
    contenu BLOB NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS fichiers_hash ON fichiers_contenu (hash);

CREATE TABLE IF NOT EXISTS fichiers_membres
-- Associations entre fichiers et membres (photo de profil par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES membres (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_wiki_pages
-- Associations entre fichiers et pages du wiki
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES wiki_pages (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS fichiers_compta_journal
-- Associations entre fichiers et journal de compta (pièce comptable par exemple)
(
    fichier INTEGER NOT NULL REFERENCES fichiers (id),
    id INTEGER NOT NULL REFERENCES compta_journal (id),
    PRIMARY KEY(fichier, id)
);

CREATE TABLE IF NOT EXISTS recherches
-- Recherches enregistrées
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta_journal"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);


CREATE TABLE IF NOT EXISTS compromised_passwords_cache
-- Cache des hash de mots de passe compromis
(
    hash TEXT NOT NULL PRIMARY KEY
);

CREATE TABLE IF NOT EXISTS compromised_passwords_cache_ranges
-- Cache des préfixes de mots de passe compromis
(
    prefix TEXT NOT NULL PRIMARY KEY,
    date INTEGER NOT NULL
);
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<





1





























































































































































































































































































































































































































1.0.0_schema.sql
























































































































































































































































































































































































































Modified src/include/init.php from [d8b3de9018] to [53a38bd077].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46





47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149


150
















151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221




222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
<?php

namespace Garradin;

use KD2\ErrorManager;
use KD2\Security;
use KD2\Form;


error_reporting(-1);

/*
 * Version de Garradin
 */

function garradin_version()
{
    if (defined('Garradin\VERSION'))
    {
        return VERSION;
    }

    $file = __DIR__ . '/../VERSION';

    if (file_exists($file))
    {
        $version = trim(file_get_contents($file));
    }
    else
    {
        $version = 'unknown';
    }

    define('Garradin\VERSION', $version);
    return $version;
}

function garradin_manifest()
{
    $file = __DIR__ . '/../../manifest.uuid';

    if (@file_exists($file))
    {
        return substr(trim(file_get_contents($file)), 0, 10);
    }

    return false;





}

/*
 * Configuration globale
 */

// Configuration externalisée
if (file_exists(__DIR__ . '/../config.local.php'))
{
    require __DIR__ . '/../config.local.php';
}

// Configuration par défaut, si les constantes ne sont pas définies dans config.local.php
// (fallback)
if (!defined('Garradin\ROOT'))
{
    define('Garradin\ROOT', dirname(__DIR__));
}


/**
 * Auto-chargement des dépendances
 */
class Loader
{
    /**
     * Liste des classes déjà chargées
     * @var array
     */
    static protected $loaded = [];

    /**
     * Inclure un fichier de classe depuis le nom de la classe
     * @param  string $classname
     * @return void
     */
    static public function load($classname)
    {
        $classname = ltrim($classname, '\\');

        if (array_key_exists($classname, self::$loaded))
        {
            return true;
        }

        // Plugins
        if (substr($classname, 0, 16) == 'Garradin\\Plugin\\')
        {
            $classname = substr($classname, 16);
            $plugin_name = substr($classname, 0, strpos($classname, '\\'));
            $filename = str_replace('\\', '/', substr($classname, strpos($classname, '\\')+1));

            $path = Plugin::getPath(strtolower($plugin_name)) . '/lib/' . $filename . '.php';
        }
        else
        {
            // PSR-0 autoload
            $filename = str_replace('\\', '/', $classname);
            $path = ROOT . '/include/lib/' . $filename . '.php';
        }

        if (!file_exists($path))
        {
            throw new \Exception('File '.$path.' doesn\'t exists');
        }

        self::$loaded[$classname] = true;

        require $path;
    }
}

\spl_autoload_register(['Garradin\Loader', 'load'], true);

if (!defined('Garradin\DATA_ROOT'))
{
    define('Garradin\DATA_ROOT', ROOT);
}

if (!defined('Garradin\WWW_URI'))
{
    try {
        $uri = \KD2\HTTP::getRootURI(ROOT);
    }
    catch (\UnexpectedValueException $e) {
        echo "<h2>Impossible de détecter le chemin d'accès web de Garradin.</h2>";
        echo '<p><a href="https://fossil.kd2.org/garradin/wikiedit?name=Installation">Consulter l\'aide pour configurer manuellement le chemin d\'accès</a></p>';
        exit;
    }

    if ($uri == '/www/') {
        $uri = '/';
    }
    else {
        readfile(ROOT . '/sous-domaine.html');
        exit;
    }

    define('Garradin\WWW_URI', $uri);
    unset($uri);
}

if (!defined('Garradin\WWW_URL'))


{
















    define('Garradin\WWW_URL', \KD2\HTTP::getScheme() . '://' . \KD2\HTTP::getHost() . WWW_URI);
}

static $default_config = [
    'CACHE_ROOT'            => DATA_ROOT . '/cache',
    'DB_FILE'               => DATA_ROOT . '/association.sqlite',
    'DB_SCHEMA'             => ROOT . '/include/data/schema.sql',
    'PLUGINS_ROOT'          => DATA_ROOT . '/plugins',
    'PREFER_HTTPS'          => false,
    'ALLOW_MODIFIED_IMPORT' => true,
    'PLUGINS_SYSTEM'        => '',
    'SHOW_ERRORS'           => true,
    'MAIL_ERRORS'           => false,
    'ERRORS_REPORT_URL'     => null,
    'ERRORS_ENABLE_LOG_VIEW'=> true,
    'USE_CRON'              => false,
    'ENABLE_XSENDFILE'      => false,
    'SMTP_HOST'             => false,
    'SMTP_USER'             => null,
    'SMTP_PASSWORD'         => null,
    'SMTP_PORT'             => 587,
    'SMTP_SECURITY'         => 'STARTTLS',
    'ADMIN_URL'             => WWW_URL . 'admin/',
    'NTP_SERVER'            => 'fr.pool.ntp.org',
    'ENABLE_AUTOMATIC_BACKUPS' => true,
    'ADMIN_COLOR1'          => '#9c4f15',
    'ADMIN_COLOR2'          => '#d98628',
];

foreach ($default_config as $const => $value)
{
    $const = sprintf('Garradin\\%s', $const);

    if (!defined($const))
    {
        define($const, $value);
    }
}

if (!defined('Garradin\\ADMIN_BACKGROUND_IMAGE')) {
    define('Garradin\\ADMIN_BACKGROUND_IMAGE', ADMIN_URL . 'static/gdin_bg.png');
}

const WEBSITE = 'https://fossil.kd2.org/garradin/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';
const DEFAULT_REPORT_URL = null;

// PHP devrait être assez intelligent pour chopper la TZ système mais nan
// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour 
// éviter le message d'erreur à la con on définit une timezone par défaut
// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
// un .htaccess ou dans config.local.php
if (!ini_get('date.timezone'))
{
    if (($tz = @date_default_timezone_get()) && $tz != 'UTC')
    {
        ini_set('date.timezone', $tz);
    }
    else
    {
        ini_set('date.timezone', 'Europe/Paris');
    }
}

/*
 * Gestion des erreurs et exceptions
 */

class UserException extends \LogicException
{
}





// activer le gestionnaire d'erreurs/exceptions
ErrorManager::enable(SHOW_ERRORS ? ErrorManager::DEVELOPMENT : ErrorManager::PRODUCTION);
ErrorManager::setLogFile(DATA_ROOT . '/error.log');

// activer l'envoi de mails si besoin est
if (MAIL_ERRORS)
{
    ErrorManager::setEmail(MAIL_ERRORS);
}

ErrorManager::setContext([
    'rootDirectory'      => ROOT,
    'garradin_data_root' => DATA_ROOT,
    'garradin_version'   => garradin_version(),
]);

if (ERRORS_REPORT_URL)
{
    ErrorManager::setRemoteReporting(ERRORS_REPORT_URL, true);
}
else
{
    ErrorManager::setRemoteReporting(DEFAULT_REPORT_URL, false);
}

ErrorManager::setProductionErrorTemplate('<!DOCTYPE html><html><head><title>Erreur interne</title>
    <style type="text/css">
    body {font-family: sans-serif; }
    code, p, h1 { max-width: 400px; margin: 1em auto; display: block; }
    code { text-align: right; color: #666; }
    a { color: blue; }
    form { text-align: center; }
    </style></head><body><h1>Erreur interne</h1><p>Désolé mais le serveur a rencontré une erreur interne
    et ne peut répondre à votre requête. Merci de ré-essayer plus tard.</p>
    <p>Si vous suspectez un bug dans Garradin, vous pouvez suivre 
    <a href="http://dev.kd2.org/garradin/Rapporter+un+bug">ces instructions</a>
    pour le rapporter.</p>
    <if(sent)><p>Un-e responsable a été notifié-e et cette erreur sera corrigée dès que possible.</p></if>
    <if(logged)><code>L\'erreur a été enregistrée dans les journaux système (error.log) sous la référence : <b>{$ref}</b></code></if>
    <p><a href="' . WWW_URL . '">&larr; Retour à la page d\'accueil</a></p>
    </body></html>');

ErrorManager::setHtmlHeader('<!DOCTYPE html><meta charset="utf-8" /><style type="text/css">
    body { font-family: sans-serif; } * { margin: 0; padding: 0; }
    u, code b, i, h3 { font-style: normal; font-weight: normal; text-decoration: none; }
    #icn { color: #fff; font-size: 2em; float: right; margin: 1em; padding: 1em; background: #900; border-radius: 50%; }
    section header { background: #fdd; padding: 1em; }
    section article { margin: 1em; }
    section article h3, section article h4 { font-size: 1em; font-family: mono; }
    code { border: 1px dotted #ccc; display: block; }
    code b { margin-right: 1em; color: #999; }
    code u { background: #fcc; display: inline-block; width: 100%; }
    table { border-collapse: collapse; margin: 1em; } td, th { border: 1px solid #ccc; padding: .2em .5em; text-align: left; 
    vertical-align: top; }
    input { padding: .3em; margin: .5em; font-size: 1.2em; cursor: pointer; }
</style>
<pre id="icn"> \__/<br /> (xx)<br />//||\\\\</pre>
<section>
    <article>
    <h1>Une erreur s\'est produite</h1>
    <if(report)><form method="post" action="{$report_url}"><p><input type="hidden" name="report" value="{$report_json}" /><input type="submit" value="Rapporter l\'erreur aux développeur⋅euses de Garradin &rarr;" /></p></form></if>
    </article>
</section>
');

function user_error($e)
{
    if (PHP_SAPI == 'cli')
    {
        echo $e->getMessage();
    }
    else
    {
        $tpl = Template::getInstance();

        $tpl->assign('error', $e->getMessage());
        $tpl->assign('admin_url', ADMIN_URL);
        $tpl->display('error.tpl');
    }

    exit;
}

// Message d'erreur simple pour les erreurs de l'utilisateur
ErrorManager::setCustomExceptionHandler('\Garradin\UserException', '\Garradin\user_error');
ErrorManager::setCustomExceptionHandler('\KD2\MiniSkelMarkupException', '\Garradin\user_error');

// Clé secrète utilisée pour chiffrer les tokens CSRF etc.
if (!defined('Garradin\SECRET_KEY'))
{
    $key = base64_encode(Security::random_bytes(64));
    Install::setLocalConfig('SECRET_KEY', $key);
    define('Garradin\SECRET_KEY', $key);
}

// Intégration du secret pour les tokens
Form::tokenSetSecret(SECRET_KEY);

// Fonctions utilitaires bien utiles d'avoir dans le namespace global de Garradin
function obj_has($obj, $pattern)
{
    return \KD2\Helpers::obj_has($obj, $pattern);
}

function obj_get($src, $pattern, $default = null)
{
    return \KD2\Helpers::obj_get($src, $pattern, $default);
}

/*
 * Vérifications pour enclencher le processus d'installation ou de mise à jour
 */

if (!defined('Garradin\INSTALL_PROCESS') && !defined('Garradin\UPGRADE_PROCESS'))
{
    if (!file_exists(DB_FILE))
    {
        Utils::redirect(ADMIN_URL . 'install.php');
    }

    $config = Config::getInstance();

    if (version_compare($config->getVersion(), garradin_version(), '<'))
    {
        Utils::redirect(ADMIN_URL . 'upgrade.php');
    }
}







>









|
|
|
|

|

|
|
|
|
|
|
|
|

|
|




|

|
|
|
|

|
>
>
>
>
>









|






|


|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<

|
|
|
|
|
|

|
|
|
|
|
|
|
|

|
<
<
<
<
<
<
|
|
<
|
<



|




|
|
|
|
<
<
|
|

|
|
|
|
|
|
|

|
|


|
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|



|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|




|

|
|
|
|


|
|




<


|





|
|
|
|
|
|
|
|









>
>
>
>








|



|
|
|




|

<
|
<
<
<

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|


|
|
|
|
|
|
|
|
|
|
|
|



|
|
|
|



|

|
|
|
|
|
|
|

|
|
|
|

|









|
|
|





<
<
<
<
<
|
<
<
<
<







|
|
|
|

|

|
|
|
|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72


















73





74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91






92
93

94

95
96
97
98
99
100
101
102
103
104
105
106


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

237



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310





311




312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<?php

namespace Garradin;

use KD2\ErrorManager;
use KD2\Security;
use KD2\Form;
use KD2\DB\EntityManager;

error_reporting(-1);

/*
 * Version de Garradin
 */

function garradin_version()
{
	if (defined('Garradin\VERSION'))
	{
		return VERSION;
	}

	$file = __DIR__ . '/../VERSION';

	if (file_exists($file))
	{
		$version = trim(file_get_contents($file));
	}
	else
	{
		$version = 'unknown';
	}

	define('Garradin\VERSION', $version);
	return $version;
}

function garradin_manifest()
{
	$file = __DIR__ . '/../../manifest.uuid';

	if (@file_exists($file))
	{
		return substr(trim(file_get_contents($file)), 0, 10);
	}

	return false;
}

if (!defined('\SQLITE3_OPEN_READWRITE')) {
	echo 'Le module de base de données SQLite3 n\'est pas disponible.' . PHP_EOL;
	exit(1);
}

/*
 * Configuration globale
 */

// Configuration externalisée
if (file_exists(__DIR__ . '/../config.local.php'))
{
	require __DIR__ . '/../config.local.php';
}

// Configuration par défaut, si les constantes ne sont pas définies dans config.local.php
// (fallback)
if (!defined('Garradin\ROOT'))
{
	define('Garradin\ROOT', dirname(__DIR__));
}

\spl_autoload_register(function (string $classname) {


















	$classname = ltrim($classname, '\\');






	// Plugins
	if (substr($classname, 0, 16) == 'Garradin\\Plugin\\')
	{
		$classname = substr($classname, 16);
		$plugin_name = substr($classname, 0, strpos($classname, '\\'));
		$filename = str_replace('\\', '/', substr($classname, strpos($classname, '\\')+1));

		$path = Plugin::getPath(strtolower($plugin_name)) . '/lib/' . $filename . '.php';
	}
	else
	{
		// PSR-0 autoload
		$filename = str_replace('\\', '/', $classname);
		$path = ROOT . '/include/lib/' . $filename . '.php';
	}

	if (file_exists($path)) {






		require_once $path;
	}

}, true);


if (!defined('Garradin\DATA_ROOT'))
{
	define('Garradin\DATA_ROOT', ROOT);
}

if (!defined('Garradin\WWW_URI'))
{
	try {
		$uri = \KD2\HTTP::getRootURI(ROOT);
	}
	catch (\UnexpectedValueException $e) {


		$uri = null;
	}

	if ($uri == '/www/') {
		$uri = '/';
	}
	elseif ($uri !== null) {
		readfile(ROOT . '/sous-domaine.html');
		exit;
	}

	define('Garradin\WWW_URI', $uri);
	unset($uri);
}

if (!defined('Garradin\WWW_URL')) {
	$host = \KD2\HTTP::getHost();
}

if (WWW_URI === null || (!empty($host) && $host == 'host.unknown')) {
	$title = 'Impossible de détecter automatiquement l\'URL du site web.';
	$info = 'Consulter l\'aide pour configurer manuellement l\'URL avec la directive WWW_URL et WWW_URI.';
	$url ='https://fossil.kd2.org/garradin/wiki?name=Installation';

	if (PHP_SAPI == 'cli') {
		printf("\n/!\\ %s\n%s\n-> %s\n\n", $title, $info, $url);
	}
	else {
		printf('<h2 style="color: red">%s</h2><p><a href="%s">%s</a></p>', $title, $url, $info);
	}

	exit(1);
}

if (!defined('Garradin\WWW_URL')) {
	define('Garradin\WWW_URL', \KD2\HTTP::getScheme() . '://' . $host . WWW_URI);
}

static $default_config = [
	'CACHE_ROOT'            => DATA_ROOT . '/cache',
	'DB_FILE'               => DATA_ROOT . '/association.sqlite',
	'DB_SCHEMA'             => ROOT . '/include/data/schema.sql',
	'PLUGINS_ROOT'          => DATA_ROOT . '/plugins',
	'PREFER_HTTPS'          => false,
	'ALLOW_MODIFIED_IMPORT' => true,
	'PLUGINS_SYSTEM'        => '',
	'SHOW_ERRORS'           => true,
	'MAIL_ERRORS'           => false,
	'ERRORS_REPORT_URL'     => null,
	'ENABLE_TECH_DETAILS'   => true,
	'USE_CRON'              => false,
	'ENABLE_XSENDFILE'      => false,
	'SMTP_HOST'             => false,
	'SMTP_USER'             => null,
	'SMTP_PASSWORD'         => null,
	'SMTP_PORT'             => 587,
	'SMTP_SECURITY'         => 'STARTTLS',
	'ADMIN_URL'             => WWW_URL . 'admin/',
	'NTP_SERVER'            => 'fr.pool.ntp.org',
	'ENABLE_AUTOMATIC_BACKUPS' => true,
	'ADMIN_COLOR1'          => '#9c4f15',
	'ADMIN_COLOR2'          => '#d98628',
];

foreach ($default_config as $const => $value)
{
	$const = sprintf('Garradin\\%s', $const);

	if (!defined($const))
	{
		define($const, $value);
	}
}

if (!defined('Garradin\ADMIN_BACKGROUND_IMAGE')) {
	define('Garradin\ADMIN_BACKGROUND_IMAGE', ADMIN_URL . 'static/gdin_bg.png');
}

const WEBSITE = 'https://fossil.kd2.org/garradin/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';


// PHP devrait être assez intelligent pour chopper la TZ système mais nan
// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour
// éviter le message d'erreur à la con on définit une timezone par défaut
// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
// un .htaccess ou dans config.local.php
if (!ini_get('date.timezone'))
{
	if (($tz = @date_default_timezone_get()) && $tz != 'UTC')
	{
		ini_set('date.timezone', $tz);
	}
	else
	{
		ini_set('date.timezone', 'Europe/Paris');
	}
}

/*
 * Gestion des erreurs et exceptions
 */

class UserException extends \LogicException
{
}

class ValidationException extends UserException
{
}

// activer le gestionnaire d'erreurs/exceptions
ErrorManager::enable(SHOW_ERRORS ? ErrorManager::DEVELOPMENT : ErrorManager::PRODUCTION);
ErrorManager::setLogFile(DATA_ROOT . '/error.log');

// activer l'envoi de mails si besoin est
if (MAIL_ERRORS)
{
	ErrorManager::setEmail(MAIL_ERRORS);
}

ErrorManager::setContext([
	'rootDirectory'      => ROOT,
	'garradin_data_root' => DATA_ROOT,
	'garradin_version'   => garradin_version(),
]);

if (ERRORS_REPORT_URL)
{
	ErrorManager::setRemoteReporting(ERRORS_REPORT_URL, true);
}





ErrorManager::setProductionErrorTemplate('<!DOCTYPE html><html><head><title>Erreur interne</title>
	<style type="text/css">
	body {font-family: sans-serif; }
	code, p, h1 { max-width: 400px; margin: 1em auto; display: block; }
	code { text-align: right; color: #666; }
	a { color: blue; }
	form { text-align: center; }
	</style></head><body><h1>Erreur interne</h1><p>Désolé mais le serveur a rencontré une erreur interne
	et ne peut répondre à votre requête. Merci de ré-essayer plus tard.</p>
	<p>Si vous suspectez un bug dans Garradin, vous pouvez suivre 
	<a href="http://dev.kd2.org/garradin/Rapporter+un+bug">ces instructions</a>
	pour le rapporter.</p>
	<if(sent)><p>Un-e responsable a été notifié-e et cette erreur sera corrigée dès que possible.</p></if>
	<if(logged)><code>L\'erreur a été enregistrée dans les journaux système (error.log) sous la référence : <b>{$ref}</b></code></if>
	<p><a href="' . WWW_URL . '">&larr; Retour à la page d\'accueil</a></p>
	</body></html>');

ErrorManager::setHtmlHeader('<!DOCTYPE html><meta charset="utf-8" /><style type="text/css">
	body { font-family: sans-serif; } * { margin: 0; padding: 0; }
	u, code b, i, h3 { font-style: normal; font-weight: normal; text-decoration: none; }
	#icn { color: #fff; font-size: 2em; float: right; margin: 1em; padding: 1em; background: #900; border-radius: 50%; }
	section header { background: #fdd; padding: 1em; }
	section article { margin: 1em; }
	section article h3, section article h4 { font-size: 1em; font-family: mono; }
	code { border: 1px dotted #ccc; display: block; }
	code b { margin-right: 1em; color: #999; }
	code u { background: #fcc; display: inline-block; width: 100%; }
	table { border-collapse: collapse; margin: 1em; } td, th { border: 1px solid #ccc; padding: .2em .5em; text-align: left; 
	vertical-align: top; }
	input { padding: .3em; margin: .5em; font-size: 1.2em; cursor: pointer; }
</style>
<pre id="icn"> \__/<br /> (xx)<br />//||\\\\</pre>
<section>
	<article>
	<h1>Une erreur s\'est produite</h1>
	<if(report)><form method="post" action="{$report_url}"><p><input type="hidden" name="report" value="{$report_json}" /><input type="submit" value="Rapporter l\'erreur aux développeur⋅euses de Garradin &rarr;" /></p></form></if>
	</article>
</section>
');

function user_error(\Exception $e)
{
	if (PHP_SAPI == 'cli')
	{
		echo $e->getMessage();
	}
	else
	{
		$tpl = Template::getInstance();

		$tpl->assign('error', $e->getMessage());
		$tpl->assign('admin_url', ADMIN_URL);
		$tpl->display('error.tpl');
	}

	exit;
}

// Message d'erreur simple pour les erreurs de l'utilisateur
ErrorManager::setCustomExceptionHandler('\Garradin\UserException', '\Garradin\user_error');
ErrorManager::setCustomExceptionHandler('\KD2\MiniSkelMarkupException', '\Garradin\user_error');

// Clé secrète utilisée pour chiffrer les tokens CSRF etc.
if (!defined('Garradin\SECRET_KEY'))
{
	$key = base64_encode(random_bytes(64));
	Install::setLocalConfig('SECRET_KEY', $key);
	define('Garradin\SECRET_KEY', $key);
}

// Intégration du secret pour les tokens
Form::tokenSetSecret(SECRET_KEY);






EntityManager::setGlobalDB(DB::getInstance());





/*
 * Vérifications pour enclencher le processus d'installation ou de mise à jour
 */

if (!defined('Garradin\INSTALL_PROCESS') && !defined('Garradin\UPGRADE_PROCESS'))
{
	if (!file_exists(DB_FILE))
	{
		Utils::redirect(ADMIN_URL . 'install.php');
	}

	$config = Config::getInstance();

	if (version_compare($config->getVersion(), garradin_version(), '<'))
	{
		Utils::redirect(ADMIN_URL . 'upgrade.php');
	}
}

Added src/include/lib/Garradin/Accounting/Accounts.php version [104aa964ca].















































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
<?php

namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Line;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Accounting\Year;
use Garradin\CSV;
use Garradin\DB;
use Garradin\Utils;
use KD2\DB\EntityManager;

class Accounts
{
	protected $chart_id;
	protected $em;

	const EXPECTED_CSV_COLUMNS = ['code', 'label', 'description', 'position', 'type'];

	public function __construct(int $chart_id)
	{
		$this->chart_id = $chart_id;
		$this->em = EntityManager::getInstance(Account::class);
	}

	static public function get(int $id)
	{
		return EntityManager::findOneById(Account::class, $id);
	}

	static public function getSelectorLabel(int $id)
	{
		return EntityManager::getInstance(Account::class)->col('SELECT code || \' — \' || label FROM @TABLE WHERE id = ?;', $id);
	}

	public function getIdFromCode(string $code): int
	{
		return $this->em->col('SELECT id FROM @TABLE WHERE code = ? AND id_chart = ?;', $code, $this->chart_id);
	}

	/**
	 * Return common accounting accounts from current chart
	 * (will not return analytical and volunteering accounts)
	 */
	public function listCommonTypes(): array
	{
		return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? AND type != 0 AND type NOT IN (?, ?) ORDER BY code COLLATE NOCASE;',
			$this->chart_id, Account::TYPE_ANALYTICAL, Account::TYPE_VOLUNTEERING);
	}

	/**
	 * Return all accounts from current chart
	 */
	public function listAll(): array
	{
		return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? ORDER BY code COLLATE NOCASE;',
			$this->chart_id);
	}

	public function listForCodes(array $codes): array
	{
		return DB::getInstance()->getGrouped('SELECT code, id, label FROM acc_accounts WHERE id_chart = ?;', $this->chart_id);
	}

	/**
	 * Return all accounts from current chart
	 */
	public function export(): \Generator
	{
		$res = $this->em->DB()->iterate($this->em->formatQuery('SELECT code, label, description, position, type FROM @TABLE WHERE id_chart = ? ORDER BY code COLLATE NOCASE;'),
			$this->chart_id);

		foreach ($res as $row) {
			$row->type = Account::TYPES_NAMES[$row->type];
			$row->position = Account::POSITIONS_NAMES[$row->position];
			yield $row;
		}
	}

	/**
	 * Return only analytical accounts
	 */
	public function listAnalytical(): array
	{
		return $this->em->DB()->getAssoc($this->em->formatQuery('SELECT id, label FROM @TABLE WHERE id_chart = ? AND type = ? ORDER BY code COLLATE NOCASE;'), $this->chart_id, Account::TYPE_ANALYTICAL);
	}

	/**
	 * Return only analytical accounts
	 */
	public function listVolunteering(): array
	{
		return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? AND type = ? ORDER BY code COLLATE NOCASE;',
			$this->chart_id, Account::TYPE_VOLUNTEERING);
	}

	/**
	 * List common accounts, grouped by type
	 * @return array
	 */
	public function listCommonGrouped(array $types = null): array
	{
		if (null === $types) {
			$types = '';
		}
		else {
			$types = array_map('intval', $types);
			$types = ' AND ' . $this->em->DB()->where('type', $types);
		}

		$out = [];
		$query = $this->em->iterate('SELECT * FROM @TABLE WHERE id_chart = ? AND type != 0 ' . $types . ' ORDER BY type, code COLLATE NOCASE;',
			$this->chart_id);

		foreach ($query as $row) {
			if (!isset($out[$row->type])) {
				$out[$row->type] = (object) [
					'label'    => Account::TYPES_NAMES[$row->type],
					'type'     => $row->type,
					'accounts' => [],
				];
			}

			$out[$row->type]->accounts[] = $row;
		}

		return $out;
	}

	public function getNextCodesForTypes(): array
	{
		$db = DB::getInstance();
		$codes = $db->getAssoc(sprintf('SELECT type, MAX(code) FROM %s WHERE id_chart = ? AND type > 0 GROUP BY type;', Account::TABLE), $this->chart_id);

		foreach ($codes as &$code) {
			if (($letter = substr($code, -1)) && !is_numeric($letter)) {
				$code = substr($code, 0, -1);
				$letter = strtoupper($letter);
				$letter = ($letter == 'Z') ? 'AA' : chr(ord($letter)+1);
			}
			else {
				$letter = 'A';
			}

			$code = $code . $letter;
		}

		unset($code);
		return $codes;
	}

	public function copyFrom(int $id)
	{
		$db = DB::getInstance();
		return $db->exec(sprintf('INSERT INTO %s (id_chart, code, label, description, position, type, user)
			SELECT %d, code, label, description, position, type, user FROM %1$s WHERE id_chart = %d;', Account::TABLE, $this->chart_id, $id));
	}

	public function importUpload(array $file)
	{
		if (empty($file['size']) || empty($file['tmp_name'])) {
			throw new UserException('Fichier invalide');
		}

		self::importCSV($file['tmp_name']);
	}

	public function importCSV(string $file): void
	{
		$db = DB::getInstance();
		$positions = array_flip(Account::POSITIONS_NAMES);
		$types = array_flip(Account::TYPES_NAMES);

		$db->begin();

		try {
			foreach (CSV::import($file, self::EXPECTED_CSV_COLUMNS) as $line => $row) {
				$account = new Account;
				$account->id_chart = $this->chart_id;
				try {
					$row['position'] = $positions[$row['position']];
					$row['type'] = $types[$row['type']];
					$account->importForm($row);
					$account->save();
				}
				catch (ValidationException $e) {
					throw new UserException(sprintf('Ligne %d : %s', $line, $e->getMessage()));
				}
			}

			$db->commit();
		}
		catch (\Exception $e) {
			$db->rollback();
			throw $e;
		}
	}

	public function countByType(int $type)
	{
		return DB::getInstance()->count(Account::TABLE, 'id_chart = ? AND type = ?', $this->chart_id, $type);
	}

	public function getSingleAccountForType(int $type)
	{
		return DB::getInstance()->first('SELECT * FROM acc_accounts WHERE type = ? AND id_chart = ? LIMIT 1;', $type, $this->chart_id);
	}

/* FIXME: implement closing of accounts
	public function getClosingAccountId()
	{
		return DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE type = ? AND id_chart = ?;', Account::TYPE_CLOSING, $this->chart_id);
	}

	public function closeRevenueExpenseAccounts(Year $year, int $user_id)
	{
		$closing_id = $this->getClosingAccountId();

		if (!$closing_id) {
			throw new UserException('Aucun compte n\'est indiqué comme compte de clôture dans le plan comptable');
		}

		$transaction = new Transaction;
		$transaction->id_creator = $user_id;
		$transaction->id_year = $year->id();
		$transaction->type = Transaction::TYPE_ADVANCED;
		$transaction->label = 'Clôture de l\'exercice';
		$transaction->date = new \DateTime;
		$debit = 0;
		$credit = 0;

		$sql = 'SELECT a.id, SUM(l.credit - l.debit) AS sum, a.position, a.code
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			INNER JOIN acc_accounts a ON a.id = l.id_account
			WHERE t.id_year = ? AND a.position IN (?, ?)
			GROUP BY a.id
			ORDER BY a.code;';

		$res = DB::getInstance()->iterate($sql, $year->id(), Account::REVENUE, Account::EXPENSE);

		foreach ($res as $row) {
			$reversed = $row->position == Account::ASSET;

			$line = new Line;
			$line->id_account = $row->id;
			$line->credit = $reversed ? abs($row->sum) : 0;
			$line->debit = !$reversed ? abs($row->sum) : 0;
			$transaction->addLine($line);

			if ($reversed) {
				$debit += abs($row->sum);
			}
			else {
				$credit += abs($row->sum);
			}
		}

		if ($debit) {
			$line = new Line;
			$line->id_account = $closing_id;
			$line->credit = 0;
			$line->debit = $debit;
			$transaction->addLine($line);
		}

		if ($credit) {
			$line = new Line;
			$line->id_account = $closing_id;
			$line->credit = $credit;
			$line->debit = 0;
			$transaction->addLine($line);
		}

		$transaction->save();
	}
*/
}

Added src/include/lib/Garradin/Accounting/Charts.php version [23502d0c8f].





























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Chart;
use Garradin\Utils;
use Garradin\DB;
use KD2\DB\EntityManager;

class Charts
{
	static public function get(int $id)
	{
		return EntityManager::findOneById(Chart::class, $id);
	}

	static public function list()
	{
		$em = EntityManager::getInstance(Chart::class);
		return $em->all('SELECT * FROM @TABLE ORDER BY country, label;');
	}

	static public function listAssoc()
	{
		return DB::getInstance()->getAssoc(sprintf('SELECT id, country || \' - \' || label FROM %s ORDER BY country, label;', Chart::TABLE));
	}

	static public function listByCountry()
	{
		$sql = sprintf('SELECT id, country, label FROM %s ORDER BY country, label;', Chart::TABLE);
		$list = DB::getInstance()->getGrouped($sql);
		$out = [];

		foreach ($list as $row) {
			$country = Utils::getCountryName($row->country);

			if (!array_key_exists($country, $out)) {
				$out[$country] = [];
			}

			$out[$country][$row->id] = $row->label;
		}

		return $out;
	}
}

Added src/include/lib/Garradin/Accounting/Graph.php version [a1b33675d1].













































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
<?php

namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Line;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Utils;
use Garradin\Config;
use Garradin\DB;
use const Garradin\ADMIN_COLOR1;
use const Garradin\ADMIN_COLOR2;
use const Garradin\ADMIN_URL;
use KD2\DB\EntityManager;

use KD2\Graphics\SVG\Plot;
use KD2\Graphics\SVG\Plot_Data;

use KD2\Graphics\SVG\Pie;
use KD2\Graphics\SVG\Pie_Data;

class Graph
{
	const URL_LIST = [
		ADMIN_URL . 'acc/reports/graph_plot.php?type=assets&%s' => 'Évolution banques et caisses',
		ADMIN_URL . 'acc/reports/graph_plot.php?type=result&%s' => 'Évolution dépenses et recettes',
		ADMIN_URL . 'acc/reports/graph_plot.php?type=debts&%s' => 'Évolution créances (positif) et dettes (négatif)',
		ADMIN_URL . 'acc/reports/graph_pie.php?type=revenue&%s' => 'Répartition recettes',
		ADMIN_URL . 'acc/reports/graph_pie.php?type=expense&%s' => 'Répartition dépenses',
		ADMIN_URL . 'acc/reports/graph_pie.php?type=assets&%s' => 'Répartition actif',
	];

	const PLOT_TYPES = [
		'assets' => [
			'Total' => ['type' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]],
			'Banques' => ['type' => Account::TYPE_BANK],
			'Caisses' => ['type' => Account::TYPE_CASH],
			'En attente' => ['type' => Account::TYPE_OUTSTANDING],
		],
		'result' => [
			'Recettes' => ['position' => Account::REVENUE],
			'Dépenses' => ['position' => Account::EXPENSE],
		],
		'debts' => [
			'Comptes de tiers' => ['type' => Account::TYPE_THIRD_PARTY],
		],
	];

	const PIE_TYPES = [
		'revenue' => ['position' => Account::REVENUE],
		'expense' => ['position' => Account::EXPENSE],
		'assets' => ['type' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]],
	];

	const WEEKLY_INTERVAL = 604800; // 7 days
	const MONTHLY_INTERVAL = 2635200; // 1 month

	static public function plot(string $type, array $criterias, int $interval = self::WEEKLY_INTERVAL, int $width = 700)
	{
		if (!array_key_exists($type, self::PLOT_TYPES)) {
			throw new \InvalidArgumentException('Unknown type');
		}

		$plot = new Plot($width, 300);

		$lines = self::PLOT_TYPES[$type];
		$data = [];

		foreach ($lines as $label => $line_criterias) {
			$line_criterias = array_merge($criterias, $line_criterias);
			$sums = Reports::getSumsByInterval($line_criterias, $interval);

			if (count($sums) <= 1) {
				continue;
			}

			// Invert sums for banks, cash, etc.
			if ('assets' === $type || 'debts' === $type || ('result' === $type && $line_criterias['position'] == Account::EXPENSE)) {
				$sums = array_map(function ($v) { return $v * -1; }, $sums);
			}

			$sums = array_map(function ($v) { return (int)$v/100; }, $sums);

			$graph = new Plot_Data($sums);
			$graph->title = $label;
			$data[] = $graph;
		}


		if (count($data))
		{
			$labels = [];

			foreach ($data[0]->get() as $k=>$v)
			{
				$date = new \DateTime('@' . ($k * $interval));
				$labels[] = Utils::date_fr($date, 'M y');
			}

			$plot->setLabels($labels);

			$i = 0;
			$colors = self::getColors();

			foreach ($data as $line)
			{
				$line->color = $colors[$i++];
				$line->width = 3;
				$plot->add($line);

				if ($i >= count($colors))
					$i = 0;
			}
		}

		return $plot->output();
	}

	static public function pie(string $type, array $criterias)
	{
		if (!array_key_exists($type, self::PIE_TYPES)) {
			throw new \InvalidArgumentException('Unknown type');
		}

		$pie = new Pie(700, 300);

		$pie_criterias = self::PIE_TYPES[$type];
		$data = Reports::getClosingSumsWithAccounts(array_merge($criterias, $pie_criterias), 'ABS(sum) DESC');

		$others = 0;
		$colors = self::getColors();
		$max = count($colors);
		$total = 0;
		$count = 0;
		$i = 0;

		foreach ($data as $row) {
			$row->sum = abs($row->sum);
			$total += $row->sum;
		}

		foreach ($data as $row)
		{
			if ($i++ >= $max || $count > $total*0.95)
			{
				$others += $row->sum;
			}
			else
			{
				$label = strlen($row->label) > 40 ? substr($row->label, 0, 38) . '…' : $row->label;
				$pie->add(new Pie_Data(abs($row->sum) / 100, $label, $colors[$i-1]));
			}

			$count += $row->sum;
		}

		if ($others != 0)
		{
			$pie->add(new Pie_Data(abs($others) / 100, 'Autres', '#ccc'));
		}

		$pie->togglePercentage(true);

		return $pie->output();
	}

	static protected function getColors()
	{
		$config = Config::getInstance();
		$c1 = $config->get('couleur1') ?: ADMIN_COLOR1;
		$c2 = $config->get('couleur2') ?: ADMIN_COLOR2;
		list($h, $s, $v) = Utils::rgbToHsv($c1);
		list($h1, $s, $v) = Utils::rgbToHsv($c2);

		$colors = [];

		for ($i = 0; $i < 6; $i++) {
			if ($i % 2 == 0) {
				$s = $v = 50;
				$h =& $h1;
			}
			else {
				$s = $v = 70;
				$h =& $h2;
			}

			$colors[] = sprintf('hsl(%d, %d%%, %d%%)', $h, $s, $v);

			$h += 30;

			if ($h > 360) {
				$h -= 360;
			}
		}

		return $colors;
	}
}

Added src/include/lib/Garradin/Accounting/Reports.php version [bd1f8f1a69].































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
<?php

namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Line;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Utils;
use Garradin\DB;
use KD2\DB\EntityManager;

class Reports
{
	static public function getWhereClause(array $criterias): string
	{
		$where = [];

		if (!empty($criterias['year'])) {
			$where[] = sprintf('t.id_year = %d', $criterias['year']);
		}

		if (!empty($criterias['position'])) {
			$db = DB::getInstance();
			$where[] = $db->where('position', $criterias['position']);
		}

		if (!empty($criterias['exclude_position'])) {
			$db = DB::getInstance();
			$where[] = $db->where('position', 'NOT IN', $criterias['exclude_position']);
		}

		if (!empty($criterias['type'])) {
			$db = DB::getInstance();
			$criterias['type'] = array_map('intval', (array)$criterias['type']);
			$where[] = sprintf('a.type IN (%s)', implode(',', $criterias['type']));
		}

		if (!empty($criterias['user'])) {
			$where[] = sprintf('t.id IN (SELECT id_transaction FROM acc_transactions_users WHERE id_user = %d)', $criterias['user']);
		}

		if (!empty($criterias['creator'])) {
			$where[] = sprintf('t.id_creator = %d', $criterias['creator']);
		}

		if (!empty($criterias['service_user'])) {
			$where[] = sprintf('t.id IN (SELECT tu.id_transaction FROM acc_transactions_users tu WHERE id_service_user = %d)', $criterias['service_user']);
		}

		if (!empty($criterias['analytical'])) {
			$where[] = sprintf('l.id_analytical = %d', $criterias['analytical']);
		}

		if (!count($where)) {
			throw new \LogicException('Unknown criteria');
		}

		return implode(' AND ', $where);
	}

	/**
	 * Return account sums per year or per account
	 * @param  bool $order_year If true will return accounts grouped by year, if false it will return years grouped by account
	 */
	static public function getAnalyticalSums(bool $by_year = false): \Generator
	{
		$sql = 'SELECT a.label AS account_label, a.id AS id_account, y.id AS id_year, y.label AS year_label, y.start_date, y.end_date,
			SUM(l.credit - l.debit) AS sum, SUM(l.credit) AS credit, SUM(l.debit) AS debit
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			INNER JOIN acc_accounts a ON a.id = l.id_analytical
			INNER JOIN acc_years y ON y.id = t.id_year
			GROUP BY %s
			ORDER BY %s;';

		if ($by_year) {
			$group = 'y.id, a.id';
			$order = 'y.start_date DESC, a.label COLLATE NOCASE';
		}
		else {
			$group = 'a.id, y.id';
			$order = 'a.label COLLATE NOCASE, y.id';
		}

		$sql = sprintf($sql, $group, $order);

		$current = null;

		foreach (DB::getInstance()->iterate($sql) as $row) {
			$id = $by_year ? $row->id_year : $row->id_account;

			if (null !== $current && $current->id !== $id) {
				$current->items[] = (object) [
					'label' => 'Total',
					'credit' => $current->credit,
					'debit' => $current->debit,
					'sum' => $current->sum,
					'id_account' => $by_year ? null : $current->id,
					'id_year' => $by_year ? $current->id : null,
				];

				yield $current;
				$current = null;
			}

			if (null === $current) {
				$current = (object) [
					'id' => $by_year ? $row->id_year : $row->id_account,
					'label' => $by_year ? $row->year_label : $row->account_label,
					'credit' => 0,
					'debit' => 0,
					'sum' => 0,
					'items' => []
				];
			}

			$row->label = !$by_year ? $row->year_label : $row->account_label;
			$current->items[] = $row;
			$current->credit += $row->credit;
			$current->debit += $row->debit;
			$current->sum += $row->sum;
		}

		if ($current === null) {
			return;
		}

		$current->items[] = (object) [
			'label' => 'Total',
			'credit' => $current->credit,
			'debit' => $current->debit,
			'sum' => $current->sum,
			'id_account' => $by_year ? null : $row->id_account,
			'id_year' => $by_year ? $row->id_year : null,
		];
		yield $current;
	}

	static public function getSumsByInterval(array $criterias, int $interval)
	{
		$where = self::getWhereClause($criterias);
		$where_interval = !empty($criterias['year']) ? sprintf(' WHERE id_year = %d', $criterias['year']) : '';

		$db = DB::getInstance();

		$sql = sprintf('SELECT
			strftime(\'%%s\', MIN(date)) / %d AS start_interval,
			strftime(\'%%s\', MAX(date)) / %1$d AS end_interval
			FROM acc_transactions %s;',
			$interval, $where_interval);

		extract((array)$db->first($sql));

		$out = array_fill_keys(range($start_interval, $end_interval), 0);

		$sql = sprintf('SELECT strftime(\'%%s\', t.date) / %d AS interval, SUM(l.credit) - SUM(l.debit) AS sum, t.id_year
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			INNER JOIN acc_accounts a ON a.id = l.id_account
			WHERE %s
			GROUP BY %s ORDER BY %3$s;', $interval, $where, isset($criterias['year']) ? 'interval' : 't.id_year, interval');

		$data = $db->getGrouped($sql);
		$sum = 0;
		$year = null;

		foreach ($out as $k => &$v) {
			if (array_key_exists($k, $data)) {
				$row = $data[$k];
				if ($row->id_year != $year) {
					$sum = 0;
					$year = $row->id_year;
				}

				$sum += $data[$k]->sum;
			}

			$v = $sum;
		}

		unset($v);

		return $out;
	}

	static public function getResult(array $criterias): int
	{
		$where = self::getWhereClause($criterias);
		$sql = sprintf('SELECT SUM(l.credit) - SUM(l.debit)
			FROM %s l
			INNER JOIN %s t ON t.id = l.id_transaction
			INNER JOIN %s a ON a.id = l.id_account
			WHERE %s AND a.position = ?;',
			Line::TABLE, Transaction::TABLE, Account::TABLE, $where);

		$db = DB::getInstance();
		$a = $db->firstColumn($sql, Account::REVENUE);
		$b = $db->firstColumn($sql, Account::EXPENSE);

		return (int)$a - abs((int)$b);
	}

	static public function getClosingSumsWithAccounts(array $criterias, ?string $order = null, bool $reverse = false): array
	{
		$where = self::getWhereClause($criterias);

		$order = $order ?: 'a.code COLLATE NOCASE';
		$reverse = $reverse ? '* - 1' : '';

		// Find sums, link them to accounts
		$sql = sprintf('SELECT a.id, a.code, a.label, a.position, SUM(l.credit) AS credit, SUM(l.debit) AS debit,
			SUM(l.credit - l.debit) %s AS sum
			FROM %s l
			INNER JOIN %s t ON t.id = l.id_transaction
			INNER JOIN %s a ON a.id = l.id_account
			WHERE %s
			GROUP BY l.id_account
			HAVING sum != 0
			ORDER BY %s;',
			$reverse, Line::TABLE, Transaction::TABLE, Account::TABLE, $where, $order);
		return DB::getInstance()->getGrouped($sql);
	}

	static public function getBalanceSheet(array $criterias): array
	{
		$out = [
			Account::ASSET => [],
			Account::LIABILITY => [],
			'sums' => [
				Account::ASSET => 0,
				Account::LIABILITY => 0,
			],
		];

		$position_criteria = ['position' => [Account::ASSET, Account::LIABILITY, Account::ASSET_OR_LIABILITY]];
		$list = self::getClosingSumsWithAccounts($criterias + $position_criteria);

		foreach ($list as $row) {
			if ($row->sum == 0) {
				// Ignore empty accounts
				continue;
			}

			$position = $row->position;

			if ($position == Account::ASSET_OR_LIABILITY) {
				$position = $row->sum < 0 ? Account::ASSET : Account::LIABILITY;
				$row->sum = abs($row->sum);
			}
			elseif ($position == Account::ASSET) {
				// reverse number for assets
				$row->sum *= -1;
			}

			$out[$position][] = $row;
		}

		$result = self::getResult($criterias);

		$out[Account::LIABILITY][] = (object) [
			'id' => null,
			'label' => $result > 0 ? 'Résultat de l\'exercice courant (excédent)' : 'Résultat de l\'exercice courant (perte)',
			'sum' => $result,
		];

		// Calculate the total sum for assets and liabilities
		foreach ($out as $position => $rows) {
			if ($position == 'sums') {
				continue;
			}

			$sum = 0;
			foreach ($rows as $row) {
				$sum += $row->sum;
			}

			$out['sums'][$position] = $sum;
		}

		return $out;
	}

	/**
	 * Return list of favorite accounts (accounts with a type), grouped by type, with their current sum
	 * @return \Generator list of accounts grouped by type
	 */
	static public function getClosingSumsFavoriteAccounts(array $criterias): \Generator
	{
		$where = self::getWhereClause($criterias);

		$sql = sprintf('SELECT a.id, a.code, a.label, a.description, a.type,
			SUM(l.credit) - SUM(l.debit) AS sum
			FROM %s a
			INNER JOIN %s t ON t.id = l.id_transaction
			INNER JOIN %s l ON a.id = l.id_account
			WHERE a.type != 0 AND %s
			GROUP BY l.id_account
			ORDER BY a.type, a.code COLLATE NOCASE;', Account::TABLE, Transaction::TABLE, Line::TABLE, $where);

		$group = null;

		foreach (DB::getInstance()->iterate($sql) as $row) {
			if (null !== $group && $row->type !== $group->type) {
				yield $group;
				$group = null;
			}

			if (null === $group) {
				$group = (object) [
					'label'    => Account::TYPES_NAMES[$row->type],
					'type'     => $row->type,
					'accounts' => []
				];
			}

			$reverse = Account::isReversed($row->type) ? -1 : 1;
			$row->sum *= $reverse;

			$group->accounts[] = $row;
		}

		if (null !== $group) {
			yield $group;
		}
	}

	/**
	 * Grand livre
	 */
	static public function getGeneralLedger(array $criterias): \Generator
	{
		$where = self::getWhereClause($criterias);

		$db = DB::getInstance();

		$sql = sprintf('SELECT t.id_year, l.id_account, l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			INNER JOIN acc_accounts a ON a.id = l.id_account
			WHERE %s
			ORDER BY a.code COLLATE NOCASE, t.date, t.id;', $where);

		$account = null;
		$debit = $credit = 0;
		$accounts = null;

		foreach ($db->iterate($sql) as $row) {
			if (null === $accounts) {
				$accounts = $db->getGrouped('SELECT id, code, label FROM acc_accounts WHERE id_chart = (SELECT id_chart FROM acc_years WHERE id = ?);', $row->id_year);
			}

			if (null !== $account && $account->id != $row->id_account) {
				yield $account;
				$account = null;
			}

			if (null === $account) {
				$account = (object) [
					'code'  => $accounts[$row->id_account]->code,
					'label' => $accounts[$row->id_account]->label,
					'id'    => $row->id_account,
					'id_year' => $row->id_year,
					'sum'   => 0,
					'debit' => 0,
					'credit'=> 0,
					'lines' => [],
				];
			}

			$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);

			$account->sum += ($row->credit - $row->debit);
			$account->debit += $row->debit;
			$account->credit += $row->credit;
			$debit += $row->debit;
			$credit += $row->credit;
			$row->running_sum = $account->sum;


			$account->lines[] = $row;
		}

		if (null === $account) {
			return;
		}

		$account->all_debit = $debit;
		$account->all_credit = $credit;

		yield $account;
	}

	static public function getJournal(array $criterias): \Generator
	{
		$where = self::getWhereClause($criterias);

		$sql = sprintf('SELECT t.id_year, l.id_account, l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			WHERE %s ORDER BY t.date, t.id;', $where);

		$transaction = null;
		$accounts = null;
		$db = DB::getInstance();

		foreach ($db->iterate($sql) as $row) {
			if (null === $accounts) {
				$accounts = $db->getGrouped('SELECT id, code, label FROM acc_accounts WHERE id_chart = (SELECT id_chart FROM acc_years WHERE id = ?);', $row->id_year);
			}

			if (null !== $transaction && $transaction->id != $row->id) {
				yield $transaction;
				$transaction = null;
			}

			if (null === $transaction) {
				$transaction = (object) [
					'id'        => $row->id,
					'label'     => $row->label,
					'date'      => \DateTime::createFromFormat('Y-m-d', $row->date),
					'reference' => $row->reference,
					'lines'     => [],
				];
			}

			if (!isset($accounts[$row->id_account])) {
				throw new \LogicException(sprintf('Account #%s not found', $row->id_account));
			}

			$transaction->lines[] = (object) [
				'account_label' => $accounts[$row->id_account]->label,
				'account_code'  => $accounts[$row->id_account]->code,
				'label'         => $row->line_label,
				'reference'     => $row->line_reference,
				'id_account'    => $row->id_account,
				'credit'        => $row->credit,
				'debit'         => $row->debit,
				'id_year'       => $row->id_year,
			];
		}

		if (null === $transaction) {
			return;
		}

		yield $transaction;
	}
}

Added src/include/lib/Garradin/Accounting/Transactions.php version [6561462819].







































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
<?php

namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Line;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Accounting\Year;
use KD2\DB\EntityManager;
use Garradin\CSV;
use Garradin\CSV_Custom;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{
	const EXPECTED_CSV_COLUMNS_SELF = ['id', 'type', 'status', 'label', 'date', 'notes', 'reference',
		'line_id', 'account', 'credit', 'debit', 'line_reference', 'line_label', 'reconciled'];

	const POSSIBLE_CSV_COLUMNS = [
		'id'             => 'Numéro d\'écriture',
		'label'          => 'Libellé',
		'date'           => 'Date',
		'notes'          => 'Notes',
		'reference'      => 'Numéro pièce comptable',
		'p_reference'    => 'Référence paiement',
		'debit_account'  => 'Compte de débit',
		'credit_account' => 'Compte de crédit',
		'amount'         => 'Montant',
	];

	const MANDATORY_CSV_COLUMNS = ['id', 'label', 'date', 'credit_account', 'debit_account', 'amount'];

	static public function get(int $id)
	{
		return EntityManager::findOneById(Transaction::class, $id);
	}

	static public function saveReconciled(\Generator $journal, ?array $checked)
	{
		if (null === $checked) {
			$checked = [];
		}

		$db = DB::getInstance();
		$db->begin();

		// Synchro des trucs cochés
		$st = $db->prepare('UPDATE acc_transactions_lines SET reconciled = :r WHERE id = :id;');

		foreach ($journal as $row)
		{
			if (!isset($row->id_line)) {
				continue;
			}

			$st->bindValue(':id', (int)$row->id_line, \SQLITE3_INTEGER);
			$st->bindValue(':r', !empty($checked[$row->id_line]) ? 1 : 0, \SQLITE3_INTEGER);
			$st->execute();
		}

		$db->commit();
	}

	static public function saveDeposit(Transaction $transaction, \Generator $journal, array $checked)
	{
		$db = DB::getInstance();
		$db->begin();

		try {
			$ids = [];
			foreach ($journal as $row) {
				if (!array_key_exists($row->id, $checked)) {
					continue;
				}

				$ids[] = (int)$row->id;

				$line = new Line;
				$line->importForm([
					'reference'  => $row->line_reference,
					'id_account' => $row->id_account,
				]);
				$line->credit = $row->debit;

				$transaction->addLine($line);
			}

			$transaction->save();
			$ids = implode(',', $ids);
			$db->exec(sprintf('UPDATE acc_transactions SET status = (status | %d) WHERE id IN (%s);', Transaction::STATUS_DEPOSIT, $ids));
			$db->commit();
		}
		catch (\Exception $e) {
			$db->rollback();
			throw $e;
		}
	}

	static public function countForUser(int $user_id): int
	{
		return DB::getInstance()->count('acc_transactions_users', 'id_user = ?', $user_id);
	}

	static public function countForCreator(int $user_id): int
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}

	/**
	 * Return all transactions from year
	 */
	static public function export(int $year_id): \Generator
	{
		$sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference,
			l.id AS line_id, a.code AS account, l.debit AS debit, l.credit AS credit,
			l.reference AS line_reference, l.label AS line_label, l.reconciled,
			a2.code AS analytical
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			INNER JOIN acc_accounts a ON a.id = l.id_account
			LEFT JOIN acc_accounts a2 ON a2.id = l.id_analytical
			WHERE t.id_year = ? ORDER BY t.date, t.id, l.id;';

		$res = DB::getInstance()->iterate($sql, $year_id);

		$previous_id = null;

		foreach ($res as $row) {
			if ($previous_id === $row->id) {
				$row->id = $row->type = $row->status = $row->label = $row->date = $row->notes = $row->reference = null;
			}
			else {
				$row->type = Transaction::TYPES_NAMES[$row->type];

				$status = [];

				foreach (Transaction::STATUS_NAMES as $k => $v) {
					if ($row->status & $k) {
						$status[] = $v;
					}
				}

				$row->status = implode(', ', $status);
				$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);
				$row->date = $row->date->format('d/m/Y');
			}

			$row->credit = Utils::money_format($row->credit, ',', '');
			$row->debit = Utils::money_format($row->debit, ',', '');

			$previous_id = $row->id;
			yield $row;
		}
	}

	static public function importCSV(Year $year, array $file, int $user_id)
	{
		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}

		$db = DB::getInstance();
		$db->begin();

		$accounts = $year->accounts();
		$transaction = null;
		$types = array_flip(Transaction::TYPES_NAMES);

		$l = 0;

		try {
			foreach (CSV::importUpload($file, self::EXPECTED_CSV_COLUMNS_SELF) as $l => $row) {
				$row = (object) $row;

				$has_transaction = !empty($row->id) || !empty($row->type) || !empty($row->status) || !empty($row->label) || !empty($row->date) || !empty($row->notes) || !empty($row->reference);

				if (null !== $transaction && $has_transaction) {
					$transaction->save();
					$transaction = null;
				}

				if (null === $transaction) {
					if (!$has_transaction) {
						throw new UserException('cette ligne n\'est reliée à aucune écriture');
					}

					if ($row->id) {
						$transaction = self::get((int)$row->id);

						if (!$transaction) {
							throw new UserException(sprintf('l\'écriture n°%d est introuvable', $row->id));
						}

						if ($transaction->validated) {
							throw new UserException(sprintf('l\'écriture n°%d est validée et ne peut être modifiée', $row->id));
						}
					}
					else {
						$transaction = new Transaction;
						$transaction->id_creator = $user_id;
						$transaction->id_year = $year->id();
					}

					if (!isset($types[$row->type])) {
						throw new UserException(sprintf('le type "%s" est inconnu', $row->type));
					}

					$transaction->type = $types[$row->type];
					$fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference']));

					$transaction->importForm($fields);
				}

				$id_account = $accounts->getIdFromCode($row->account);

				if (!$id_account) {
					throw new UserException(sprintf('le compte "%s" n\'existe pas dans le plan comptable', $row->account));
				}

				$row->line_id = trim($row->line_id);
				$id_analytical = null;
				$data = [
					'credit'     => $row->credit ?: 0,
					'debit'      => $row->debit ?: 0,
					'id_account' => $id_account,
					'reference'  => $row->line_reference,
					'label'      => $row->line_label,
					'reconciled' => $row->reconciled,
				];

				if (!empty($row->analytical)) {
					$id_analytical = $accounts->getIdFromCode($row->analytical);

					if (!$id_analytical) {
						throw new UserException(sprintf('le compte analytique "%s" n\'existe pas dans le plan comptable', $row->analytical));
					}

					$data['id_analytical'] = $id_analytical;
				}
				elseif (property_exists($row, 'analytical')) {
					$data['id_analytical'] = null;
				}

				if ($row->line_id) {
					$line = $transaction->getLine((int)$row->line_id);

					if (!$line) {
						throw new UserException(sprintf('le numéro de ligne "%s" n\'existe pas dans l\'écriture "%s"', $row->line_id, $transaction->id ?: 'à créer'));
					}
				}
				else {
					$line = new Line;
				}

				$line->importForm($data);

				if (!$row->line_id) {
					$transaction->addLine($line);
				}
			}

			if (null !== $transaction) {
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();
			throw new UserException(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));
		}

		$db->commit();
	}

	static public function importCustom(Year $year, CSV_Custom $csv, int $user_id)
	{
		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}

		$db = DB::getInstance();
		$db->begin();

		$accounts = $year->accounts();
		$l = 0;

		try {
			foreach ($csv->iterate() as $l => $row) {
				if (!isset($row->credit_account, $row->debit_account, $row->amount)) {
					throw new UserException('Une des colonnes compte de crédit, compte de débit ou montant est manquante.');
				}

				if (!empty($row->id)) {
					$transaction = self::get((int)$row->id);

					if (!$transaction) {
						throw new UserException(sprintf('Ligne %d : l\'écriture n°%d est introuvable', $l, $row->id));
					}

					if ($transaction->validated) {
						throw new UserException(sprintf('Ligne %d : l\'écriture n°%d est validée et ne peut être modifiée', $l, $row->id));
					}

					$transaction->resetLines();
				}
				else {
					$transaction = new Transaction;
					$transaction->type = Transaction::TYPE_ADVANCED;
					$transaction->id_creator = $user_id;
					$transaction->id_year = $year->id();
				}

				$fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference']));
				$transaction->importForm($fields);

				$credit_account = $accounts->getIdFromCode($row->credit_account);
				$debit_account = $accounts->getIdFromCode($row->debit_account);

				if (!$credit_account) {
					throw new UserException(sprintf('Compte de crédit "%s" inconnu dans le plan comptable', $row->credit_account));
				}

				if (!$debit_account) {
					throw new UserException(sprintf('Compte de débit "%s" inconnu dans le plan comptable', $row->debit_account));
				}

				$line = new Line;
				$line->importForm([
					'credit'     => $row->amount,
					'debit'      => 0,
					'id_account' => $credit_account,
					'reference'  => isset($row->p_reference) ? $row->p_reference : null,
				]);
				$transaction->addLine($line);

				$line = new Line;
				$line->importForm([
					'credit'     => 0,
					'debit'      => $row->amount,
					'id_account' => $debit_account,
					'reference'  => isset($row->p_reference) ? $row->p_reference : null,
				]);
				$transaction->addLine($line);
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();
			throw new UserException(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));
		}

		$db->commit();
	}

	static public function setAnalytical(?int $id_analytical, array $lines)
	{
		$db = DB::getInstance();

		if (null !== $id_analytical && !$db->test(Account::TABLE, 'type = 7 AND id = ?', $id_analytical)) {
			throw new \InvalidArgumentException('Chosen account ID is not analytical');
		}

		$lines = array_map('intval', $lines);

		return $db->exec(sprintf('UPDATE acc_transactions_lines SET id_analytical = %s WHERE id IN (%s);',
			(int)$id_analytical ?: 'NULL',
			implode(', ', $lines)));
	}

	static public function listByType(int $year_id, int $type)
	{
		$reverse = 1;

		$columns = Account::LIST_COLUMNS;
		unset($columns['line_label'], $columns['sum'], $columns['debit'], $columns['credit']);
		$columns['line_reference']['label'] = 'Réf. paiement';
		$columns['change']['select'] = sprintf('SUM(l.credit) * %d', $reverse);
		$columns['change']['label'] = 'Montant';

		$tables = 'acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			INNER JOIN acc_accounts a ON a.id = l.id_account
			LEFT JOIN acc_accounts b ON b.id = l.id_analytical';
		$conditions = sprintf('t.type = %s AND t.id_year = %d', $type, $year_id);

		$sum = 0;

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', true);
		$list->setCount('COUNT(*)');
		$list->groupBy('t.id');
		$list->setModifier(function (&$row) use (&$sum, $reverse) {
			$row->date = \DateTime::createFromFormat('!Y-m-d', $row->date);
		});
		$list->setExportCallback(function (&$row) {
			$row->change = Utils::money_format($row->change, '.', '', false);
		});

		return $list;
	}
}

Added src/include/lib/Garradin/Accounting/Years.php version [31fc8b31a2].















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php

namespace Garradin\Accounting;

use Garradin\Entities\Accounting\Year;
use Garradin\Utils;
use Garradin\DB;
use KD2\DB\EntityManager;

class Years
{
	static public function get(int $year_id)
	{
		return EntityManager::findOneById(Year::class, $year_id);
	}

	static public function getCurrentOpenYear()
	{
		return EntityManager::findOne(Year::class, 'SELECT * FROM @TABLE WHERE closed = 0 ORDER BY start_date LIMIT 1;');
	}

	static public function listOpen()
	{
		$db = EntityManager::getInstance(Year::class)->DB();
		return $db->get('SELECT *, (SELECT COUNT(*) FROM acc_transactions WHERE id_year = acc_years.id) AS nb_transactions
			FROM acc_years WHERE closed = 0 ORDER BY end_date;');
	}

	static public function listAssoc()
	{
		return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years ORDER BY end_date;');
	}

	static public function listClosed()
	{
		$em = EntityManager::getInstance(Year::class);
		return $em->all('SELECT * FROM @TABLE WHERE closed = 1 ORDER BY end_date;');
	}

	static public function countClosed()
	{
		return DB::getInstance()->count(Year::TABLE, 'closed = 1');
	}

	static public function list(bool $reverse = false)
	{
		$desc = $reverse ? 'DESC' : '';
		$em = EntityManager::getInstance(Year::class);
		return $em->all(sprintf('SELECT * FROM @TABLE ORDER BY end_date %s;', $desc));
	}

	static public function getNewYearDates(): array
	{
		$last_year = EntityManager::findOne(Year::class, 'SELECT * FROM @TABLE ORDER BY end_date DESC LIMIT 1;');

		if ($last_year) {
			$start_date = clone $last_year->start_date;
			$start_date->modify('+1 year');

			$end_date = clone $last_year->end_date;
			$end_date->modify('+1 year');
		}
		else {
			$start_date = new \DateTime;
			$end_date = clone $start_date;
			$end_date->modify('+1 year');
		}

		return [$start_date, $end_date];
	}
}

Added src/include/lib/Garradin/CSV.php version [b79b34dab0].

















































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
<?php

namespace Garradin;

use KD2\Office\Calc\Writer as ODSWriter;

class CSV
{
	static public function readAsArray(string $path)
	{
		if (!file_exists($path) || !is_readable($path))
		{
			throw new \RuntimeException('Fichier inconnu : '.$path);
		}

		$fp = self::open($path);

		if (!$fp)
		{
			return false;
		}

		$delim = self::findDelimiter($fp);
		self::skipBOM($fp);

		$line = 0;
		$out = [];
		$nb_columns = null;

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);
			$line++;

			if (empty($row))
			{
				continue;
			}

			if (null === $nb_columns)
			{
				$nb_columns = count($row);
			}

			if (count($row) != $nb_columns)
			{
				throw new UserException('Erreur sur la ligne ' . $line . ' : incohérence dans le nombre de colonnes avec la première ligne.');
			}

			$out[$line] = $row;
		}

		fclose($fp);

		return $out;
	}

	static public function open(string $file)
	{
		ini_set('auto_detect_line_endings', true);
		return fopen($file, 'r');
	}

	static public function findDelimiter(&$fp)
	{
		$line = '';

		while ($line === '' && !feof($fp))
		{
			$line = fgets($fp, 4096);
		}

		if (strlen($line) >= 4095) {
			throw new UserException('Fichier CSV illisible : la première ligne est trop longue.');
		}

		// Delete the columns content
		$line = preg_replace('/".*?"/', '', $line);

		$delims = [
			';' => substr_count($line, ';'),
			',' => substr_count($line, ','),
			"\t"=> substr_count($line, "\t")
		];

		arsort($delims);
		reset($delims);

		rewind($fp);

		return key($delims);
	}

	static public function skipBOM(&$fp)
	{
		// Skip BOM
		if (fgets($fp, 4) !== chr(0xEF) . chr(0xBB) . chr(0xBF))
		{
			fseek($fp, 0);
		}
	}

	static public function row($row): string
	{
		$row = (array) $row;

		array_walk($row, function (&$field) {
			$field = strtr($field, ['"' => '""', "\r\n" => "\n"]);
		});

		return sprintf("\"%s\"\r\n", implode('","', $row));
	}

	static public function export(string $format, string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void
	{
		if ('csv' == $format) {
			self::toCSV(... array_slice(func_get_args(), 1));
		}
		else {
			self::toODS(... array_slice(func_get_args(), 1));
		}
	}

	static protected function rowToArray($row, ?callable $row_map_callback)
	{
		if (null !== $row_map_callback) {
			call_user_func_array($row_map_callback, [&$row]);
		}

		if (is_object($row) && $row instanceof Entity) {
			$row = $row->asArray();
		}
		elseif (is_object($row)) {
			$row = (array) $row;
		}

		foreach ($row as $key => &$v) {
			if ((is_object($v) && !($v instanceof \DateTimeInterface)) || is_array($v)) {
				throw new \UnexpectedValueException(sprintf('Unexpected value for "%s": %s', $key, gettype($v)));
			}
		}

		return $row;
	}

	static public function toCSV(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void
	{
		header('Content-type: application/csv');
		header(sprintf('Content-Disposition: attachment; filename="%s.csv"', $name));

		$fp = fopen('php://output', 'w');

		if ($header) {
			fputs($fp, self::row($header));
		}

		if ($iterator->valid()) {
			foreach ($iterator as $row) {
				foreach ($row as $key => &$v) {
					if (is_object($v)&& $v instanceof \DateTimeInterface) {
						$v = $v->format('d/m/Y');
					}
				}

				$row = self::rowToArray($row, $row_map_callback);

				if (!$header)
				{
					fputs($fp, self::row(array_keys($row)));
					$header = true;
				}

				fputs($fp, self::row($row));
			}
		}

		fclose($fp);
	}

	static public function toODS(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void
	{
		header('Content-type: application/vnd.oasis.opendocument.spreadsheet');
		header(sprintf('Content-Disposition: attachment; filename="%s.ods"', $name));

		$ods = new ODSWriter;
		$ods->table_name = $name;

		if ($header) {
			$ods->add((array) $header);
		}

		if ($iterator->valid()) {
			foreach ($iterator as $row) {
				$row = self::rowToArray($row, $row_map_callback);

				if (!$header)
				{
					$ods->add(array_keys($row));
					$header = true;
				}

				$ods->add((array) $row);
			}
		}

		$ods->output();
	}

	static public function importUpload(array $file, array $expected_columns): \Generator
	{
		if (empty($file['size']) || empty($file['tmp_name'])) {
			throw new UserException('Fichier invalide');
		}

		return self::import($file['tmp_name'], $expected_columns);
	}

	static public function import(string $file, array $expected_columns): \Generator
	{
		$fp = fopen($file, 'r');

		if (!$fp) {
			throw new UserException('Le fichier ne peut être ouvert');
		}

		// Find the delimiter
		$delim = self::findDelimiter($fp);
		self::skipBOM($fp);

		$line = 1;

		$columns = fgetcsv($fp, 4096, $delim);
		$columns = array_map('trim', $columns);

		// Check for required columns
		foreach ($expected_columns as $column) {
			if (!in_array($column, $columns, true)) {
				throw new UserException(sprintf('La colonne "%s" est absente du fichier importé', $column));
			}
		}

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);
			$line++;

			// Empty line, skip
			if (empty($row)) {
				continue;
			}

			if (count($row) != count($columns))
			{
				throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
			}

			$row = array_combine($columns, $row);

			yield $line => $row;
		}

		fclose($fp);
	}
}

Added src/include/lib/Garradin/CSV_Custom.php version [12dd8c64a5].





















































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
<?php

namespace Garradin;

use KD2\UserSession;

class CSV_Custom
{
	protected $session;
	protected $key;
	protected $csv;
	protected $translation;
	protected $columns;
	protected $mandatory_columns = [];
	protected $skip = 1;

	public function __construct(UserSession $session, string $key)
	{
		$this->session = $session;
		$this->key = $key;
		$this->csv = $this->session->get($this->key);
		$this->translation = $this->session->get($this->key . '_translation') ?: [];
		$this->skip = $this->session->get($this->key . '_skip') ?: 1;
	}

	public function load(array $file)
	{
		if (empty($file['size']) || empty($file['tmp_name'])) {
			throw new UserException('Fichier invalide');
		}

		$csv = CSV::readAsArray($file['tmp_name']);

		if (!count($csv)) {
			throw new UserException('Ce fichier est vide (aucune ligne trouvée).');
		}

		$this->session->set($this->key, $csv);
	}

	public function iterate(): \Generator
	{
		if (empty($this->csv)) {
			throw new \LogicException('No file has been loaded');
		}

		if (!$this->columns || !$this->translation) {
			throw new \LogicException('Missing columns or translation table');
		}

		$default = array_map(function ($a) { return null; }, $this->columns);

		$i = 0;

		foreach ($this->csv as $k => $line) {
			if ($i++ < $this->skip) {
				continue;
			}

			$row = $default;

			foreach ($line as $col => $value) {
				if (!isset($this->translation[$col])) {
					continue;
				}

				$row[$this->translation[$col]] = $value;
			}

			$row = (object) $row;
			yield $k => $row;
		}
	}

	public function getFirstLine(): array
	{
		if (empty($this->csv)) {
			throw new \LogicException('No file has been loaded');
		}

		return reset($this->csv);
	}

	public function getSelectedTable(?array $source = null): array
	{
		if (null === $source && isset($_POST['translation_table'])) {
			$source = $_POST['translation_table'];
		}
		elseif (null === $source) {
			$source = [];
		}

		$selected = $this->getFirstLine();

		foreach ($selected as $k => &$v) {
			if (isset($source[$k])) {
				$v = $source[$k];
			}
			elseif (isset($this->translation[$k])) {
				$v = $this->translation[$k];
			}
			elseif (false !== ($pos = array_search($v, $this->columns, true))) {
				$v = $pos;
			}
			else {
				$v = null;
			}
		}

		return $selected;
	}

	public function getTranslationTable(): array
	{
		return $this->translation;
	}

	public function setTranslationTable(array $table): void
	{
		$translation = [];

		foreach ($table as $csv => $target) {
			if (empty($target)) {
				continue;
			}

			if (!array_key_exists($target, $this->columns)) {
				throw new UserException('Colonne inconnue');
			}

			$translation[(int)$csv] = $target;
		}

		foreach ($this->mandatory_columns as $key) {
			if (!in_array($key, $translation, true)) {
				throw new UserException(sprintf('La colonne "%s" est obligatoire mais n\'a pas été sélectionnée ou n\'existe pas.', $this->columns[$key]));
			}
		}

		$this->translation = $translation;

		$this->session->set($this->key . '_translation', $this->translation);
	}

	public function clear(): void
	{
		$this->session->set($this->key, null);
		$this->session->set($this->key . '_translation', null);
		$this->session->set($this->key . '_skip', null);
	}

	public function loaded(): bool
	{
		return null !== $this->csv;
	}

	public function ready(): bool
	{
		return $this->loaded() && !empty($this->translation);
	}

	public function count(): ?int
	{
		return null !== $this->csv ? count($this->csv) : null;
	}

	public function skip(int $count): void
	{
		$this->skip = $count;
		$this->session->set($this->key . '_skip', $count);
	}

	public function setColumns(array $columns): void
	{
		$this->columns = $columns;
	}

	public function setMandatoryColumns(array $columns): void
	{
		$this->mandatory_columns = $columns;
	}

	public function getColumnsString(): string
	{
		return implode(', ', $this->getColumns());
	}

	public function getMandatoryColumnsString(): string
	{
		return implode(', ', $this->getMandatoryColumns());
	}

	public function getColumns(): array
	{
		return $this->columns;
	}

	public function getMandatoryColumns(): array
	{
		return array_intersect_key($this->columns, $this->mandatory_columns);
	}
}

Deleted src/include/lib/Garradin/Compta/Categories.php version [aacf281b4b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<?php

namespace Garradin\Compta;

use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;

/**
 * Catégories comptables
 */
class Categories
{
    const DEPENSES = -1;
    const RECETTES = 1;
    const AUTRES = 0;

    public function importCategories()
    {
        $db = DB::getInstance();
        $db->import(\Garradin\ROOT . '/include/data/categories_comptables.sql');
    }

    public function add($data)
    {
        $this->_checkFields($data);

        $db = DB::getInstance();

        if (empty($data['compte']) || !trim($data['compte']))
        {
            throw new UserException('Le compte associé ne peut rester vide.');
        }

        $data['compte'] = trim($data['compte']);

        if (!$db->test('compta_comptes', $db->where('id', $data['compte'])))
        {
            throw new UserException('Le compte associé n\'existe pas.');
        }

        if (!isset($data['type']) ||
            ($data['type'] != self::DEPENSES && $data['type'] != self::RECETTES))
        {
            // Catégories "autres" pas possibles pour le moment
            throw new UserException('Type de catégorie inconnu.');
        }

        $db->insert('compta_categories', [
            'intitule'  =>  $data['intitule'],
            'description'=> $data['description'],
            'compte'    =>  $data['compte'],
            'type'      =>  (int)$data['type'],
        ]);

        return $db->lastInsertRowId();
    }

    public function edit($id, $data)
    {
        $this->_checkFields($data);

        $db = DB::getInstance();

        $db->update('compta_categories',
            [
                'intitule'  =>  $data['intitule'],
                'description'=> $data['description'],
            ],
            'id = :id_select',
            ['id_select' => (int) $id]
        );

        return true;
    }

    public function delete($id)
    {
        $db = DB::getInstance();

        $id = (int) $id;

        // Ne pas supprimer une catégorie qui est utilisée !
        if ($db->test('compta_journal', $db->where('id_categorie', $id)))
        {
            throw new UserException('Cette catégorie ne peut être supprimée car des opérations comptables y sont liées.');
        }

        $db->delete('compta_categories', 'id = ?', $id);

        return true;
    }

    public function get($id)
    {
        $db = DB::getInstance();
        return $db->first('SELECT * FROM compta_categories WHERE id = ?;', (int)$id);
    }

    public function getList($type = null)
    {
        $db = DB::getInstance();
        $where = is_null($type) ? '1' : 'cat.type = '.(int)$type;

        $query = sprintf('SELECT cat.id, cat.*, cc.libelle AS compte_libelle
            FROM compta_categories AS cat INNER JOIN compta_comptes AS cc
                ON cc.id = cat.compte
            WHERE %s ORDER BY cat.intitule;', $where);

        return $db->getGrouped($query);
    }

    public function listMoyensPaiement($assoc = false)
    {
        $db = DB::getInstance();

        $query = 'SELECT code, nom FROM compta_moyens_paiement ORDER BY nom COLLATE NOCASE;';

        if ($assoc) {
            return $db->getAssoc($query);
        }
        else {
            return $db->getGrouped($query);
        }
    }

    public function getMoyenPaiement($code)
    {
        $db = DB::getInstance();
        return $db->firstColumn('SELECT nom FROM compta_moyens_paiement WHERE code = ?;', $code);
    }

    protected function _checkFields(&$data)
    {
        if (empty($data['intitule']) || !trim($data['intitule']))
        {
            throw new UserException('L\'intitulé ne peut rester vide.');
        }

        $data['intitule'] = trim($data['intitule']);
        $data['description'] = isset($data['description']) ? trim($data['description']) : '';

        return true;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Comptes.php version [9bf36bdeef].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;

class Comptes
{
    const CAISSE = '530';

    const CHEQUE_A_ENCAISSER = '5112';
    const CARTE_A_ENCAISSER = '5115';

    const PASSIF = 0x01;
    const ACTIF = 0x02;
    const PRODUIT = 0x04;
    const CHARGE = 0x08;

    /**
     * Importe un plan comptable
     * @param  string $source_file Chemin du fichier à importer.
     * @param  boolean $delete_all True active la suppression des tous les anciens comptes (peu importe plan_comptable)
     * @return boolean/array Retourne un array des comptes non-supprimés avec leur raison, s'il y en a. Sinon true.
     *
     * Accepte 0 ou 1 argument : soit un chemin, soit true.
     * Sans arguments : importe le plan par défaut et ne supprime que les comptes
     * plus présent appartenants au plan d'origine (WHERE plan_comptable = 1)
     */
    public function importPlan($source_file = null, $delete_all = false)
    {
        $reset = false;

        if(null == $source_file)
        {
            $reset = true;
            $source_file = \Garradin\ROOT . '/include/data/plan_comptable.json';
        }

        $plan = json_decode(file_get_contents($source_file));

        if(is_null($plan))
        {
            throw new UserException('Le fichier n\'est pas du JSON ou n\'a pas pu être décodé.');
        }

        $db = DB::getInstance();
        $db->begin();
        $ids = [];

        foreach ($plan as $id=>$compte)
        {
            $ids[] = $id;

            if ($db->test('compta_comptes', $db->where('id', $id)))
            {
                $db->update('compta_comptes', [
                    'parent'    =>  $compte->parent,
                    'libelle'   =>  $compte->nom,
                    'position'  =>  $compte->position,
                    'plan_comptable' => $reset || !empty($compte->plan_comptable) ? 1 : 0,
                    'desactive' => !empty($compte->desactive) ? 1 : 0,
                ], $db->where('id', $id));
            }
            else
            {
                $db->insert('compta_comptes', [
                    'id'        =>  $id,
                    'parent'    =>  $compte->parent,
                    'libelle'   =>  $compte->nom,
                    'position'  =>  $compte->position,
                    'plan_comptable' => $reset || !empty($compte->plan_comptable) ? 1 : 0,
                    'desactive' => !empty($compte->desactive) ? 1 : 0,
                ]);
            }
        }

        // Effacer les comptes du plan comptable s'ils ne sont pas utilisés ailleurs
        // et qu'ils ne sont pas dans le nouveau plan comptable qu'on vient d'importer
        $sql = 'DELETE FROM compta_comptes WHERE id NOT IN (
            SELECT id FROM compta_comptes_bancaires
            UNION SELECT compte_credit FROM compta_journal
            UNION SELECT compte_debit FROM compta_journal
            UNION SELECT id FROM compta_categories)
            AND '. $db->where('id', 'NOT IN', $ids);

        // Si on ne fait qu'importer une mise à jour du plan comptable,
        // ne supprimer que les comptes qui n'ont pas été créés par l'usager
        if (!$delete_all) {
            $sql .= ' AND ' . $db->where('plan_comptable', 1);
        }

        $db->commit();

        return true;
    }

    public function exportPlan()
    {
        $name = 'plan_comptable';

        header('Content-type: application/json');
        header(sprintf('Content-Disposition: attachment; filename="%s.json"', $name));

        $liste = $this->listTree(0, true);

        $export = [];

        foreach ($liste as $k => $v)
        {
            $export[$v->id] = [
                'code'           => $v->id,
                'nom'            => $v->libelle,
                'parent'         => $v->parent,
                'position'       => $v->position,
                'plan_comptable' => $v->plan_comptable,
                'desactive'      => $v->desactive,
            ];
        }

        file_put_contents('php://output', json_encode($export, JSON_PRETTY_PRINT));

        return true;
    }

    public function add($data)
    {
        $this->_checkFields($data, true);

        $db = DB::getInstance();

        if (empty($data['id']))
        {
            $new_id = $data['parent'];
            $letters = range('A', 'Z');
            $sub_accounts = $db->getAssoc('SELECT id, id FROM compta_comptes 
                WHERE parent = ? ORDER BY id COLLATE NOCASE ASC;', $data['parent']);

            foreach ($letters as $letter)
            {
                if (!in_array($new_id . $letter, $sub_accounts))
                {
                    $new_id .= $letter;
                    break;
                }
            }

            // On a exaucé le nombre de sous-comptes possibles
            if ($new_id == $data['parent'])
            {
                throw new UserException('Nombre de sous-comptes maximal atteint pour ce compte parent-ci.');
            }
        }
        else
        {
            $new_id = strtoupper($data['id']);

            $parent = false;
            $id = $new_id;

            // Vérification que c'est bien le bon parent !
            // Sinon risque par exemple d'avoir parent = 5 et id = 512A !
            while (!$parent && strlen($id))
            {
                // On enlève un caractère à la fin jusqu'à trouver un compte parent
                $id = substr($id, 0, -1);
                $parent = $db->firstColumn('SELECT id FROM compta_comptes WHERE id = ?;', $id);
            }

            if (!$parent || $parent != $data['parent'])
            {
                throw new UserException('Le compte parent sélectionné est incorrect, par exemple pour créer un compte 512A il faut sélectionner 512 comme compte parent.');
            }
        }

        // Vérification que le compte n'existe pas déjà
        if ($db->test('compta_comptes', 'id = ?', $new_id))
        {
            throw new UserException('Ce numéro de compte existe déjà dans le plan comptable : ' . $new_id);
        }

        if (isset($data['position']))
        {
            $position = (int) $data['position'];
        }
        else
        {
            $position = $db->firstColumn('SELECT position FROM compta_comptes WHERE id = ?;', $data['parent']);
        }

        $db->insert('compta_comptes', [
            'id'        =>  $new_id,
            'libelle'   =>  trim($data['libelle']),
            'parent'    =>  $data['parent'],
            'plan_comptable' => 0,
            'position'  =>  (int)$position,
        ]);

        return $new_id;
    }

    public function edit($id, $data)
    {
        $db = DB::getInstance();

        $id = trim($id);

        // Vérification que l'on peut éditer ce compte
        if ($db->firstColumn('SELECT plan_comptable FROM compta_comptes WHERE id = ?;', $id))
        {
            throw new UserException('Ce compte fait partie du plan comptable et n\'est pas modifiable.');
        }

        if (isset($data['position']) && empty($data['position']))
        {
            throw new UserException('Aucune position du compte n\'a été indiquée.');
        }

        $this->_checkFields($data);

        $update = [
            'libelle'   =>  trim($data['libelle']),
        ];

        if (isset($data['position']))
        {
            $update['position'] = (int) trim($data['position']);
        }

        $db->update('compta_comptes', $update, $db->where('id', $id));

        return true;
    }

    public function delete($id)
    {
        $db = DB::getInstance();

        $id = trim($id);

        // Ne pas supprimer un compte qui est utilisé !
        if ($db->test('compta_journal', 'compte_debit = ? OR compte_credit = ?', $id, $id))
        {
            throw new UserException('Ce compte ne peut être supprimé car des opérations comptables y sont liées.');
        }

        if ($db->test('compta_comptes_bancaires', $db->where('id', $id)))
        {
            throw new UserException('Ce compte ne peut être supprimé car il est lié à un compte bancaire.');
        }

        $db->delete('compta_comptes', $db->where('id', $id));

        return true;
    }

    /**
     * Peut-on supprimer ce compte ? (OUI s'il n'a pas d'écriture liée)
     * @param  string $id Numéro du compte
     * @return boolean TRUE si le compte n'a pas d'écriture liée
     */
    public function canDelete($id)
    {
        $db = DB::getInstance();

        $id = trim($id);

        if ($db->firstColumn('SELECT 1 FROM compta_journal
                WHERE compte_debit = ? OR compte_credit = ? LIMIT 1;', $id, $id))
        {
            return false;
        }

        if ($db->test('compta_categories', $db->where('compte', $id)))
        {
            return false;
        }

        return true;
    }

    /**
     * Peut-on désactiver ce compte ? (OUI s'il n'a pas d'écriture liée dans l'exercice courant)
     * @param  string $id Numéro du compte
     * @return boolean TRUE si le compte n'a pas d'écriture liée dans l'exercice courant
     */
    public function canDisable($id, &$code = 0)
    {
        $db = DB::getInstance();

        $id = trim($id);

        if ($db->firstColumn('SELECT 1 FROM compta_journal
                WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1) 
                AND (compte_debit = ? OR compte_credit = ?) LIMIT 1;', $id, $id))
        {
            $code = 1;
            return false;
        }

        if ($db->test('compta_categories', $db->where('compte', $id)))
        {
            $code = 2;
            return false;
        }

        return true;
    }

    /**
     * Désactiver un compte
     * Le compte ne sera plus utilisable pour les écritures ou les catégories mais restera en base de données
     * @param  string $id Numéro du compte
     * @return boolean TRUE si la désactivation a fonctionné, une exception utilisateur si
     * la désactivation n'est pas possible.
     */
    public function disable($id)
    {
        $db = DB::getInstance();

        $id = trim($id);

        if (!$this->canDisable($id, $code))
        {
            if ($code === 1)
            {
                throw new UserException('Ce compte ne peut être désactivé car des écritures y sont liées sur l\'exercice courant. '
                    . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.');
            }
            else
            {
                throw new UserException('Ce compte ne peut être désactivé car des catégories y sont liées.');
            }
        }

        return $db->update('compta_comptes', ['desactive' => 1], $db->where('id', $id));
    }

    /**
     * Renvoie si un compte existe et n'est pas désactivé
     * @param  string  $id Numéro de compte
     * @return boolean     TRUE si le compte existe et n'est pas désactivé
     */
    public function isActive($id)
    {
        $db = DB::getInstance();
        return $db->test('compta_comptes', $db->where('id', trim($id)) . ' AND ' . $db->where('desactive', '!=', 1));
    }

    public function get($id)
    {
        $db = DB::getInstance();
        return $db->first('SELECT * FROM compta_comptes WHERE id = ?;', trim($id));
    }

    public function getList($parent = 0)
    {
        $db = DB::getInstance();
        return $db->getGrouped('SELECT id, * FROM compta_comptes WHERE parent = ? ORDER BY id;', $parent);
    }

    public function getListAll()
    {
        $db = DB::getInstance();
        return $db->getAssoc('SELECT id, libelle FROM compta_comptes ORDER BY id;');
    }

    public function listTree($parent_id = 0, $include_children = true)
    {
        $db = DB::getInstance();

        if ($include_children && $parent_id)
        {
            $where = $db->where('parent', 'LIKE', $parent_id . '%');
        }
        elseif ($include_children && !$parent_id)
        {
            $where = '1';
        }
        else
        {
            $where = $db->where('parent', !$parent_id ? (int) $parent_id : (string) $parent_id);
        }

        $query = 'SELECT * FROM compta_comptes WHERE %s OR %s ORDER BY id;';
        $query = sprintf($query, $db->where('id', (string) $parent_id), $where);

        return $db->get($query);
    }

    protected function _checkFields(&$data, $force_parent_check = false)
    {
        $db = DB::getInstance();

        if (empty($data['libelle']) || !trim($data['libelle']))
        {
            throw new UserException('Le libellé ne peut rester vide.');
        }

        $data['libelle'] = trim($data['libelle']);

        if (isset($data['id']))
        {
            $force_parent_check = true;
            $data['id'] = trim($data['id']);

            if ($db->test('compta_comptes', $db->where('id', $data['id'])))
            {
                throw new UserException('Le compte numéro '.$data['id'].' existe déjà.');
            }
        }

        if (isset($data['parent']) || $force_parent_check)
        {
            if (empty($data['parent']) && !trim($data['parent']))
            {
                throw new UserException('Le compte ne peut pas ne pas avoir de compte parent.');
            }

            if (!($id = $db->firstColumn('SELECT id FROM compta_comptes WHERE id = ?;', $data['parent'])))
            {
                throw new UserException('Le compte parent indiqué n\'existe pas.');
            }

            $data['parent'] = trim($id);
        }

        if (isset($data['id']))
        {
            if (strncmp($data['id'], $data['parent'], strlen($data['parent'])) !== 0)
            {
                throw new UserException('Le compte '.$data['id'].' n\'est pas un sous-compte de '.$data['parent'].'.');
            }
        }

        return true;
    }

    public function getPositions()
    {
        return [
            self::ACTIF     =>  'Actif',
            self::PASSIF    =>  'Passif',
            self::ACTIF | self::PASSIF      =>  'Actif ou passif (déterminé automatiquement au bilan selon le solde du compte)',
            self::CHARGE    =>  'Charge',
            self::PRODUIT   =>  'Produit',
            self::CHARGE | self::PRODUIT    =>  'Charge et produit',
        ];
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































































































































































































































































































































































































































































































































































































































































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Comptes_Bancaires.php version [c2ddfadd03].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;


class Comptes_Bancaires extends Comptes
{
    const NUMERO_PARENT_COMPTES = 512;

    public function add($data)
    {
        $db = DB::getInstance();

        $data['parent'] = self::NUMERO_PARENT_COMPTES;
        $data['id'] = null;

        $this->_checkBankFields($data);

        $new_id = parent::add($data);

        $db->insert('compta_comptes_bancaires', [
            'id'     =>  $new_id,
            'banque' =>  $data['banque'],
            'iban'   =>  $data['iban'],
            'bic'    =>  $data['bic'],
        ]);

        return $new_id;
    }

    public function edit($id, $data)
    {
        $db = DB::getInstance();
        $id = trim($id);

        if (!$db->test('compta_comptes_bancaires', $db->where('id', $id)))
        {
            throw new UserException('Ce compte n\'est pas un compte bancaire.');
        }

        $this->_checkBankFields($data);
        $result = parent::edit($id, $data);

        if (!$result)
        {
            return $result;
        }

        $db->update('compta_comptes_bancaires', [
            'banque' =>  $data['banque'],
            'iban'   =>  $data['iban'],
            'bic'    =>  $data['bic'],
        ], $db->where('id', $id));

        return true;
    }

    /**
     * Supprime un compte bancaire
     * La suppression sera refusée si le compte est utilisé dans l'exercice en cours
     * ou dans une catégorie.
     * Le compte bancaire sera supprimé et le compte au plan comptable seulement désactivé
     * si le compte est utilisé dans un exercice précédent.
     *
     * La désactivation d'un compte fait qu'il n'est plus utilisable dans l'exercice courant
     * ou les exercices suivants, mais il est possible de le réactiver.
     * @param  string $id  Numéro du compte
     * @return boolean     TRUE si la suppression ou désactivation a été effectuée, une exception ou FALSE sinon
     */
    public function delete($id)
    {
        $db = DB::getInstance();
        $id = trim($id);

        if (!$db->test('compta_comptes_bancaires', $db->where('id', $id)))
        {
            throw new UserException('Ce compte n\'est pas un compte bancaire.');
        }

        // Ne pas supprimer/désactiver un compte qui est utilisé dans l'exercice courant
        if ($db->firstColumn('SELECT 1 FROM compta_journal
                WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1) 
                AND (compte_debit = ? OR compte_credit = ?) LIMIT 1;', $id, $id))
        {
            throw new UserException('Ce compte ne peut être supprimé car des écritures y sont liées sur l\'exercice courant. '
                . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.');
        }

        // Il n'est pas possible de supprimer ou désactiver un compte qui est lié à des catégories
        if ($db->test('compta_categories', $db->where('compte', $id)))
        {
            throw new UserException('Ce compte ne peut être supprimé car des catégories y sont liées. '
                . 'Merci de supprimer ou modifier les catégories liées avant de le supprimer.');
        }

        $db->delete('compta_comptes_bancaires', $db->where('id', $id));

        try {
            $return = parent::delete($id);
        }
        catch (UserException $e) {
            // Impossible de supprimer car des opérations y sont encore liées
            // sur les exercices précédents, alors on le désactive
            $return = parent::disable($id);
        }

        return $return;
    }

    public function get($id)
    {
        $db = DB::getInstance();
        return $db->first('SELECT * FROM compta_comptes AS c
            INNER JOIN compta_comptes_bancaires AS cc
            ON c.id = cc.id
            WHERE c.id = ?;', $id);
    }

    public function getList($parent = false)
    {
        $db = DB::getInstance();
        return $db->getGrouped('SELECT c.id AS id, * FROM compta_comptes AS c
            INNER JOIN compta_comptes_bancaires AS cc ON c.id = cc.id
            WHERE c.parent = ? ORDER BY c.id;', self::NUMERO_PARENT_COMPTES);
    }

    protected function _checkBankFields(&$data)
    {
        if (empty($data['banque']) || !trim($data['banque']))
        {
            throw new UserException('Le nom de la banque ne peut rester vide.');
        }

        if (empty($data['bic']))
        {
            $data['bic'] = '';
        }
        else
        {
            $data['bic'] = trim(strtoupper($data['bic']));
            $data['bic'] = preg_replace('![^\dA-Z]!', '', $data['bic']);

            if (!Utils::checkBIC($data['bic']))
            {
                throw new UserException('Code BIC/SWIFT invalide.');
            }
        }

        if (empty($data['iban']))
        {
            $data['iban'] = '';
        }
        else
        {
            $data['iban'] = trim(strtoupper($data['iban']));
            $data['iban'] = preg_replace('![^\dA-Z]!', '', $data['iban']);

            if (!Utils::checkIBAN($data['iban']))
            {
                throw new UserException('Code IBAN invalide.');
            }
        }

        return true;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Exercices.php version [3bc6667d2a].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;

class Exercices
{
    public function add($data)
    {
        $this->_checkFields($data);

        $db = DB::getInstance();

        if ($db->firstColumn('SELECT 1 FROM compta_exercices WHERE
            (debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin);',
            ['debut' => $data['debut'], 'fin' => $data['fin']]))
        {
            throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.');
        }

        if ($db->firstColumn('SELECT 1 FROM compta_exercices WHERE cloture = 0;'))
        {
            throw new UserException('Il n\'est pas possible de créer un nouvel exercice tant qu\'il existe un exercice non-clôturé.');
        }

        $db->insert('compta_exercices', [
            'libelle'   =>  trim($data['libelle']),
            'debut'     =>  $data['debut'],
            'fin'       =>  $data['fin'],
        ]);

        return $db->lastInsertRowId();
    }

    public function edit($id, $data)
    {
        $db = DB::getInstance();

        $this->_checkFields($data);

        // Evitons que les exercices se croisent
        if ($db->firstColumn('SELECT 1 FROM compta_exercices WHERE id != :id AND
            ((debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin));',
            ['debut' => $data['debut'], 'fin' => $data['fin'], 'id' => (int) $id]))
        {
            throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.');
        }

        // On vérifie qu'on ne va pas mettre des opérations en dehors de tout exercice
        if ($db->firstColumn('SELECT 1 FROM compta_journal WHERE id_exercice = ?
            AND date < ? LIMIT 1;', (int)$id, $data['debut']))
        {
            throw new UserException('Des opérations de cet exercice ont une date antérieure à la date de début de l\'exercice.');
        }

        if ($db->firstColumn('SELECT 1 FROM compta_journal WHERE id_exercice = ?
            AND date > ? LIMIT 1;', (int)$id, $data['fin']))
        {
            throw new UserException('Des opérations de cet exercice ont une date postérieure à la date de fin de l\'exercice.');
        }

        $db->update('compta_exercices', [
            'libelle'   =>  trim($data['libelle']),
            'debut'     =>  $data['debut'],
            'fin'       =>  $data['fin'],
        ], 'id = :id', ['id' => (int)$id]);

        return true;
    }

    /**
     * Clôturer un exercice et en ouvrir un nouveau
     * Le report à nouveau n'est pas effectué automatiquement par cette fonction, voir doReports pour ça.
     * @param  integer  $id     ID de l'exercice à clôturer
     * @param  string   $end    Date de clôture de l'exercice au format Y-m-d
     * @return integer          L'ID du nouvel exercice créé
     */
    public function close($id, $end)
    {
        $db = DB::getInstance();

        if (!Utils::checkDate($end))
        {
            throw new UserException('Date de fin vide ou invalide.');
        }

        $db->begin();

        // Clôture de l'exercice
        $db->update('compta_exercices', [
            'cloture'   =>  1,
            'fin'       =>  $end,
        ], 'id = :id', ['id' => (int)$id]);

        // Date de début du nouvel exercice : lendemain de la clôture du précédent exercice
        $new_begin = Utils::modifyDate($end, '+1 day');

        // Date de fin du nouvel exercice : un an moins un jour après l'ouverture
        $new_end = Utils::modifyDate($new_begin, '+1 year -1 day');

        // Enfin sauf s'il existe déjà des opérations après cette date, auquel cas la date de fin
        // est fixée à la date de la dernière opération, ceci pour ne pas avoir d'opération
        // orpheline d'exercice
        $last = $db->firstColumn('SELECT date FROM compta_journal WHERE id_exercice = ? AND date >= ? ORDER BY date DESC LIMIT 1;', $id, $new_end);
        $new_end = $last ?: $new_end;

        $year_begin = substr($new_begin, 0, 4);
        $year_end = substr($new_end, 0, 4);

        // Nom du nouvel exercice
        if ($year_begin == $year_end) {
            $label = sprintf('Exercice %d', $year_begin);
        }
        else {
            $label = sprintf('Exercice %d-%d', $year_begin, $year_end);
        }

        // Création du nouvel exercice
        $new_id = $this->add([
            'debut'     =>  $new_begin,
            'fin'       =>  $new_end,
            'libelle'   =>  $label,
        ]);

        // Ré-attribution des opérations de l'exercice à clôturer qui ne sont pas dans son
        // intervale au nouvel exercice
        $db->update('compta_journal', ['id_exercice' => $new_id], 'id_exercice = :id AND date >= :date', [
            'id'   => $id,
            'date' => $new_begin,
        ]);

        $db->commit();

        return $new_id;
    }

    /**
     * Créer les reports à nouveau issus de l'exercice $old_id dans le nouvel exercice courant
     * @param  integer $old_id  ID de l'ancien exercice
     * @param  integer $new_id  ID du nouvel exercice
     * @param  string  $date    Date Y-m-d donnée aux opérations créées
     * @return boolean          true si succès
     */
    public function doReports($old_id, $date)
    {
        $db = DB::getInstance();

        $db->begin();

        $report_crediteur = 110;
        $report_debiteur  = 119;

        $comptes = new Comptes;

        if (!$comptes->isActive($report_crediteur))
        {
            throw new UserException('Impossible de faire le report à nouveau : le compte de report créditeur ' . $report_crediteur . ' n\'existe pas ou est désactivé.');
        }
        else if (!$comptes->isActive($report_debiteur))
        {
            throw new UserException('Impossible de faire le report à nouveau : le compte de report débiteur ' . $report_debiteur . ' n\'existe pas ou est désactivé.');
        }

        unset($comptes);

        $this->solderResultat($old_id, $date);

        // Récupérer chacun des comptes de bilan et leurs soldes (uniquement les classes 1 à 5)
        $statement = $db->preparedQuery('SELECT compta_comptes.id AS compte, compta_comptes.position AS position,
            ROUND(COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit = compta_comptes.id AND id_exercice = :id), 0), 2)
            - ROUND(COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit = compta_comptes.id AND id_exercice = :id), 0), 2) AS solde
            FROM compta_comptes 
            INNER JOIN compta_journal ON 
                compta_journal.id_exercice = :id AND (
                    (compta_comptes.id = compta_journal.compte_debit AND CAST(substr(compta_journal.compte_debit, 1, 1) AS INTEGER) <= 5)
                    OR (compta_comptes.id = compta_journal.compte_credit AND CAST(substr(compta_journal.compte_credit, 1, 1) AS INTEGER) <= 5)
                )
            WHERE solde != 0
            GROUP BY compta_comptes.id;', ['id' => $old_id]);

        $diff = 0;
        $journal = new Journal;

        while ($row = $statement->fetchArray(SQLITE3_ASSOC))
        {
            $solde = $row['solde'];

            // Solde du compte à zéro : aucun report à faire
            if (empty($solde))
            {
                continue;
            }

            $compte_debit = $solde < 0 ? 890 : $row['compte'];
            $compte_credit = $solde > 0 ? 890 : $row['compte'];

            $diff += $solde;
            $solde = round(abs($solde), 2);

            // Chaque solde de compte est reporté dans le nouvel exercice
            $journal->add([
                'libelle'       =>  'Report à nouveau',
                'date'          =>  $date,
                'montant'       =>  $solde,
                'compte_debit'  =>  $compte_debit,
                'compte_credit' =>  $compte_credit,
                'remarques'     =>  'Report de solde créé automatiquement à la clôture de l\'exercice précédent',
            ]);
        }

        // FIXME utiliser $diff pour équilibrer

        $db->commit();

        return true;
    }

    /**
     * Solder les comptes de charge et de produits de l'exercice N 
     * et les inscrire au résultat de l'exercice N+1
     * @param  integer  $exercice   ID de l'exercice à solder
     * @param  string   $date       Date de début de l'exercice Y-m-d
     * @return boolean              true en cas de succès
     */
    public function solderResultat($exercice, $date)
    {
        $resultat_excedent = 120;
        $resultat_debiteur = 129;

        $comptes = new Comptes;

        if (!$comptes->isActive($resultat_excedent))
        {
            throw new UserException('Impossible de solder l\'exercice : le compte de résultat excédent ' . $resultat_excedent . ' n\'existe pas ou est désactivé.');
        }
        else if (!$comptes->isActive($resultat_debiteur))
        {
            throw new UserException('Impossible de solder l\'exercice : le compte de résultat débiteur ' . $resultat_debiteur . ' n\'existe pas ou est désactivé.');
        }

        unset($comptes);

        $rapports = new Rapports;
        $resultat = $rapports->compteResultat(['id_exercice' => $exercice], [6, 7]);
        $resultat = $resultat['resultat'];

        if ($resultat != 0)
        {
            $journal = new Journal;
            $journal->add([
                'libelle'   =>  'Résultat de l\'exercice précédent',
                'date'      =>  $date,
                'montant'   =>  abs($resultat),
                'compte_debit'  =>  $resultat < 0 ? $resultat_debiteur : 890,
                'compte_credit' =>  $resultat > 0 ? $resultat_excedent : 890,
            ]);
        }

        return true;
    }

    public function delete($id)
    {
        $db = DB::getInstance();

        // Ne pas supprimer un compte qui est utilisé !
        if ($db->test('compta_journal', $db->where('id_exercice', $id)))
        {
            throw new UserException('Cet exercice ne peut être supprimé car des opérations comptables y sont liées.');
        }

        $db->delete('compta_exercices', 'id = ?', (int)$id);

        return true;
    }

    public function get($id, $with_count = false)
    {
        $with_count = $with_count
            ? ', (SELECT COUNT(*) FROM compta_journal WHERE id_exercice = compta_exercices.id) AS nb_operations'
            : '';

        $db = DB::getInstance();
        return $db->first('SELECT *, strftime(\'%s\', debut) AS debut,
            strftime(\'%s\', fin) AS fin ' . $with_count . '
            FROM compta_exercices WHERE id = ?;', (int)$id);
    }

    public function getCurrent()
    {
        $db = DB::getInstance();
        return $db->first('SELECT *, strftime(\'%s\', debut) AS debut, strftime(\'%s\', fin) AS fin FROM compta_exercices
            WHERE cloture = 0 LIMIT 1;');
    }

    public function getCurrentId()
    {
        $db = DB::getInstance();
        return $db->firstColumn('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;');
    }

    public function getList()
    {
        $db = DB::getInstance();
        return $db->getGrouped('SELECT id, *, strftime(\'%s\', debut) AS debut,
            strftime(\'%s\', fin) AS fin,
            (SELECT COUNT(*) FROM compta_journal WHERE id_exercice = compta_exercices.id) AS nb_operations
            FROM compta_exercices ORDER BY fin DESC;');
    }

    protected function _checkFields(&$data)
    {
        if (empty($data['libelle']) || !trim($data['libelle']))
        {
            throw new UserException('Le libellé ne peut rester vide.');
        }

        $data['libelle'] = trim($data['libelle']);

        if (empty($data['debut']) || !checkdate(substr($data['debut'], 5, 2), substr($data['debut'], 8, 2), substr($data['debut'], 0, 4)))
        {
            throw new UserException('Date de début vide ou invalide.');
        }

        if (empty($data['fin']) || !checkdate(substr($data['fin'], 5, 2), substr($data['fin'], 8, 2), substr($data['fin'], 0, 4)))
        {
            throw new UserException('Date de fin vide ou invalide.');
        }

        return true;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































































































































































































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Import.php version [f437eb905d].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
<?php

namespace Garradin\Compta;

use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Config;

class Import
{
	protected $header = [
		'Numéro mouvement',
		'Date',
		'Type de mouvement',
		'Catégorie',
		'Libellé',
		'Montant',
		'Compte de débit - numéro',
		'Compte de débit - libellé',
		'Compte de crédit - numéro',
		'Compte de crédit - libellé',
		'Moyen de paiement',
		'Numéro de chèque',
		'Numéro de pièce',
		'Remarques',
		'Projet'
	];

	protected function export($exercice)
	{
		return DB::getInstance()->iterate('SELECT
			journal.id,
			strftime(\'%d/%m/%Y\', date) AS date,
			(CASE cat.type WHEN 1 THEN \'Recette\' WHEN -1 THEN \'Dépense\' ELSE \'Autre\' END) AS type,
			(CASE cat.intitule WHEN NULL THEN \'\' ELSE cat.intitule END) AS cat,
			journal.libelle,
			montant,
			compte_debit,
			debit.libelle AS libelle_debit,
			compte_credit,
			credit.libelle AS libelle_credit,
			(CASE moyen_paiement WHEN NULL THEN \'\' ELSE moyen.nom END) AS moyen,
			numero_cheque,
			numero_piece,
			remarques,
			projet.libelle AS projet
			FROM compta_journal AS journal
				LEFT JOIN compta_categories AS cat ON cat.id = journal.id_categorie
				LEFT JOIN compta_comptes AS debit ON debit.id = journal.compte_debit
				LEFT JOIN compta_comptes AS credit ON credit.id = journal.compte_credit
				LEFT JOIN compta_moyens_paiement AS moyen ON moyen.code = journal.moyen_paiement
				LEFT JOIN compta_projets AS projet ON projet.id = journal.id_projet
			WHERE id_exercice = '.(int)$exercice.'
			ORDER BY journal.date;
		');
	}

	protected function exportName()
	{
		return sprintf('Export comptabilité - %s - %s', Config::getInstance()->get('nom_asso'), date('Y-m-d'));
	}

	public function toCSV($exercice)
	{
		return Utils::toCSV($this->exportName(), $this->export($exercice), $this->header);
	}

	public function toODS($exercice)
	{
		return Utils::toODS($this->exportName(), $this->export($exercice), $this->header);
	}

	public function fromCSV($path)
	{
		if (!file_exists($path) || !is_readable($path))
		{
			throw new \RuntimeException('Fichier inconnu : '.$path);
		}

		$fp = fopen($path, 'r');

		if (!$fp)
		{
			return false;
		}

		$db = DB::getInstance();
		$db->begin();
		$cats = new Categories;
		$journal = new Journal;

		$liste_cats = $db->getAssoc('SELECT type || intitule, id FROM compta_categories;');
		// Liste des moyens sous la forme nom -> code
		$liste_moyens = array_flip($cats->listMoyensPaiement(true));
		$liste_moyens = array_change_key_case($liste_moyens, \CASE_LOWER);

		// Liste associative des projets
		$liste_projets = $db->getAssoc('SELECT libelle, id FROM compta_projets;');

		$col = function($column) use (&$row, &$columns)
		{
			if (!isset($columns[$column]))
				return null;

			if (!isset($row[$columns[$column]]))
				return null;

			return $row[$columns[$column]];
		};

		$line = 0;
		$delim = Utils::find_csv_delim($fp);
		Utils::skip_bom($fp);

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);
			$line++;

			if (empty($row))
			{
				continue;
			}

			if ($line === 1)
			{
				if (trim($row[0]) != 'Numéro mouvement')
				{
					throw new UserException('Erreur sur la ligne ' . $line . ' : l\'entête des colonnes est absent ou incorrect.');
				}

				$columns = array_flip($row);

				continue;
			}

			if (count($row) != count($columns))
			{
				$db->rollback();
				throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
			}

			if (trim($row[0]) !== '' && !is_numeric($row[0]))
			{
				$db->rollback();
				throw new UserException('Erreur sur la ligne ' . $line . ' : la première colonne doit être vide ou contenir le numéro unique d\'opération.');
			}

			$id = $col('Numéro mouvement');
			$date = $col('Date');

			if (!preg_match('!^\d{2}/\d{2}/\d{4}$!', $date))
			{
				$db->rollback();
				throw new UserException('Erreur sur la ligne ' . $line . ' : la date n\'est pas au format jj/mm/aaaa.');
			}

			$date = explode('/', $date);
			$date = $date[2] . '-' . $date[1] . '-' . $date[0];

			// En dehors de l'exercice courant
			if ($db->test('compta_exercices', '(? < debut OR ? > fin) AND cloture = 0', $date, $date))
			{
				continue;
			}

			$debit = $col('Compte de débit - numéro');
			$credit = $col('Compte de crédit - numéro');

			$cat = $col('Catégorie');
			$moyen = strtolower($col('Moyen de paiement'));
			$type = $col('Type de mouvement');

			if ('Recette' === $type) {
				$type = 1;
			}
			elseif ('Dépense' === $type) {
				$type = -1;
			}
			else {
				$type = 0;
			}

			// Association du moyen de paiement par nom
			if ($moyen && array_key_exists($moyen, $liste_moyens))
			{
				$moyen = $liste_moyens[$moyen];
			}
			// Sinon on estime que c'est juste le code qui est fourni
			else
			{
				$moyen = substr(strtoupper($moyen), 0, 2);
			}

			// Vérification de l'existence du moyen de paiement
			// s'il n'est pas valide, on ne peut pas avoir de catégorie non plus
			if (!trim($moyen) || !in_array($moyen, $liste_moyens, true))
			{
				$moyen = false;
				$cat = false;
			}

			if ($cat && array_key_exists($type . $cat, $liste_cats)) {
				$cat = $liste_cats[$type . $cat];
			}
			else {
				$cat = $moyen = false;
			}

			$id_projet = null;

			if (!empty($col('Projet'))) {
				if (!array_key_exists($col('Projet'), $liste_projets)) {
					throw new UserException(sprintf('Erreur sur la ligne %d : le projet "%s" est inconnu', $line, $col('Projet')));
				}

				$id_projet = $liste_projets[$col('Projet')];
			}

			$data = [
				'libelle'       =>  $col('Libellé'),
				'montant'       =>  (float) $col('Montant'),
				'date'          =>  $date,
				'compte_credit' =>  $credit,
				'compte_debit'  =>  $debit,
				'numero_piece'  =>  $col('Numéro de pièce'),
				'remarques'     =>  $col('Remarques'),
				'id_projet'     =>  $id_projet,
			];

			if ($cat)
			{
				$data['moyen_paiement']	=	$moyen;
				$data['numero_cheque']	=	$col('Numéro de chèque');
				$data['id_categorie']	=	$cat;
			}

			try {
				if (empty($id))
				{
					$journal->add($data);
				}
				else
				{
					$journal->edit($id, $data);
				}
			}
			catch (UserException $e)
			{
				throw new UserException(sprintf('Ligne %s: %s', $line, $e->getMessage()));
			}
		}

		$db->commit();

		fclose($fp);
		return true;
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































































































































































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Journal.php version [88e9a2c624].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;
use \Garradin\Config;

class Journal
{
    protected function _getCurrentExercice()
    {
        $db = DB::getInstance();
        $id = $db->firstColumn('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;');

        if (!$id)
        {
            throw new UserException('Aucun exercice en cours.');
        }

        return $id;
    }

    public function checkExercice()
    {
        return $this->_getCurrentExercice();
    }

    protected function _checkOpenExercice($id)
    {
        if (is_null($id))
            return true;

        return DB::getInstance()->test('compta_exercices', 'cloture = 0 AND id = ?', (int)$id);
    }

    public function getSolde($id_compte, $inclure_sous_comptes = false, $exercice = null)
    {
        $db = DB::getInstance();
        $exercice = (int) $exercice ?: $this->_getCurrentExercice();
        $compte = $inclure_sous_comptes
            ? 'LIKE \'' . $db->escapeString(trim($id_compte)) . '%\''
            : '= \'' . $db->escapeString(trim($id_compte)) . '\'';

        $debit = 'COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit '.$compte.' AND id_exercice = '.(int)$exercice.'), 0)';
        $credit = 'COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit '.$compte.' AND id_exercice = '.(int)$exercice.'), 0)';

        // L'actif augmente au débit, le passif au crédit
        $position = $db->firstColumn('SELECT position FROM compta_comptes WHERE id = ?;', $id_compte);

        if (($position & Comptes::ACTIF) || ($position & Comptes::CHARGE))
        {
            $query = $debit . ' - ' . $credit;
        }
        else
        {
            $query = $credit . ' - ' . $debit;
        }

        return $db->firstColumn('SELECT ' . $query . ';');
    }

    public function getJournalCompte($compte, $inclure_sous_comptes = false, $exercice = null)
    {
        $db = DB::getInstance();

        $position = $db->firstColumn('SELECT position FROM compta_comptes WHERE id = ?;', $compte);

        $exercice = (int) $exercice ?: $this->_getCurrentExercice();
        $compte = $inclure_sous_comptes
            ? 'LIKE \'' . $db->escapeString(trim($compte)) . '%\''
            : '= \'' . $db->escapeString(trim($compte)) . '\'';

        // L'actif et les charges augmentent au débit, le passif et les produits au crédit
        if (($position & Comptes::ACTIF) || ($position & Comptes::CHARGE))
        {
            $d = '';
            $c = '-';
        }
        else
        {
            $d = '-';
            $c = '';
        }

        $query = 'SELECT *, strftime(\'%s\', date) AS date, 
            (CASE WHEN compte_debit '.$compte.' THEN '.$d.'montant ELSE '.$c.'montant END) AS solde
            FROM compta_journal WHERE (compte_debit '.$compte.' OR compte_credit '.$compte.') 
            AND id_exercice = '.(int)$exercice.'
            ORDER BY date ASC;';

        $result = $db->get($query);
        $solde = 0.0;

        foreach ($result as &$row)
        {
            $solde += $row->solde;
            $row->solde = $solde;
        }

        return $result;
    }

    public function add($data)
    {
        $this->_checkFields($data);

        $db = DB::getInstance();

        $data['id_exercice'] = $this->_getCurrentExercice();

        $db->insert('compta_journal', $data);
        $id = $db->lastInsertRowId();

        return $id;
    }

    public function edit($id, $data)
    {
        $db = DB::getInstance();

        // On ne peut éditer une opération qui n'existe pas
        if (!$db->test('compta_journal', 'id = ?', $id))
        {
            throw new UserException(sprintf('L\'opération n°%s n\'existe pas et ne peut donc être modifiée.', (int)$id));
        }

        // Vérification que l'on peut éditer cette opération
        if (!$this->_checkOpenExercice($db->firstColumn('SELECT id_exercice FROM compta_journal WHERE id = ?;', (int)$id)))
        {
            throw new UserException('Cette opération fait partie d\'un exercice qui a été clôturé.');
        }

        $this->_checkFields($data);

        $db->update('compta_journal', $data, $db->where('id', trim($id)));

        return true;
    }

    public function delete($id)
    {
        $db = DB::getInstance();

        // On ne peut supprimer une opération qui n'existe pas
        if (!$db->test('compta_journal', 'id = ?', $id))
        {
            throw new UserException(sprintf('L\'opération n°%s n\'existe pas et ne peut donc être supprimée.', (int)$id));
        }

        // Vérification que l'on peut éditer cette opération
        if (!$this->_checkOpenExercice($db->firstColumn('SELECT id_exercice FROM compta_journal WHERE id = ?;', (int)$id)))
        {
            throw new UserException('Cette opération fait partie d\'un exercice qui a été clôturé.');
        }

        $db->begin();
        $db->delete('membres_operations', $db->where('id_operation', (int)$id));
        $db->delete('compta_rapprochement', $db->where('id_operation', (int)$id));
        $db->delete('compta_journal', $db->where('id', (int)$id));
        $db->commit();

        return true;
    }

    public function get($id)
    {
        $db = DB::getInstance();
        return $db->first('SELECT *, strftime(\'%s\', date) AS date FROM compta_journal WHERE id = ?;', $id);
    }

    /**
     * Compte le nombre d'écritures liées à un membre
     * @param  integer $id Numéro de membre
     * @return integer     Nombre d'écritures liées
     */
    public function countForMember($id)
    {
        $db = DB::getInstance();
        return $db->count('compta_journal', $db->where('id_auteur', $id));
    }

    /**
     * Lister les écritures liées à un membre
     * @param  integer $id       Identifiant de membre
     * @param  integer $exercice Identifiant d'exercice
     * @return array           Liste des écritures liées
     */
    public function listForMember($id, $exercice)
    {
        $db = DB::getInstance();
        return $db->get('SELECT * FROM compta_journal
            WHERE id_auteur = ? AND id_exercice = ?;', (int)$id, (int)$exercice);
    }

    /**
     * Lister les membres liés à cette écriture
     * @param  integer $id Numéro d'écriture
     * @return array     Liste des membres liés
     */
    public function listRelatedMembers($id)
    {
        $db = DB::getInstance();
        $champ_id = Config::getInstance()->get('champ_identite');

        return $db->get('SELECT mo.id_membre, mo.id_cotisation, m.'.$champ_id.' AS identite
            FROM membres_operations AS mo INNER JOIN membres AS m ON mo.id_membre = m.id
            WHERE mo.id_operation = ?;', (int)$id);
    }

    protected function _checkFields(&$data)
    {
        $db = DB::getInstance();

        if (empty($data['libelle']) || !trim($data['libelle']))
        {
            throw new UserException('Le libellé ne peut rester vide.');
        }

        $data['libelle'] = trim($data['libelle']);

        if (!empty($data['moyen_paiement'])
            && !$db->test('compta_moyens_paiement', $db->where('code', $data['moyen_paiement'])))
        {
            throw new UserException('Moyen de paiement invalide.');
        }

        if (empty($data['date']) || !Utils::checkDate($data['date']))
        {
            throw new UserException('Date vide ou invalide.');
        }

        if (!$db->test('compta_exercices', 'cloture = 0 AND debut <= :date AND fin >= :date;', 
            ['date' => $data['date']]))
        {
            throw new UserException('La date ne correspond pas à l\'exercice en cours.');
        }

        if (empty($data['moyen_paiement']))
        {
            $data['moyen_paiement'] = null;
            $data['numero_cheque'] = null;
        }
        else
        {
            $data['moyen_paiement'] = strtoupper($data['moyen_paiement']);

            if ($data['moyen_paiement'] != 'CH')
            {
                $data['numero_cheque'] = null;
            }

            if (!$db->test('compta_moyens_paiement', $db->where('code', $data['moyen_paiement'])))
            {
                throw new UserException('Moyen de paiement invalide.');
            }
        }

        $data['montant'] = str_replace(',', '.', $data['montant']);
        $data['montant'] = (float)$data['montant'];

        if ($data['montant'] <= 0)
        {
            throw new UserException('Le montant ne peut être égal ou inférieur à zéro.');
        }

        foreach (['remarques', 'numero_piece', 'numero_cheque'] as $champ)
        {
            if (empty($data[$champ]) || !trim($data[$champ]))
            {
                $data[$champ] = '';
            }
            else
            {
                $data[$champ] = trim($data[$champ]);
            }
        }

        if (empty($data['compte_debit']) || !$db->test('compta_comptes', $db->where('id', $data['compte_debit'])))
        {
            throw new UserException('Compte débité inconnu.');
        }

        if (empty($data['compte_credit']) || !$db->test('compta_comptes', $db->where('id', $data['compte_credit'])))
        {
            throw new UserException('Compte crédité inconnu.');
        }

        $data['compte_credit'] = strtoupper(trim($data['compte_credit']));
        $data['compte_debit'] = strtoupper(trim($data['compte_debit']));

        if ($data['compte_credit'] == $data['compte_debit'])
        {
            throw new UserException('Compte crédité identique au compte débité.');
        }

        if (isset($data['id_categorie']))
        {
            if (!$db->test('compta_categories', $db->where('id', (int)$data['id_categorie'])))
            {
                throw new UserException('Catégorie inconnue.');
            }

            $data['id_categorie'] = (int)$data['id_categorie'];
        }
        else
        {
            $data['id_categorie'] = NULL;
        }

        if (isset($data['id_auteur']))
        {
            $data['id_auteur'] = (int)$data['id_auteur'];
        }

        if (empty($data['id_projet']))
        {
            $data['id_projet'] = null;
        }
        elseif (isset($data['id_projet']))
        {
            $data['id_projet'] = (int)$data['id_projet'];

            if (!$db->test('compta_projets', $db->where('id', $data['id_projet'])))
            {
                throw new UserException('Projet inconnu.');
            }
        }

        return true;
    }

    public function getListForCategory($type = null, $cat = null)
    {
        $db = DB::getInstance();
        $exercice = $this->_getCurrentExercice();

        $query = 'SELECT compta_journal.*, strftime(\'%s\', compta_journal.date) AS date ';

        if (is_null($cat) && !is_null($type))
        {
            $query.= ', compta_categories.intitule AS categorie
                FROM compta_journal LEFT JOIN compta_categories
                ON compta_journal.id_categorie = compta_categories.id ';
        }
        else
        {
            $query.= ' FROM compta_journal ';
        }

        $query .= ' WHERE ';

        if (!is_null($cat))
        {
            $query .= 'id_categorie = ' . (int)$cat;
        }
        elseif (is_null($type) && is_null($cat))
        {
            $query .= 'id_categorie IS NULL';
        }
        else
        {
            $query.= 'id_categorie IN (SELECT id FROM compta_categories WHERE type = '.(int)$type.')';
        }

        $query .= ' AND id_exercice = ' . (int)$exercice;
        $query .= ' ORDER BY date;';

        return $db->get($query);
    }

    public function searchSQL($query)
    {
        if (!preg_match('/LIMIT\s+/i', $query))
        {
            $query = preg_replace('/;?\s*$/', '', $query);
            $query .= ' LIMIT 100';
        }

        try {
            return DB::getInstance()->userSelectGet($query);
        }
        catch (\Exception $e) {
            throw new UserException('Erreur dans la requête : ' . $e->getMessage());
        }
    }

    public function schemaSQL()
    {
        $db = DB::getInstance();

        $tables = [
            'journal'   =>  $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'compta_journal\';'),
        ];

        return $tables;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































































































































































































































































































































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Projets.php version [e557763ba3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\UserException;

class Projets
{
    public function getAssocList()
    {
        return DB::getInstance()->getAssoc('SELECT id, libelle FROM compta_projets ORDER BY libelle;');
    }

    public function getList()
    {
        return DB::getInstance()->get('SELECT *, 
            (SELECT COUNT(*) FROM compta_journal WHERE id_projet = compta_projets.id) AS nb_operations
            FROM compta_projets ORDER BY libelle;');
    }

    public function get($id)
    {
        return DB::getInstance()->first('SELECT * FROM compta_projets WHERE id = ?;', (int) $id);
    }

    public function add($libelle)
    {
        if (trim($libelle) == '')
        {
            throw new UserException('Le libellé est obligatoire');
        }

        $db = DB::getInstance();

        $db->insert('compta_projets', ['libelle' => trim($libelle)]);

        return $db->lastInsertRowId();
    }

    public function edit($id, $libelle)
    {
        if (trim($libelle) == '')
        {
            throw new UserException('Le libellé est obligatoire');
        }

        $db = DB::getInstance();

        return $db->update('compta_projets', ['libelle' => trim($libelle)], $db->where('id', (int) $id));
    }

    public function remove($id)
    {
        $db = DB::getInstance();

        return $db->delete('compta_projets', $db->where('id', (int) $id));
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































Deleted src/include/lib/Garradin/Compta/Rapports.php version [9726d24a12].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;

class Rapports
{
    protected function getWhereClause(array $criterias)
    {
        $where = [];
        $db = DB::getInstance();

        foreach ($criterias as $name => $value)
        {
            $where[] = $db->where($name, $value);
        }

        return implode(' AND ', $where);
    }

    public function journal(array $criterias)
    {
        $db = DB::getInstance();
        return $db->get('SELECT *, strftime(\'%s\', date) AS date FROM compta_journal
            WHERE ' . $this->getWhereClause($criterias) . ' ORDER BY date, id;');
    }

    public function grandLivre(array $criterias)
    {
        $db = DB::getInstance();
        $livre = ['classes' => [], 'debit' => 0.0, 'credit' => 0.0];
        $where = $this->getWhereClause($criterias);

        $res = $db->preparedQuery('SELECT compte FROM
            (SELECT compte_debit AS compte FROM compta_journal
                    WHERE ' . $where . ' GROUP BY compte_debit
                UNION
                SELECT compte_credit AS compte FROM compta_journal
                    WHERE ' . $where . ' GROUP BY compte_credit)
            ORDER BY compte ASC;');

        while ($row = $res->fetchArray(SQLITE3_NUM))
        {
            $compte = $row[0];

            if (trim($compte) === '')
            {
                continue;
            }

            $classe = substr($compte, 0, 1);
            $parent = substr($compte, 0, 2);

            if (!array_key_exists($classe, $livre['classes']))
            {
                $livre['classes'][$classe] = [];
            }

            if (!array_key_exists($parent, $livre['classes'][$classe]))
            {
                $livre['classes'][$classe][$parent] = [
                    'total'         =>  0.0,
                    'comptes'       =>  [],
                ];
            }

            $livre['classes'][$classe][$parent]['comptes'][$compte] = ['debit' => 0.0, 'credit' => 0.0, 'journal' => []];

            $livre['classes'][$classe][$parent]['comptes'][$compte]['journal'] = $db->get(
                'SELECT *, strftime(\'%s\', date) AS date FROM (
                    SELECT * FROM compta_journal WHERE compte_debit = :compte AND ' . $where . '
                    UNION
                    SELECT * FROM compta_journal WHERE compte_credit = :compte AND ' . $where . '
                    )
                ORDER BY date, numero_piece, id;', 
                ['compte' => $compte]);

            $solde = 0.0;

            foreach ($livre['classes'][$classe][$parent]['comptes'][$compte]['journal'] as &$ligne)
            {
                if ($ligne->compte_credit == $compte)
                {
                    $solde += $ligne->montant;
                }
                else
                {
                    $solde -= $ligne->montant;
                }

                $ligne->solde = $solde;
            }

            $debit = (float) $db->firstColumn(
                'SELECT SUM(montant) FROM compta_journal WHERE compte_debit = ? AND ' . $where . ';',
                $compte);

            $credit = (float) $db->firstColumn(
                'SELECT SUM(montant) FROM compta_journal WHERE compte_credit = ? AND ' . $where . ';',
                $compte);

            $livre['classes'][$classe][$parent]['comptes'][$compte]['debit'] = $debit;
            $livre['classes'][$classe][$parent]['comptes'][$compte]['credit'] = $credit;
            $livre['classes'][$classe][$parent]['comptes'][$compte]['solde'] = $credit - $debit;

            $livre['classes'][$classe][$parent]['total'] += $debit;
            $livre['classes'][$classe][$parent]['total'] -= $credit;

            $livre['debit'] += $debit;
            $livre['credit'] += $credit;
        }

        $res->finalize();

        return $livre;
    }

    public function compteResultat(array $criterias, array $comptes)
    {
        $db = DB::getInstance();
        $where = $this->getWhereClause($criterias);

        $charges    = ['comptes' => [], 'total' => 0.0];
        $produits   = ['comptes' => [], 'total' => 0.0];
        $resultat   = 0.0;

        $where_comptes = [];

        foreach ($comptes as $compte)
        {
            $where_comptes[] = sprintf('compte LIKE \'%s%%\'', $compte);
        }

        $where_comptes = implode(' OR ', $where_comptes);

        $res = $db->preparedQuery('SELECT compte, SUM(debit), SUM(credit)
            FROM
                (SELECT compte_debit AS compte, SUM(montant) AS debit, 0 AS credit
                    FROM compta_journal WHERE ' . $where . ' GROUP BY compte_debit
                UNION
                SELECT compte_credit AS compte, 0 AS debit, SUM(montant) AS credit
                    FROM compta_journal WHERE ' . $where . ' GROUP BY compte_credit)
            WHERE ' . $where_comptes . '
            GROUP BY compte
            ORDER BY compte ASC;');

        while ($row = $res->fetchArray(SQLITE3_NUM))
        {
            list($compte, $debit, $credit) = $row;
            $classe = substr($compte, 0, 1);
            $sousclasse = substr($compte, 0, 2);
            $parent = substr($compte, 0, 2);

            if ($classe == 6 || $sousclasse == 86)
            {
                if (!isset($charges['comptes'][$parent]))
                {
                    $charges['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0];
                }

                $solde = round($debit - $credit, 2);

                if (empty($solde))
                    continue;

                $charges['comptes'][$parent]['comptes'][$compte] = $solde;
                $charges['total'] += $solde;
                $charges['comptes'][$parent]['solde'] += $solde;
            }
            elseif ($classe == 7 || $sousclasse == 87)
            {
                if (!isset($produits['comptes'][$parent]))
                {
                    $produits['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0];
                }

                $solde = round($credit - $debit, 2);

                if (empty($solde))
                    continue;

                $produits['comptes'][$parent]['comptes'][$compte] = $solde;
                $produits['total'] += $solde;
                $produits['comptes'][$parent]['solde'] += $solde;
            }
        }

        // Suppression des soldes nuls
        $this->removeEmptyAccounts($produits);
        $this->removeEmptyAccounts($charges);

        $res->finalize();

        $resultat = $produits['total'] - $charges['total'];

        return ['charges' => $charges, 'produits' => $produits, 'resultat' => $resultat];
    }

    /**
     * Calculer le bilan comptable 
     * @return array    Un tableau multi-dimensionnel avec deux clés : actif et passif
     */
    public function bilan(array $criterias)
    {
        $db = DB::getInstance();
        $where = $this->getWhereClause($criterias);

        $include = [Comptes::ACTIF, Comptes::PASSIF,
            Comptes::PASSIF | Comptes::ACTIF];

        $actif           = ['comptes' => [], 'total' => 0.0];
        $passif          = ['comptes' => [], 'total' => 0.0];
        $actif_ou_passif = ['comptes' => [], 'total' => 0.0];

        $resultat = $this->compteResultat($criterias, [6, 7]);

        if ($resultat['resultat'] >= 0)
        {
            $passif['comptes']['12'] = [
                'comptes'   =>  ['120' => $resultat['resultat']],
                'solde'     =>  $resultat['resultat']
            ];

            $passif['total'] = $resultat['resultat'];
        }
        else
        {
            $passif['comptes']['12'] = [
                'comptes'   =>  ['129' => $resultat['resultat']],
                'solde'     =>  $resultat['resultat']
            ];

            $passif['total'] = $resultat['resultat'];
        }

        // Y'a sûrement moyen d'améliorer tout ça pour que le maximum de travail
        // soit fait au niveau du SQL, mais pour le moment ça marche
        $res = $db->preparedQuery('SELECT compte, debit, credit, (SELECT position FROM compta_comptes WHERE id = compte) AS position
            FROM
                (SELECT compte_debit AS compte, SUM(montant) AS debit, NULL AS credit
                    FROM compta_journal WHERE ' . $where . ' GROUP BY compte_debit
                UNION
                SELECT compte_credit AS compte, NULL AS debit, SUM(montant) AS credit
                    FROM compta_journal WHERE ' . $where . ' GROUP BY compte_credit)
            WHERE compte IN (SELECT id FROM compta_comptes WHERE position IN ('.implode(', ', $include).'))
            ORDER BY compte ASC;');

        while ($row = $res->fetchArray(SQLITE3_NUM))
        {
            list($compte, $debit, $credit, $position) = $row;
            $parent = substr($compte, 0, 2);

            if (($position & Comptes::ACTIF) && ($position & Comptes::PASSIF))
            {
                $position = 'actif_ou_passif';
                $solde = $debit - $credit;
            }
            else if ($position & Comptes::ACTIF)
            {
                $position = 'actif';
                $solde = $debit - $credit;
            }
            else if ($position & Comptes::PASSIF)
            {
                $position = 'passif';
                $solde = $credit - $debit;
            }
            else
            {
                continue;
            }

            if (!isset(${$position}['comptes'][$parent]))
            {
                ${$position}['comptes'][$parent] = ['comptes' => [], 'solde' => 0];
            }

            if (!isset(${$position}['comptes'][$parent]['comptes'][$compte]))
            {
                ${$position}['comptes'][$parent]['comptes'][$compte] = 0;
            }

            $solde = round($solde, 2);
            ${$position}['comptes'][$parent]['comptes'][$compte] += $solde;
            ${$position}['total'] += $solde;
            ${$position}['comptes'][$parent]['solde'] += $solde;
        }

        $res->finalize();

        foreach ($actif_ou_passif['comptes'] as $parent=>$p)
        {
            foreach ($p['comptes'] as $compte=>$solde)
            {
                if ($solde > 0)
                {
                    $position = 'actif';
                }
                else if ($solde < 0)
                {
                    $position = 'passif';
                    $solde = -$solde;
                }
                else
                {
                    continue;
                }

                if (!isset(${$position}['comptes'][$parent]))
                {
                    ${$position}['comptes'][$parent] = ['comptes' => [], 'solde' => 0];
                }

                if (!isset(${$position}['comptes'][$parent]['comptes'][$compte]))
                {
                    ${$position}['comptes'][$parent]['comptes'][$compte] = 0;
                }

                ${$position}['comptes'][$parent]['comptes'][$compte] += $solde;
                ${$position}['total'] += $solde;
                ${$position}['comptes'][$parent]['solde'] += $solde;
            }
        }

        // Suppression des soldes nuls
        $this->removeEmptyAccounts($passif);
        $this->removeEmptyAccounts($actif);

        return ['actif' => $actif, 'passif' => $passif];
    }

    protected function removeEmptyAccounts(&$source)
    {
        // Suppression des soldes nuls
        foreach ($source['comptes'] as $parent=>$p)
        {
            if ($p['solde'] == 0)
            {
                unset($source['comptes'][$parent]);
                continue;
            }

            foreach ($p['comptes'] as $id=>$solde)
            {
                if ($solde == 0)
                {
                    unset($source['comptes'][$parent]['comptes'][$id]);
                }
            }
        }
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































































































































































































































































































































































































































































































































































































































































































































Deleted src/include/lib/Garradin/Compta/Rapprochement.php version [21098b9dae].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;
use \Garradin\Compta\Journal;
use \Garradin\Compta\Comptes_Bancaires;

class Rapprochement
{
    public function getJournal($compte, $debut, $fin, &$solde_initial, &$solde_final, $sauf_deja_rapprochees = false)
    {
        $db = DB::getInstance();

        $query = 'SELECT 
            COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit = :compte AND compte_credit NOT LIKE \'8%\'  AND date < :date), 0)
            - COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit = :compte AND compte_debit NOT LIKE \'8%\'  AND date < :date), 0)';

        $solde_initial = $solde = $db->firstColumn($query, [
            'compte'    =>  $compte,
            'date'      =>  $debut,
        ]);

        $query = '
            SELECT j.*, strftime(\'%s\', j.date) AS date,
                (CASE WHEN j.compte_debit = :compte THEN j.montant ELSE -(j.montant) END) AS solde,
                r.date AS date_rapprochement
            FROM compta_journal AS j
                LEFT JOIN compta_rapprochement AS r ON r.id_operation = j.id
            WHERE (compte_debit = :compte OR compte_credit = :compte)
                AND j.date >= :debut AND j.date <= :fin
                AND compte_debit NOT LIKE \'8%\' AND compte_credit NOT LIKE \'8%\'
                ' . ($sauf_deja_rapprochees ? 'AND r.id_operation IS NULL' : '') . '
            ORDER BY date ASC;';

        $result = $db->get($query, [
            'compte'    =>  $compte,
            'debut'     =>  $debut,
            'fin'       =>  $fin,
        ]);

        foreach ($result as &$row)
        {
            $solde += $row->solde;
            $row->solde = $solde;
        }

        $solde_final = $solde;

        return $result;
    }

    public function record(array $journal, array $cases = null, $id_auteur)
    {
        if (!is_array($cases) && empty($cases))
        {
            $cases = [];
        }

        $db = DB::getInstance();
        $db->begin();

        // Synchro des trucs cochés
        $st = $db->prepare('INSERT OR REPLACE INTO compta_rapprochement (id_operation, id_auteur) 
            VALUES (:operation, :auteur);');
        $st->bindValue(':auteur', (int)$id_auteur, \SQLITE3_INTEGER);

        foreach ($journal as $row)
        {
            if (!array_key_exists($row->id, $cases))
                continue;

            $st->bindValue(':operation', (int)$row->id, \SQLITE3_INTEGER);
            $st->execute();
        }

        // Synchro des trucs NON cochés
        $st = $db->prepare('DELETE FROM compta_rapprochement WHERE id_operation = :id;');

        foreach ($journal as $row)
        {
            if (array_key_exists($row->id, $cases))
                continue;

            $st->bindValue(':id', (int)$row->id, \SQLITE3_INTEGER);
            $st->execute();
        }

        $db->commit();
        return true;
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































Deleted src/include/lib/Garradin/Compta/Stats.php version [5104b17973].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<?php

namespace Garradin\Compta;

use \Garradin\DB;
use \Garradin\Utils;
use \Garradin\UserException;

class Stats
{
	protected function _parRepartitionCategorie($type)
	{
		return DB::getInstance()->get('SELECT SUM(montant) AS somme, id_categorie
			FROM compta_journal
			WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = ?)
			AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0)
			GROUP BY id_categorie ORDER BY somme DESC;', $type);
	}

	public function repartitionRecettes()
	{
		return $this->_parRepartitionCategorie(Categories::RECETTES);
	}

	public function repartitionDepenses()
	{
		return $this->_parRepartitionCategorie(Categories::DEPENSES);
	}

	protected function _parType($type)
	{
		return $this->getStats('SELECT strftime(\'%Y%m\', date) AS date,
			SUM(montant) FROM compta_journal
			WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = :type)
			AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0)
			GROUP BY strftime(\'%Y-%m\', date) ORDER BY date;',
			['type' => $type]);
	}

	public function recettes()
	{
		return $this->_parType(Categories::RECETTES);
	}

	public function depenses()
	{
		return $this->_parType(Categories::DEPENSES);
	}

	public function soldeCompte($compte, $augmente = 'debit', $diminue = 'credit')
	{
		$db = DB::getInstance();

		if (strpos($compte, '%') !== false)
		{
			$compte = 'LIKE \''. $db->escapeString($compte) . '\'';
		}
		else
		{
			$compte = '= \''. $db->escapeString($compte) . '\'';
		}

		$stats = $this->getStats('SELECT strftime(\'%Y%m\', date) AS date,
			(COALESCE((SELECT SUM(montant) FROM compta_journal
				WHERE compte_'.$augmente.' '.$compte.' AND id_exercice = cj.id_exercice
				AND date >= strftime(\'%Y-%m-01\', cj.date)
				AND date <= strftime(\'%Y-%m-31\', cj.date)), 0)
			- COALESCE((SELECT SUM(montant) FROM compta_journal
				WHERE compte_'.$diminue.' '.$compte.' AND id_exercice = cj.id_exercice
				AND date >= strftime(\'%Y-%m-01\', cj.date)
				AND date <= strftime(\'%Y-%m-31\', cj.date)), 0)
			) AS solde
			FROM compta_journal AS cj
			WHERE (compte_debit '.$compte.' OR compte_credit '.$compte.')
			AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0)
			GROUP BY strftime(\'%Y-%m\', date) ORDER BY date;');

		$c = 0;
		foreach ($stats as $k=>$v)
		{
			$c += $v;
			$stats[$k] = $c;
		}

		return $stats;
	}

	public function getStats($query, Array $args = [])
	{
		$db = DB::getInstance();

		$data = $db->getAssoc($query, $args);

		$e = $db->first('SELECT *, strftime(\'%s\', debut) AS debut,
			strftime(\'%s\', fin) AS fin FROM compta_exercices
			WHERE cloture = 0 LIMIT 1;');

		if (!$e)
		{
			return [];
		}

		$y = date('Y', $e->debut);
		$m = date('m', $e->debut);
		$max = date('Ym', $e->fin);

		while ($y . $m <= $max)
		{
			if (!isset($data[$y . $m]))
			{
				$data[$y . $m] = 0;
			}

			if ($m == 12)
			{
				$m = '01';
				$y++;
			}
			else
			{
				$m++;
				$m = str_pad((int)$m, 2, '0', STR_PAD_LEFT);
			}
		}

		ksort($data);

		return $data;
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































































































































































































































Modified src/include/lib/Garradin/Config.php from [c889a0eda3] to [85c9d942d5].

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72


73
74
75
76
77
78
79
80
81
82
83
84
85
        $object = new \stdClass;

        $this->fields_types = [
            'nom_asso'                =>  $string,
            'adresse_asso'            =>  $string,
            'email_asso'              =>  $string,
            'site_asso'               =>  $string,
            
            'monnaie'                 =>  $string,
            'pays'                    =>  $string,
            
            'champs_membres'          =>  $object,
            
            'categorie_membres'       =>  $int,
            
            'categorie_dons'          =>  $int,
            'categorie_cotisations'   =>  $int,
            
            'accueil_wiki'            =>  $string,
            'accueil_connexion'       =>  $string,
            
            'frequence_sauvegardes'   =>  $int,
            'nombre_sauvegardes'      =>  $int,
            
            'champ_identifiant'       =>  $string,
            'champ_identite'          =>  $string,
            
            'version'                 =>  $string,


            
            'couleur1'                =>  $string,
            'couleur2'                =>  $string,
            'image_fond'              =>  $string,

            'desactiver_site'         => $bool,
        ];

        $db = DB::getInstance();

        $this->config = $db->getAssoc('SELECT cle, valeur FROM config ORDER BY cle;');

        foreach ($this->config as $key=>&$value)







|


|

|

|
<
<
<


|


|


|

>
>
|




|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
        $object = new \stdClass;

        $this->fields_types = [
            'nom_asso'                =>  $string,
            'adresse_asso'            =>  $string,
            'email_asso'              =>  $string,
            'site_asso'               =>  $string,

            'monnaie'                 =>  $string,
            'pays'                    =>  $string,

            'champs_membres'          =>  $object,

            'categorie_membres'       =>  $int,




            'accueil_wiki'            =>  $string,
            'accueil_connexion'       =>  $string,

            'frequence_sauvegardes'   =>  $int,
            'nombre_sauvegardes'      =>  $int,

            'champ_identifiant'       =>  $string,
            'champ_identite'          =>  $string,

            'version'                 =>  $string,
            'last_chart_change'       =>  $int,
            'last_version_check'      =>  $string,

            'couleur1'                =>  $string,
            'couleur2'                =>  $string,
            'image_fond'              =>  $string,

            'desactiver_site'         =>  $bool,
        ];

        $db = DB::getInstance();

        $this->config = $db->getAssoc('SELECT cle, valeur FROM config ORDER BY cle;');

        foreach ($this->config as $key=>&$value)
118
119
120
121
122
123
124

125


126
127
128
129
130
131
132
133
134
135
136
137
138
139


140
141
142
143
144


145
146
147
148
149
150
151
    {
        if (empty($this->modified))
            return true;

        $values = [];
        $db = DB::getInstance();


        if (isset($this->modified['image_fond']))


        {
            if ($current = $db->firstColumn('SELECT valeur FROM config WHERE cle = \'image_fond\';'))
            {
                try {
                    $f = new Fichiers($current);
                    $f->remove();
                }
                catch (\InvalidArgumentException $e) {
                    // Ignore: the file has already been deleted
                }
            }

            if (strlen($this->config['image_fond']) > 0)
            {


                $f = Fichiers::storeFromBase64('Image_fond_admin.png', $this->config['image_fond']);
                $this->config['image_fond'] = $f->id;
                unset($f);
            }
        }



        $db->begin();

        foreach ($this->modified as $key=>$modified)
        {
            $value = $this->config[$key];








>
|
>
>
|
|










|

>
>
|
|



>
>







117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
    {
        if (empty($this->modified))
            return true;

        $values = [];
        $db = DB::getInstance();

        // Image files
        if (isset($this->modified['image_fond'])) {
            $key = 'image_fond';
            $value =& $this->config[$key];

            if ($current = $db->firstColumn('SELECT valeur FROM config WHERE cle = ?;', $key))
            {
                try {
                    $f = new Fichiers($current);
                    $f->remove();
                }
                catch (\InvalidArgumentException $e) {
                    // Ignore: the file has already been deleted
                }
            }

            if (strlen($value) > 0)
            {
                $content = $value;
                $value = null;
                $f = Fichiers::storeFromBase64($key . '.png', $content);
                $value = $f->id;
                unset($f);
            }
        }

        unset($value, $key);

        $db->begin();

        foreach ($this->modified as $key=>$modified)
        {
            $value = $this->config[$key];

161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
            $db->preparedQuery('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);',
                [$key, $value]);
        }

        if (!empty($this->modified['champ_identifiant']))
        {
            // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique
            $db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL 
                WHERE '.$this->get('champ_identifiant').' = "";');

            // Création de l'index unique
            $db->exec('DROP INDEX IF EXISTS membres_identifiant;');
            $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$this->get('champ_identifiant').');');
        }








|







167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
            $db->preparedQuery('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);',
                [$key, $value]);
        }

        if (!empty($this->modified['champ_identifiant']))
        {
            // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique
            $db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL
                WHERE '.$this->get('champ_identifiant').' = "";');

            // Création de l'index unique
            $db->exec('DROP INDEX IF EXISTS membres_identifiant;');
            $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$this->get('champ_identifiant').');');
        }

187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
            throw new \OutOfBoundsException('Ce champ est inconnu.');
        }

        if (!array_key_exists($key, $this->config))
        {
            return null;
        }
        
        return $this->config[$key];
    }

    public function getVersion()
    {
        if (!array_key_exists('version', $this->config))
        {







|







193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
            throw new \OutOfBoundsException('Ce champ est inconnu.');
        }

        if (!array_key_exists($key, $this->config))
        {
            return null;
        }

        return $this->config[$key];
    }

    public function getVersion()
    {
        if (!array_key_exists('version', $this->config))
        {
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
                // Vérification que le champ existe bien
                if (!$champs->get($value))
                {
                    throw new UserException('Le champ '.$value.' n\'existe pas pour la configuration de '.$key);
                }

                // Vérification que le champ est unique pour l'identifiant
                if ($key == 'champ_identifiant' 
                    && !$db->firstColumn('SELECT (COUNT(DISTINCT lower('.$value.')) = COUNT(*)) 
                        FROM membres WHERE '.$value.' IS NOT NULL AND '.$value.' != \'\';'))
                {
                    throw new UserException('Le champ '.$value.' comporte des doublons et ne peut donc pas servir comme identifiant pour la connexion.');
                }
                break;
            }
            case 'categorie_cotisations':
            case 'categorie_dons':
            {
                return false;
                $db = DB::getInstance();
                if (!$db->firstColumn('SELECT 1 FROM compta_categories WHERE id = ?;', $value))
                {
                    throw new UserException('Champ '.$key.' : La catégorie comptable numéro \''.$value.'\' ne semble pas exister.');
                }
                break;
            }
            case 'categorie_membres':
            {
                $db = DB::getInstance();
                if (!$db->firstColumn('SELECT 1 FROM membres_categories WHERE id = ?;', $value))
                {







|
|




<
<
<
<
<
<
<
<
<
<
<







303
304
305
306
307
308
309
310
311
312
313
314
315











316
317
318
319
320
321
322
                // Vérification que le champ existe bien
                if (!$champs->get($value))
                {
                    throw new UserException('Le champ '.$value.' n\'existe pas pour la configuration de '.$key);
                }

                // Vérification que le champ est unique pour l'identifiant
                if ($key == 'champ_identifiant'
                    && !$db->firstColumn('SELECT (COUNT(DISTINCT lower('.$value.')) = COUNT(*))
                        FROM membres WHERE '.$value.' IS NOT NULL AND '.$value.' != \'\';'))
                {
                    throw new UserException('Le champ '.$value.' comporte des doublons et ne peut donc pas servir comme identifiant pour la connexion.');
                }











                break;
            }
            case 'categorie_membres':
            {
                $db = DB::getInstance();
                if (!$db->firstColumn('SELECT 1 FROM membres_categories WHERE id = ?;', $value))
                {

Deleted src/include/lib/Garradin/Cotisations.php version [895e912baf].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<?php

namespace Garradin;

class Cotisations
{
	/**
	 * Vérification des champs fournis pour la modification de donnée
	 * @param  array $data Tableau contenant les champs à ajouter/modifier
	 * @return void
	 */
	protected function _checkFields(&$data)
	{
		$db = DB::getInstance();

		if (!isset($data['intitule']) || trim($data['intitule']) == '')
		{
			throw new UserException('L\'intitulé ne peut rester vide.');
		}

		$data['intitule'] = trim($data['intitule']);

		if (isset($data['description']))
		{
			$data['description'] = trim($data['description']);
		}

		if (!isset($data['montant']) || !is_numeric($data['montant']) || (float)$data['montant'] < 0)
		{
			throw new UserException('Le montant doit être un nombre supérieur ou égal à zéro et valide.');
		}

		$data['montant'] = (float) $data['montant'];

		if (isset($data['duree']))
		{
			$data['duree'] = (int) $data['duree'];

			if ($data['duree'] < 0)
			{
				$data['duree'] = 0;
			}
		}

		if (isset($data['debut']) && trim($data['debut']) != '')
		{
			if (!empty($data['duree']))
			{
				throw new UserException('Il n\'est pas possible de spécifier une durée ET une date fixe, merci de choisir l\'une des deux options.');
			}

			if (!isset($data['fin']) || trim($data['fin']) == '')
			{
				throw new UserException('Une date de fin est obligatoire avec la date de début de validité.');
			}

			if (!Utils::checkDate($data['debut']))
			{
				throw new UserException('La date de début est invalide.');
			}

			if (!Utils::checkDate($data['fin']))
			{
				throw new UserException('La date de fin est invalide.');
			}
		}

		if (isset($data['id_categorie_compta']))
		{
			if ($data['id_categorie_compta'] != 0 && !$db->firstColumn('SELECT 1 FROM compta_categories WHERE id = ?;', (int) $data['id_categorie_compta']))
			{
				throw new UserException('Catégorie comptable inconnue');
			}

			$data['id_categorie_compta'] = (int) $data['id_categorie_compta'];
		}
	}

	/**
	 * Ajouter une cotisation
	 * @param array $data Tableau des champs à insérer
	 * @return integer ID de la cotisation créée
	 */
	public function add($data)
	{
		$db = DB::getInstance();

		$this->_checkFields($data);

		$db->insert('cotisations', $data);
		return $db->lastInsertRowId();
	}

	/**
	 * Modifier une cotisation
	 * @param  integer $id  ID de la cotisation à modifier
	 * @param  array $data Tableau des champs à modifier
	 * @return bool true si succès
	 */
	public function edit($id, $data)
	{
		$db = DB::getInstance();

        $this->_checkFields($data);

        return $db->update('cotisations', $data, 'id = :id', ['id' => (int) $id]);
	}

	/**
	 * Supprimer une cotisation
	 * @param  integer $id ID de la cotisation à supprimer
	 * @return integer true en cas de succès
	 */
	public function delete($id)
	{
		$db = DB::getInstance();

		$db->begin();

		// Les lignes liées dans les autres tables seront supprimées grâce à ON DELETE CASCADE
		$db->delete('cotisations', 'id = ?', (int) $id);

		$db->commit();

		return true;
	}

	/**
	 * Renvoie les infos sur une cotisation
	 * @param  integer $id Numéro de la cotisation
	 * @return array     Infos de la cotisation
	 */
	public function get($id)
	{
		$db = DB::getInstance();
		return $db->first('SELECT co.*,
			(SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres WHERE id_cotisation = co.id) AS nb_membres,
			(SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres AS cm WHERE id_cotisation = co.id
				AND ((co.duree IS NOT NULL AND date(cm.date, \'+\'||co.duree||\' days\') >= date())
					OR (co.fin IS NOT NULL AND co.debut <= cm.date AND co.fin >= cm.date))) AS nb_a_jour
			FROM cotisations AS co WHERE id = :id;', ['id' => (int) $id]);
	}

	public function listByName()
	{
		$db = DB::getInstance();
		return $db->get('SELECT * FROM cotisations ORDER BY intitule;');
	}

	public function listCurrent()
	{
		$db = DB::getInstance();
		return $db->get('SELECT * FROM cotisations WHERE fin >= date(\'now\') OR fin IS NULL
			ORDER BY transliterate_to_ascii(intitule) COLLATE NOCASE;');
	}

	public function listWithStats()
	{
		$db = DB::getInstance();
		return $db->get('SELECT co.*,
			(SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres WHERE id_cotisation = co.id) AS nb_membres,
			(SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres AS cm WHERE id_cotisation = co.id
				AND ((co.duree IS NOT NULL AND date(cm.date, \'+\'||co.duree||\' days\') >= date())
					OR (co.fin IS NOT NULL AND co.debut <= cm.date AND co.fin >= cm.date))) AS nb_a_jour
			FROM cotisations AS co
			ORDER BY transliterate_to_ascii(intitule) COLLATE NOCASE;');
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































































































































































































































Modified src/include/lib/Garradin/DB.php from [b5f765f6ec] to [dad60ad267].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18




19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55




56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75


76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
<?php

namespace Garradin;

use KD2\DB_SQLite3;

class DB extends DB_SQLite3
{
    /**
     * Application ID pour SQLite
     * @link https://www.sqlite.org/pragma.html#pragma_application_id
     */
    const APPID = 0x5da2d811;

    static protected $_instance = null;

    static public function getInstance($create = false)
    {




        return self::$_instance ?: self::$_instance = new DB($create);
    }

    private function __clone()
    {
        // Désactiver le clonage, car on ne veut qu'une seule instance
    }

    public function __construct($create = false)
    {
        if (!defined('\SQLITE3_OPEN_READWRITE'))
        {
            throw new \Exception('Module SQLite3 de PHP non présent. Merci de l\'installer.');
        }

        $flags = \SQLITE3_OPEN_READWRITE;

        if ($create)
        {
            $flags |= \SQLITE3_OPEN_CREATE;
        }

        parent::__construct(DB_FILE, $flags);

        // Ne pas se connecter ici, on ne se connectera que quand une requête sera faite
    }

    public function connect()
    {
        if (parent::connect())
        {
            // Activer les contraintes des foreign keys
            $this->exec('PRAGMA foreign_keys = ON;');

            // 10 secondes
            $this->db->busyTimeout(10 * 1000);
            $this->exec('PRAGMA journal_mode = TRUNCATE;');





            $this->db->createFunction('transliterate_to_ascii', ['Garradin\Utils', 'transliterateToAscii']);
        }
    }

    public function close()
    {
        parent::close();
        self::$_instance = null;
    }

    /**
     * Import a file containing SQL commands
     * Allows to use the statement ".read other_file.sql" to load other files
     * @param  string $file Path to file containing SQL commands
     * @return boolean
     */
    public function import($file)
    {
        $sql = file_get_contents($file);



        $dir = dirname($file);

        $sql = preg_replace_callback('/^\.read (.+\.sql)$/m', function ($match) use ($dir) {
            return file_get_contents($dir . DIRECTORY_SEPARATOR . $match[1]) . "\n";
        }, $sql);

        return $this->exec($sql);

    }

    /**
     * @see https://www.sqlite.org/lang_altertable.html
     */
    public function toggleForeignKeys($enable)
    {




|

|









|

>
>
>
>
|







|

<
<
<
<
|
<
|
<
<
<


<
<
<
<
<
<
<
|
|
|
|

|
|
|
>
>
>
>

|
|
|
<
|





<
<
<
<
<
<
|

<
>
>
|
<

|
<
<
|
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32




33

34



35
36







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

53
54
55
56
57
58






59
60

61
62
63

64
65


66
67
68
69
70
71
72
73
74
75
<?php

namespace Garradin;

use KD2\DB\SQLite3;

class DB extends SQLite3
{
    /**
     * Application ID pour SQLite
     * @link https://www.sqlite.org/pragma.html#pragma_application_id
     */
    const APPID = 0x5da2d811;

    static protected $_instance = null;

    static public function getInstance($create = false, $readonly = false)
    {
        if (null === self::$_instance) {
            self::$_instance = new DB('sqlite', ['file' => DB_FILE]);
        }

        return self::$_instance;
    }

    private function __clone()
    {
        // Désactiver le clonage, car on ne veut qu'une seule instance
    }

    public function connect(): void
    {




        if (null !== $this->db) {

            return;



        }








        parent::connect();

        // Activer les contraintes des foreign keys
        $this->db->exec('PRAGMA foreign_keys = ON;');

        // 10 secondes
        $this->db->busyTimeout(10 * 1000);

        // Performance enhancement
        // see https://www.cs.utexas.edu/~jaya/slides/apsys17-sqlite-slides.pdf
        // https://ericdraken.com/sqlite-performance-testing/
        $this->exec(sprintf('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA journal_size_limit = %d;', 32 * 1024 * 1024));

        $this->db->createFunction('transliterate_to_ascii', ['Garradin\Utils', 'transliterateToAscii']);
    }


    public function close(): void
    {
        parent::close();
        self::$_instance = null;
    }







    public function beginSchemaUpdate()
    {

        $this->toggleForeignKeys(false);
        $this->begin();
    }


    public function commitSchemaUpdate()


    {
        $this->commit();
        $this->toggleForeignKeys(true);
    }

    /**
     * @see https://www.sqlite.org/lang_altertable.html
     */
    public function toggleForeignKeys($enable)
    {

Added src/include/lib/Garradin/DynamicList.php version [f85cc3737c].





























































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
<?php

namespace Garradin;

class DynamicList
{
	protected $columns;
	protected $tables;
	protected $conditions;
	protected $group;
	protected $order;
	protected $modifier;
	protected $export_callback;
	protected $title = 'Liste';
	protected $count = 'COUNT(*)';
	protected $desc = true;
	protected $per_page = 100;
	protected $page = 1;

	private $count_result;

	public function __construct(array $columns, string $tables, string $conditions)
	{
		$this->columns = $columns;
		$this->tables = $tables;
		$this->conditions = $conditions;
		$this->order = key($columns);
	}

	public function __get($key)
	{
		return $this->$key;
	}

	public function setTitle(string $title) {
		$this->title = $title;
	}

	public function setModifier(callable $fn) {
		$this->modifier = $fn;
	}

	public function setExportCallback(callable $fn) {
		$this->export_callback = $fn;
	}

	public function setPageSize(?int $size) {
		$this->per_page = $size;
	}

	public function setConditions(string $conditions)
	{
		$this->conditions = $conditions;
	}

	public function orderBy(string $key, bool $desc)
	{
		if (!array_key_exists($key, $this->columns)) {
			throw new UserException('Invalid order: ' . $key);
		}

		$this->order = $key;
		$this->desc = $desc;
	}

	public function groupBy(string $value)
	{
		$this->group = $value;
	}

	public function count()
	{
		if (null === $this->count_result) {
			$sql = sprintf('SELECT %s FROM %s WHERE %s;', $this->count, $this->tables, $this->conditions);
			$this->count_result = DB::getInstance()->firstColumn($sql);
		}

		return $this->count_result;
	}

	public function export(string $name, string $format = 'csv')
	{
		$this->setPageSize(null);
		$columns = [];

		foreach ($this->columns as $key => $column) {
			if (empty($column['label'])) {
				$columns[] = $key;
				continue;
			}

			$columns[] = $column['label'];
		}

		if ('csv' == $format) {
			CSV::toCSV($name, $this->iterate(false), $this->getHeaderColumns(true), $this->export_callback);
		}
		else if ('ods' == $format) {
			CSV::toODS($name, $this->iterate(false), $this->getHeaderColumns(true), $this->export_callback);
		}
		else {
			throw new UserException('Invalid export format');
		}
	}

	public function paginationURL()
	{
		return Utils::getModifiedURL('?p=[ID]');
	}

	public function orderURL(string $order, bool $desc)
	{
		$query = array_merge($_GET, ['o' => $order, 'd' => (int) $desc]);
		$url = Utils::getSelfURL($query);
		return $url;
	}

	public function setCount(string $count)
	{
		$this->count = $count;
	}

	public function getHeaderColumns(bool $label_only = false)
	{
		$columns = [];

		foreach ($this->columns as $alias => $properties) {
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order && !$this->desc)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}

			if (!isset($properties['label'])) {
				continue;
			}

			$columns[$alias] = $label_only ? $properties['label'] : $properties;
		}

		return $columns;
	}

	public function iterate(bool $include_hidden = true)
	{
		$start = ($this->page - 1) * $this->per_page;
		$columns = [];

		foreach ($this->columns as $alias => $properties) {
			// Skip columns that require a certain order (eg. calculating a running sum)
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order && !$this->desc)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}

			if (!isset($properties['label']) && !$include_hidden) {
				continue;
			}

			$select = array_key_exists('select', $properties) ? $properties['select'] : $alias;

			if (null === $select) {
				$select = 'NULL';
			}

			$columns[] = sprintf('%s AS %s', $select, $alias);
		}

		$columns = implode(', ', $columns);

		if (isset($this->columns[$this->order]['order'])) {
			$order = sprintf($this->columns[$this->order]['order'], $this->desc ? 'DESC' : 'ASC');
		}
		else {
			$order = $this->order;

			if (true === $this->desc) {
				$order .= ' DESC';
			}
		}

		$group = $this->group ? 'GROUP BY ' . $this->group : '';

		$sql = sprintf('SELECT %s FROM %s WHERE %s %s ORDER BY %s',
			$columns, $this->tables, $this->conditions, $group, $order);

		if (null !== $this->per_page) {
			$sql .= sprintf(' LIMIT %d,%d', $start, $this->per_page);
		}

		foreach (DB::getInstance()->iterate($sql) as $row) {
			if ($this->modifier) {
				call_user_func_array($this->modifier, [&$row]);
			}

			yield $row;
		}
	}

	public function loadFromQueryString()
	{
		if (!empty($_GET['export'])) {
			$this->export($this->title, $_GET['export']);
			exit;
		}

		if (!empty($_GET['o'])) {
			$this->orderBy($_GET['o'], !empty($_GET['d']));
		}

		if (!empty($_GET['p'])) {
			$this->page = (int)$_GET['p'];
		}
	}
}

Added src/include/lib/Garradin/Entities/Accounting/Account.php version [bfec22341c].

































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
<?php

namespace Garradin\Entities\Accounting;

use DateTimeInterface;
use Garradin\Config;
use Garradin\CSV_Custom;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Entity;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Accounting\Charts;

class Account extends Entity
{
	const TABLE = 'acc_accounts';

	// Actif
	const ASSET = 1;

	// Passif
	const LIABILITY = 2;

	// Passif ou actif
	const ASSET_OR_LIABILITY = 3;

	// Charge
	const EXPENSE = 4;

	// Produit
	const REVENUE = 5;

	const POSITIONS_NAMES = [
		'',
		'Actif',
		'Passif',
		'Actif ou passif',
		'Charge',
		'Produit',
	];

	const TYPE_NONE = 0;
	const TYPE_BANK = 1;
	const TYPE_CASH = 2;

	/**
	 * Outstanding transaction accounts (like cheque or card payments)
	 */
	const TYPE_OUTSTANDING = 3;
	const TYPE_THIRD_PARTY = 4;

	const TYPE_EXPENSE = 5;
	const TYPE_REVENUE = 6;

	const TYPE_ANALYTICAL = 7;
	const TYPE_VOLUNTEERING = 8;

	const TYPE_OPENING = 9;
	const TYPE_CLOSING = 10;

	const TYPE_POSITIVE_RESULT = 11;
	const TYPE_NEGATIVE_RESULT = 12;

	const TYPES_NAMES = [
		'',
		'Banque',
		'Caisse',
		'Attente d\'encaissement',
		'Tiers',
		'Dépenses',
		'Recettes',
		'Analytique',
		'Bénévolat',
		'Ouverture',
		'Clôture',
		'Résultat excédentaire',
		'Résultat déficitaire',
	];

	const LIST_COLUMNS = [
		'id' => [
			'select' => 't.id',
			'label' => 'N°',
		],
		'id_line' => [
			'select' => 'l.id',
		],
		'date' => [
			'label' => 'Date',
			'select' => 't.date',
			'order' => 'date %s, id %1$s',
		],
		'debit' => [
			'select' => 'l.debit',
			'label' => 'Débit',
		],
		'credit' => [
			'select' => 'l.credit',
			'label' => 'Crédit',
		],
		'change' => [
			'select' => '(l.credit - l.debit) * %d',
			'label' => 'Mouvement',
		],
		'sum' => [
			'select' => NULL,
			'label' => 'Solde cumulé',
			'only_with_order' => 'date',
		],
		'reference' => [
			'label' => 'Pièce comptable',
			'select' => 't.reference',
		],
		'type' => [
			'select' => 't.type',
		],
		'label' => [
			'select' => 't.label',
			'label' => 'Libellé',
		],
		'line_label' => [
			'select' => 'l.label',
			'label' => 'Libellé ligne'
		],
		'line_reference' => [
			'label' => 'Réf. ligne',
			'select' => 'l.reference',
		],
		'id_analytical' => [
			'select' => 'l.id_analytical',
		],
		'code_analytical' => [
			'label' => 'Projet',
			'select' => 'b.code',
		],
		'status' => [
			'select' => 't.status',
		],
	];

	protected $id;
	protected $id_chart;
	protected $code;
	protected $label;
	protected $description;
	protected $position;
	protected $type;
	protected $user = 0;

	protected $_types = [
		'id'          => 'int',
		'id_chart'    => 'int',
		'code'        => 'string',
		'label'       => 'string',
		'description' => '?string',
		'position'    => 'int',
		'type'        => 'int',
		'user'        => 'int',
	];

	protected $_form_rules = [
		'code'        => 'required|string|alpha_num|max:10',
		'label'       => 'required|string|max:200',
		'description' => 'string|max:2000',
		'position'    => 'required|numeric|min:0',
		'type'        => 'required|numeric|min:0',
	];

	public function selfCheck(): void
	{
		$db = DB::getInstance();

		$this->assert(!empty($this->id_chart), 'Aucun plan comptable lié');

		$where = 'code = ? AND id_chart = ?';
		$where .= $this->exists() ? sprintf(' AND id != %d', $this->id()) : '';

		if ($db->test(self::TABLE, $where, $this->code, $this->id_chart)) {
			throw new ValidationException(sprintf('Le code "%s" est déjà utilisé par un autre compte.', $this->code));
		}

		$this->assert(array_key_exists($this->type, self::TYPES_NAMES), 'Type invalide');
		$this->assert(array_key_exists($this->position, self::POSITIONS_NAMES), 'Position invalide');
		$this->assert($this->user === 0 || $this->user === 1);

		parent::selfCheck();
	}

	public function listJournal(int $year_id, bool $simple = false)
	{
		$columns = self::LIST_COLUMNS;

		$tables = 'acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			LEFT JOIN acc_accounts b ON b.id = l.id_analytical';
		$conditions = sprintf('l.id_account = %d AND t.id_year = %d', $this->id(), $year_id);

		$sum = 0;
		$reverse = $simple && self::isReversed($this->type) ? -1 : 1;

		if ($simple) {
			unset($columns['debit']['label'], $columns['credit']['label'], $columns['line_label']);
			$columns['line_reference']['label'] = 'Réf. paiement';
			$columns['change']['select'] = sprintf($columns['change']['select'], $reverse);
		}
		else {
			unset($columns['change']);
		}

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', false);
		$list->setCount('COUNT(*)');
		$list->setPageSize(null);
		$list->setModifier(function (&$row) use (&$sum, $reverse) {
			if (property_exists($row, 'sum')) {
				$sum += isset($row->change) ? $row->change : ($row->credit - $row->debit);
				$row->sum = $sum;
			}

			$row->date = \DateTime::createFromFormat('!Y-m-d', $row->date);
		});
		$list->setExportCallback(function (&$row) {
			static $columns = ['change', 'sum', 'credit', 'debit'];
			foreach ($columns as $key) {
				if (isset($row->$key)) {
					$row->$key = Utils::money_format($row->$key, '.', '', false);
				}
			}
		});

		return $list;
	}

	static public function isReversed(int $type): bool
	{
		return in_array($type, [self::TYPE_BANK, self::TYPE_CASH, self::TYPE_OUTSTANDING, self::TYPE_EXPENSE, self::TYPE_THIRD_PARTY]);
	}

	public function getReconcileJournal(int $year_id, DateTimeInterface $start_date, DateTimeInterface $end_date, bool $only_non_reconciled = false)
	{
		if ($end_date < $start_date) {
			throw new ValidationException('La date de début ne peut être avant la date de fin.');
		}

		$condition = $only_non_reconciled ? ' AND l.reconciled = 0' : '';

		$db = DB::getInstance();
		$sql = 'SELECT l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, l.reconciled, l.id AS id_line
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			WHERE l.id_account = ? AND t.id_year = ? AND t.date >= ? AND t.date <= ? %s
			ORDER BY t.date, t.id;';
		$rows = $db->iterate(sprintf($sql, $condition), $this->id(), $year_id, $start_date->format('Y-m-d'), $end_date->format('Y-m-d'));

		$sum = $this->getSumAtDate($year_id, $start_date);
		$reconciled_sum = $this->getSumAtDate($year_id, $start_date, true);

		$start_sum = false;

		foreach ($rows as $row) {
			if (!$start_sum) {
				yield (object) ['sum' => $sum, 'date' => $start_date];
				$start_sum = true;
			}

			$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);
			$sum += ($row->credit - $row->debit);
			$row->running_sum = $sum;

			if ($row->reconciled) {
				$reconciled_sum += ($row->credit - $row->debit);
			}

			$row->reconciled_sum = $reconciled_sum;

			yield $row;
		}

		if (!$only_non_reconciled) {
			yield (object) ['sum' => $sum, 'reconciled_sum' => $reconciled_sum, 'date' => $end_date];
		}
	}

	public function mergeReconcileJournalAndCSV(\Generator $journal, CSV_Custom $csv)
	{
		$lines = [];

		$csv = iterator_to_array($csv->iterate());
		$journal = iterator_to_array($journal);
		$i = 0;
		$sum = 0;

		foreach ($csv as $k => &$line) {
			try {
				$date = \DateTime::createFromFormat('!d/m/Y', $line->date);
				$line->amount = ($line->amount < 0 ? -1 : 1) * Utils::moneyToInteger($line->amount);

				if (!$date) {
					throw new UserException('Date invalide : ' . $line->date);
				}

				$line->date = $date;
			}
			catch (UserException $e) {
				throw new UserException(sprintf('Ligne %d : %s', $k, $e->getMessage()));
			}
		}
		unset($line);

		foreach ($journal as $j) {
			$id = $j->date->format('Ymd') . '.' . $i++;

			$row = (object) ['csv' => null, 'journal' => $j];

			if (isset($j->debit)) {
				foreach ($csv as &$line) {
					if (!isset($line->date)) {
						 continue;
					}
					if ($j->date->format('Ymd') == $line->date->format('Ymd')
						&& ($j->credit == abs($line->amount) || $j->debit == abs($line->amount))) {
						$row->csv = $line;
						$line = null;
						break;
					}
				}
			}

			$lines[$id] = $row;
		}

		unset($j);

		foreach ($csv as $line) {
			if (null == $line) {
				continue;
			}

			$id = $line->date->format('Ymd') . '.' . ($i++);
			$lines[$id] = (object) ['csv' => $line, 'journal' => null];
		}

		ksort($lines);
		$prev = null;

		foreach ($lines as &$line) {
			$line->add = false;

			if (isset($line->csv)) {
				$sum += $line->csv->amount;
				$line->csv->running_sum = $sum;

				if ($prev && ($prev->date->format('Ymd') != $line->csv->date->format('Ymd') || $prev->label != $line->csv->label)) {
					$prev = null;
				}
			}

			if (isset($line->csv) && isset($line->journal)) {
				$prev = null;
			}

			if (isset($line->csv) && !isset($line->journal) && !$prev) {
				$line->add = true;
				$prev = $line->csv;
			}
		}

		return $lines;
	}

	public function getDepositJournal(int $year_id, array $checked = []): \Generator
	{
		$res = DB::getInstance()->iterate('SELECT l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, l.reconciled, l.id AS id_line, l.id_account
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			WHERE t.id_year = ? AND l.id_account = ? AND l.credit = 0 AND NOT (t.status & ?)
			ORDER BY t.date, t.id;',
			$year_id, $this->id(), Transaction::STATUS_DEPOSIT);

		$sum = 0;

		foreach ($res as $row) {
			$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);
			$sum += ($row->credit - $row->debit);
			$row->running_sum = $sum;
			$row->checked = array_key_exists($row->id, $checked);
			yield $row;
		}
	}

	public function getSum(int $year_id, bool $simple = false): int
	{
		$sum = (int) DB::getInstance()->firstColumn('SELECT SUM(l.credit) - SUM(l.debit)
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			wHERE l.id_account = ? AND t.id_year = ?;', $this->id(), $year_id);

		if ($simple && self::isReversed($this->type)) {
			$sum *= -1;
		}

		return $sum;
	}


	public function getSumAtDate(int $year_id, DateTimeInterface $date, bool $reconciled_only = false): int
	{
		$sql = sprintf('SELECT SUM(l.credit) - SUM(l.debit)
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			wHERE l.id_account = ? AND t.id_year = ? AND t.date < ? %s;',
			$reconciled_only ? 'AND l.reconciled = 1' : '');
		return (int) DB::getInstance()->firstColumn($sql, $this->id(), $year_id, $date->format('Y-m-d'));
	}

	public function importSimpleForm(array $translate_type_position, array $translate_type_codes, ?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($source['type'])) {
			throw new UserException('Le type est obligatoire dans ce formulaire');
		}

		$type = (int) $source['type'];

		if (array_key_exists($type, $translate_type_position)) {
			$source['position'] = $translate_type_position[$type];
		}
		else {
			$source['position'] = self::ASSET_OR_LIABILITY;
		}

		if (array_key_exists($type, $translate_type_codes)) {
			$source['code'] = $translate_type_codes[$type];
		}

		$this->importForm($source);
	}

	public function importLimitedForm(?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		$data = array_intersect_key($source, array_flip(['type', 'description']));
		parent::import($data);
	}

	public function canDelete(): bool
	{
		return !DB::getInstance()->firstColumn(sprintf('SELECT 1 FROM %s WHERE id_account = ? LIMIT 1;', Line::TABLE), $this->id());
	}

	/**
	 * An account properties (position, label and code) can only be changed if:
	 * * it's either a user-created account or an account part of a user-created chart
	 * * has no transactions in a closed year
	 * @return bool
	 */
	public function canEdit(): bool
	{
		$db = DB::getInstance();
		$sql = sprintf('SELECT 1 FROM %s l
			INNER JOIN %s t ON t.id = l.id_transaction
			INNER JOIN %s y ON y.id = t.id_year
			WHERE l.id_account = ? AND y.closed = 1
			LIMIT 1;', Line::TABLE, Transaction::TABLE, Year::TABLE);
		$has_transactions_in_closed_year = $db->firstColumn($sql, $this->id());

		if ($has_transactions_in_closed_year) {
			return false;
		}

		if ($this->user) {
			return true;
		}

		return $db->test(Chart::TABLE, 'id = ? AND code IS NULL', $this->id_chart);
	}

	public function chart(): Chart
	{
		return Charts::get($this->id_chart);
	}

	public function save(): bool
	{
		Config::getInstance()->set('last_chart_change', time());
		return parent::save();
	}
}

Added src/include/lib/Garradin/Entities/Accounting/Chart.php version [297b8e2f31].









































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace Garradin\Entities\Accounting;

use Garradin\DB;
use Garradin\Entity;
use Garradin\Utils;
use Garradin\ValidationException;
use Garradin\UserException;
use Garradin\Accounting\Accounts;

class Chart extends Entity
{
	const TABLE = 'acc_charts';

	protected $id;
	protected $label;
	protected $country;
	protected $code;
	protected $archived = 0;

	protected $_types = [
		'id'       => 'int',
		'label'    => 'string',
		'country'  => 'string',
		'code'     => '?string',
		'archived' => 'int',
	];

	protected $_form_rules = [
		'label'    => 'required|string|max:200',
		'country'  => 'required|string|size:2',
		'archived' => 'numeric|min:0|max:1'
	];

	public function selfCheck(): void
	{
		parent::selfCheck();
		$this->assert(Utils::getCountryName($this->country), 'Le code pays doit être un code ISO valide');
		$this->assert($this->archived === 0 || $this->archived === 1);
	}

	public function accounts()
	{
		return new Accounts($this->id());
	}

	public function canDelete()
	{
		return !DB::getInstance()->firstColumn(sprintf('SELECT 1 FROM %s WHERE id_chart = ? LIMIT 1;', Year::TABLE), $this->id());
	}
}

Added src/include/lib/Garradin/Entities/Accounting/Line.php version [0fde672e4f].







































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php

namespace Garradin\Entities\Accounting;

use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;

class Line extends Entity
{
	const TABLE = 'acc_transactions_lines';

	protected $id;
	protected $id_transaction;
	protected $id_account;
	protected $credit = 0;
	protected $debit = 0;
	protected $reference;
	protected $label;
	protected $reconciled = 0;
	protected $id_analytical;

	protected $_types = [
		'id'             => 'int',
		'id_transaction' => 'int',
		'id_account'     => 'int',
		'credit'         => 'int',
		'debit'          => 'int',
		'reference'      => '?string',
		'label'          => '?string',
		'reconciled'     => 'int',
		'id_analytical'  => '?int',
	];

	protected $_form_rules = [
		'id_account'     => 'required|numeric|in_table:acc_accounts,id',
		'id_analytical'  => 'numeric|in_table:acc_accounts,id',
		'credit'         => 'money|min:0',
		'debit'          => 'money|min:0',
		'reference'      => 'string|max:200',
		'label'          => 'string|max:200',
	];

	public function filterUserValue(string $type, $value, string $key)
	{
		if ($key == 'credit' || $key == 'debit')
		{
			$value = Utils::moneyToInteger($value);
		}
		elseif ($key == 'id_analytical' && $value == 0) {
			$value = null;
		}

		$value = parent::filterUserValue($type, $value, $key);

		return $value;
	}

	public function selfCheck(): void
	{
		parent::selfCheck();
		$this->assert($this->credit || $this->debit, 'Aucun montant au débit ou au crédit');
		$this->assert(($this->credit * $this->debit) === 0 && ($this->credit + $this->debit) > 0, 'Ligne non équilibrée : crédit ou débit doit valoir zéro.');
		$this->assert($this->id_transaction, 'Aucun mouvement n\'a été indiqué pour cette ligne.');
		$this->assert($this->reconciled === 0 || $this->reconciled === 1);
	}
}

Added src/include/lib/Garradin/Entities/Accounting/Transaction.php version [d64763b884].





















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
<?php

namespace Garradin\Entities\Accounting;

use KD2\DB\EntityManager;
use Garradin\Entity;
use Garradin\Fichiers;
use Garradin\Accounting\Accounts;
use Garradin\ValidationException;
use Garradin\DB;
use Garradin\Config;
use Garradin\Utils;
use Garradin\UserException;

class Transaction extends Entity
{
	const TABLE = 'acc_transactions';

	const TYPE_ADVANCED = 0;
	const TYPE_REVENUE = 1;
	const TYPE_EXPENSE = 2;
	const TYPE_TRANSFER = 3;
	const TYPE_DEBT = 4;
	const TYPE_CREDIT = 5;

	const STATUS_WAITING = 1;
	const STATUS_PAID = 2;
	const STATUS_DEPOSIT = 4;

	const STATUS_NAMES = [
		1 => 'En attente de règlement',
		2 => 'Réglé',
		4 => 'Déposé en banque',
	];

	const TYPES_NAMES = [
		'Avancé',
		'Recette',
		'Dépense',
		'Virement',
		'Dette',
		'Créance',
	];

	protected $id;
	protected $type;
	protected $status = 0;
	protected $label;
	protected $notes;
	protected $reference;

	protected $date;

	protected $validated = 0;

	protected $hash;
	protected $prev_hash;

	protected $id_year;
	protected $id_creator;
	protected $id_related;

	protected $_types = [
		'id'        => 'int',
		'type'      => 'int',
		'status'    => 'int',
		'label'     => 'string',
		'notes'     => '?string',
		'reference' => '?string',
		'date'      => 'date',
		'validated' => 'bool',
		'hash'      => '?string',
		'prev_hash' => '?string',
		'id_year'   => 'int',
		'id_creator' => '?int',
		'id_related' => '?int',
	];

	protected $_form_rules = [
		'label'     => 'required|string|max:200',
		'notes'     => 'string|max:20000',
		'reference' => 'string|max:200',
		'date'      => 'required|date_format:d/m/Y',
	];

	protected $_lines;
	protected $_old_lines = [];

	protected $_related;

	static public function getTypeFromAccountType(int $account_type)
	{
		switch ($account_type) {
			case Account::TYPE_REVENUE:
				return self::TYPE_REVENUE;
			case Account::TYPE_EXPENSE:
				return self::TYPE_EXPENSE;
			case Account::TYPE_THIRD_PARTY:
				return self::TYPE_DEBT;
			default:
				return self::TYPE_TRANSFER;
		}
	}

	public function getLinesWithAccounts()
	{
		$em = EntityManager::getInstance(Line::class);
		return $em->DB()->get('SELECT
			l.*, a.label AS account_name, a.code AS account_code,
			b.label AS analytical_name
			FROM acc_transactions_lines l
			INNER JOIN acc_accounts a ON a.id = l.id_account
			LEFT JOIN acc_accounts b ON b.id = l.id_analytical
			WHERE l.id_transaction = ? ORDER BY l.id;', $this->id);
	}

	public function getLines($with_accounts = false)
	{
		if (null === $this->_lines && $this->exists()) {
			$em = EntityManager::getInstance(Line::class);
			$this->_lines = $em->all('SELECT * FROM @TABLE WHERE id_transaction = ? ORDER BY id;', $this->id);
		}
		elseif (null === $this->_lines) {
			$this->_lines = [];
		}

		return $this->_lines;
	}

	public function removeLine(Line $remove)
	{
		$new = [];

		foreach ($this->getLines() as $line) {
			if ($line->id === $remove->id) {
				$this->_old_lines[] = $remove;
			}
			else {
				$new[] = $line;
			}
		}

		$this->_lines = $new;
	}

	public function resetLines()
	{
		$this->_old_lines = $this->getLines();
		$this->_lines = [];
	}

	public function getLine(int $id)
	{
		foreach ($this->getLines() as $line) {
			if ($line->id === $id) {
				return $line;
			}
		}

		return null;
	}

	public function getLinesCreditSum()
	{
		$sum = 0;

		foreach ($this->getLines() as $line) {
			$sum += $line->credit;
		}

		return $sum;
	}

	public function getTypesAccounts()
	{
		if ($this->type == self::TYPE_ADVANCED) {
			return [];
		}

		$debit = null;
		$credit = null;

		$lines = $this->getLinesWithAccounts();

		foreach ($lines as $line) {
			$account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_name)];

			if ($line->debit) {
				$debit = $account;
			}
			else {
				$credit = $account;
			}
		}

		$type = $this->getTypesDetails()[$this->type];

		return [
			$type->accounts[0]->position == 'credit' ? $credit : $debit,
			$type->accounts[1]->position == 'credit' ? $credit : $debit,
		];
	}

/*
	public function getHash()
	{
		if (!$this->id_year) {
			throw new \LogicException('Il n\'est pas possible de hasher un mouvement qui n\'est pas associé à un exercice');
		}

		static $keep_keys = [
			'label',
			'notes',
			'reference',
			'date',
			'validated',
			'prev_hash',
		];

		$hash = hash_init('sha256');
		$values = $this->asArray();
		$values = array_intersect_key($values, $keep_keys);

		hash_update($hash, implode(',', array_keys($values)));
		hash_update($hash, implode(',', $values));

		foreach ($this->getLines() as $line) {
			hash_update($hash, implode(',', [$line->compte, $line->debit, $line->credit]));
		}

		return hash_final($hash, false);
	}

	public function checkHash()
	{
		return hash_equals($this->getHash(), $this->hash);
	}
*/

	public function addLine(Line $line)
	{
		$this->_lines[] = $line;
	}

	public function sum(): int
	{
		$sum = 0;

		foreach ($this->getLines() as $line) {
			$sum += $line->credit;
			// Because credit == debit, we only use credit
		}

		return $sum;
	}

	public function save(): bool
	{
		if ($this->validated && !isset($this->_modified['validated'])) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été validé');
		}

		$db = DB::getInstance();

		if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui fait partie d\'un exercice clôturé');
		}

		if (!parent::save()) {
			return false;
		}

		foreach ($this->getLines() as $line)
		{
			$line->id_transaction = $this->id();
			$line->save();
		}

		foreach ($this->_old_lines as $line)
		{
			$line->delete();
		}

		// Remove flag
		if ((self::TYPE_DEBT == $this->type || self::TYPE_CREDIT == $this->type) && $this->_related) {
			$this->_related->removeStatus(self::STATUS_WAITING);
			$this->_related->addStatus(self::STATUS_PAID);
			$this->_related->save();
		}

		return true;
	}

	public function removeStatus(int $property) {
		$this->set('status', $this->status & ~$property);
	}

	public function addStatus(int $property) {
		$this->set('status', $this->status | $property);
	}

	public function delete(): bool
	{
		if ($this->validated) {
			throw new ValidationException('Il n\'est pas possible de supprimer une écriture qui a été validée');
		}

		$db = DB::getInstance();

		if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) {
			throw new ValidationException('Il n\'est pas possible de supprimer une écriture qui fait partie d\'un exercice clôturé');
		}

		Fichiers::deleteLinkedFiles(Fichiers::LIEN_COMPTA, $this->id());

		return parent::delete();
	}

	public function selfCheck(): void
	{
		parent::selfCheck();

		$db = DB::getInstance();
		$config = Config::getInstance();

		// ID d'exercice obligatoire
		if (null === $this->id_year) {
			throw new \LogicException('Aucun exercice spécifié.');
		}

		if (!$db->test(Year::TABLE, 'id = ? AND start_date <= ? AND end_date >= ?;', $this->id_year, $this->date->format('Y-m-d'), $this->date->format('Y-m-d')))
		{
			throw new ValidationException('La date ne correspond pas à l\'exercice sélectionné : ' . $this->date->format('d/m/Y'));
		}

		$total = 0;

		$lines = $this->getLines();

		foreach ($lines as $line) {
			$total += $line->credit;
			$total -= $line->debit;
		}

		if (0 !== $total) {
			throw new ValidationException(sprintf('Écriture non équilibrée : déséquilibre (%s) entre débits et crédits', Utils::money_format($total)));
		}

		if (!array_key_exists($this->type, self::TYPES_NAMES)) {
			throw new ValidationException('Type d\'écriture inconnu : ' . $this->type);
		}
	}

	public function importFromDepositForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($source['amount'])) {
			throw new UserException('Montant non précisé');
		}

		$this->type = self::TYPE_TRANSFER;
		$amount = $source['amount'];

		$key = 'account_transfer';
		$account = isset($source[$key]) && @count($source[$key]) ? key($source[$key]) : null;

		$line = new Line;
		$line->importForm([
			'debit'      => $amount,
			'credit'     => 0,
			'id_account' => $account,
		]);

		$this->addLine($line);

		$this->importForm($source);
	}

	public function importFromNewForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (!isset($source['type'])) {
			throw new ValidationException('Type d\'écriture inconnu');
		}

		$type = $source['type'];

		$this->importForm($source);

		if (self::TYPE_ADVANCED == $type) {
			$lines = Utils::array_transpose($source['lines']);

			foreach ($lines as $i => $line) {
				$line['id_account'] = @count($line['account']) ? key($line['account']) : null;

				if (!$line['id_account']) {
					throw new ValidationException('Numéro de compte invalide sur la ligne ' . ($i+1));
				}

				$line = (new Line)->import($line);
				$this->addLine($line);
			}
		}
		else {
			$details = self::getTypesDetails();

			if (!array_key_exists($type, $details)) {
				throw new ValidationException('Type d\'écriture inconnu');
			}

			if (empty($this->_related) && ($type == self::TYPE_DEBT || $type == self::TYPE_CREDIT)) {
				$this->status = self::STATUS_WAITING;
			}

			if (empty($source['amount'])) {
				throw new UserException('Montant non précisé');
			}

			$amount = $source['amount'];

			// Fill lines using a pre-defined setup obtained from getTypesDetails
			foreach ($details[$type]->accounts as $k => $account) {
				$credit = $account->position == 'credit' ? $amount : 0;
				$debit = $account->position == 'debit' ? $amount : 0;
				$key = sprintf('account_%d_%d', $type, $k);
				$account = isset($source[$key]) && @count($source[$key]) ? key($source[$key]) : null;

				$line = new Line;
				$line->importForm([
					'reference'     => $source['payment_reference'],
					'credit'        => $credit,
					'debit'         => $debit,
					'id_account'    => $account,
					'id_analytical' => !empty($source['id_analytical']) ? $source['id_analytical'] : null,
				]);
				$this->addLine($line);
			}
		}
	}

	public function importFromEditForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}


		$this->resetLines();
		$this->importFromNewForm($source);
	}

	public function importFromBalanceForm(Year $year, ?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (!isset($source['lines']) || !is_array($source['lines'])) {
			throw new ValidationException('Aucun contenu trouvé dans le formulaire.');
		}

		$this->label = 'Balance d\'ouverture';
		$this->date = $year->start_date;
		$this->id_year = $year->id();
		$this->type = self::TYPE_ADVANCED;

		try {
			$lines = Utils::array_transpose($source['lines']);
		}
		catch (\LogicException $e) {
			throw new ValidationException('Aucun compte sélectionné pour certaines lignes.');
		}

		$debit = $credit = 0;

		foreach ($lines as $line) {
			$line['id_account'] = @count($line['account']) ? key($line['account']) : null;
			$line = (new Line)->importForm($line);
			$this->addLine($line);

			$debit += $line->debit;
			$credit += $line->credit;
		}

		if ($debit != $credit) {
			// Add final balance line
			$line = new Line;

			if ($debit > $credit) {
				$line->debit = $debit - $credit;
			}
			else {
				$line->credit = $credit - $debit;
			}

			$open_account = EntityManager::findOne(Account::class, 'SELECT * FROM @TABLE WHERE id_chart = ? AND type = ? LIMIT 1;', $year->id_chart, Account::TYPE_OPENING);

			if (!$open_account) {
				throw new ValidationException('Aucun compte favori de bilan d\'ouverture n\'existe dans le plan comptable');
			}

			$line->id_account = $open_account->id();

			$this->addLine($line);
		}
	}

	public function year()
	{
		return EntityManager::findOneById(Year::class, $this->id_year);
	}

	public function listFiles()
	{
		return Fichiers::listLinkedFiles(Fichiers::LIEN_COMPTA, $this->id());
	}

	public function linkToUser(int $user_id, ?int $service_id = null)
	{
		$db = EntityManager::getInstance(self::class)->DB();

		return $db->preparedQuery('INSERT OR IGNORE INTO acc_transactions_users (id_transaction, id_user, id_service_user) VALUES (?, ?, ?);',
			$this->id(), $user_id, $service_id);
	}

	public function updateLinkedUsers(array $users)
	{
		$db = EntityManager::getInstance(self::class)->DB();

		$db->begin();

		$sql = sprintf('DELETE FROM acc_transactions_users WHERE id_transaction = ? AND id_service_user IS NULL AND %s;', $db->where('id_user', 'NOT IN', $users));
		$db->preparedQuery($sql, $this->id());

		foreach ($users as $id) {
			$db->preparedQuery('INSERT OR IGNORE INTO acc_transactions_users (id_transaction, id_user) VALUES (?, ?);', $this->id(), $id);
		}

		$db->commit();
	}

	public function listLinkedUsers()
	{
		$db = EntityManager::getInstance(self::class)->DB();
		$identity_column = Config::getInstance()->get('champ_identite');
		$sql = sprintf('SELECT m.id, m.%s AS identity, l.id_service_user FROM membres m INNER JOIN acc_transactions_users l ON l.id_user = m.id WHERE l.id_transaction = ?;', $identity_column);
		return $db->get($sql, $this->id());
	}

	public function listLinkedUsersAssoc()
	{
		$db = EntityManager::getInstance(self::class)->DB();
		$identity_column = Config::getInstance()->get('champ_identite');
		$sql = sprintf('SELECT m.id, m.%s AS identity FROM membres m INNER JOIN acc_transactions_users l ON l.id_user = m.id WHERE l.id_transaction = ?;', $identity_column);
		return $db->getAssoc($sql, $this->id());
	}

	static public function getTypesDetails()
	{
		$details = [
			self::TYPE_REVENUE => [
				'accounts' => [
					[
						'label' => 'Type de recette',
						'targets' => [Account::TYPE_REVENUE],
						'position' => 'credit',
					],
					[
						'label' => 'Compte d\'encaissement',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_REVENUE],
			],
			self::TYPE_EXPENSE => [
				'accounts' => [
					[
						'label' => 'Type de dépense',
						'targets' => [Account::TYPE_EXPENSE],
						'position' => 'debit',
					],
					[
						'label' => 'Compte de décaissement',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'credit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_EXPENSE],
				'help' => null,
			],
			self::TYPE_TRANSFER => [
				'accounts' => [
					[
						'label' => 'De',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'credit',
					],
					[
						'label' => 'Vers',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_TRANSFER],
				'help' => 'Dépôt en banque, virement interne, etc.',
			],
			self::TYPE_DEBT => [
				'accounts' => [
					[
						'label' => 'Compte de tiers',
						'targets' => [Account::TYPE_THIRD_PARTY],
						'position' => 'credit',
					],
					[
						'label' => 'Type de dette (dépense)',
						'targets' => [Account::TYPE_EXPENSE],
						'position' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_DEBT],
				'help' => 'Quand l\'association doit de l\'argent à un membre ou un fournisseur',
			],
			self::TYPE_CREDIT => [
				'accounts' => [
					[
						'label' => 'Compte de tiers',
						'targets' => [Account::TYPE_THIRD_PARTY],
						'position' => 'debit',
					],
					[
						'label' => 'Type de créance (recette)',
						'targets' => [Account::TYPE_REVENUE],
						'position' => 'credit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_CREDIT],
				'help' => 'Quand un membre ou un fournisseur doit de l\'argent à l\'association',
			],
			self::TYPE_ADVANCED => [
				'accounts' => [],
				'label' => self::TYPES_NAMES[self::TYPE_ADVANCED],
				'help' => 'Choisir les comptes du plan comptable, ventiler une écriture sur plusieurs comptes, etc.',
			],
		];

		foreach ($details as $key => &$type) {
			$type = (object) $type;
			$type->id = $key;
			foreach ($type->accounts as &$account) {
				$account = (object) $account;
				$account->targets_string = implode(':', $account->targets);
			}
		}

		return $details;
	}

	public function payOffFrom(int $id): \stdClass
	{
		$this->_related = EntityManager::findOneById(self::class, $id);

		if (!$this->_related) {
			throw new \LogicException('Écriture d\'origine invalide');
		}

		$this->id_related = $this->_related->id();
		$this->label = ($this->_related->type == Transaction::TYPE_DEBT ? 'Règlement de dette : ' : 'Règlement de créance : ') . $this->_related->label;
		$this->type = $this->_related->type;

		$out = (object) [
			'id' => $this->_related->id,
			'sum' => $this->_related->sum(),
			'id_account' => null,
			'form_account_name' => sprintf('account_%d_%d', $this->type, 1),
			'form_target_name' => sprintf('account_%d_%d', $this->type, 0),
		];

		foreach ($this->_related->getLines() as $line) {
			if (($this->_related->type == self::TYPE_DEBT && $line->debit)
				|| ($this->_related->type == self::TYPE_CREDIT && $line->credit)) {
				// Skip the type of debt/credit, just keep the thirdparty account
				continue;
			}

			$out->id_account = $line->id_account;
			break;
		}

		return $out;
	}
}

Added src/include/lib/Garradin/Entities/Accounting/Year.php version [efb670bde0].





































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

namespace Garradin\Entities\Accounting;

use KD2\DB\EntityManager;
use Garradin\DB;
use Garradin\Entity;
use Garradin\Fichiers;
use Garradin\UserException;
use Garradin\Accounting\Accounts;

class Year extends Entity
{
	const TABLE = 'acc_years';

	protected $id;
	protected $label;
	protected $start_date;
	protected $end_date;
	protected $closed = 0;
	protected $id_chart;

	protected $_types = [
		'id'         => 'int',
		'label'      => 'string',
		'start_date' => 'date',
		'end_date'   => 'date',
		'closed'     => 'int',
		'id_chart'   => 'int',
	];

	protected $_form_rules = [
		'label'      => 'required|string|max:200',
		'start_date' => 'required|date_format:d/m/Y',
		'end_date'   => 'required|date_format:d/m/Y',
	];

	public function selfCheck(): void
	{
		parent::selfCheck();
		$this->assert($this->start_date < $this->end_date, 'La date de fin doit être postérieure à la date de début');
		$this->assert($this->closed === 0 || $this->closed === 1);
		$this->assert($this->closed == 1 || !isset($this->_modified['closed']), 'Il est interdit de réouvrir un exercice clôturé');

		$db = DB::getInstance();

		$this->assert($this->id_chart !== null);

		if ($this->exists()) {
			$this->assert(
				!$db->test(Transaction::TABLE, 'id_year = ? AND date < ?', $this->id(), $this->start_date->format('Y-m-d')),
				'Des mouvements de cet exercice ont une date antérieure à la date de début de l\'exercice.'
			);

			$this->assert(
				!$db->test(Transaction::TABLE, 'id_year = ? AND date > ?', $this->id(), $this->end_date->format('Y-m-d')),
				'Des mouvements de cet exercice ont une date postérieure à la date de fin de l\'exercice.'
			);
		}
	}

	public function close(int $user_id): void
	{
		if ($this->closed) {
			throw new \LogicException('Cet exercice est déjà clôturé');
		}

		$this->set('closed', 1);
		$this->save();
	}

	public function delete(): bool
	{
		// Manual delete of transactions, as there is a voluntary safeguard in SQL: no cascade
		DB::getInstance()->preparedQuery('DELETE FROM acc_transactions WHERE id_year = ?;', $this->id());

		// Clean up files
		Fichiers::deleteUnlinkedFiles();

		return parent::delete();
	}

	public function countTransactions(): int
	{
		$db = DB::getInstance();
		return $db->count(Transaction::TABLE, $db->where('id_year', $this->id()));
	}

	public function chart()
	{
		return EntityManager::findOneById(Chart::class, $this->id_chart);
	}

	public function accounts()
	{
		return new Accounts($this->id_chart);
	}
}

Added src/include/lib/Garradin/Entities/Services/Fee.php version [6f825919cc].























































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<?php

namespace Garradin\Entities\Services;

use Garradin\Config;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Year;
use KD2\DB\EntityManager;

class Fee extends Entity
{
	const TABLE = 'services_fees';

	protected $id;
	protected $label;
	protected $description;
	protected $amount;
	protected $formula;
	protected $id_service;
	protected $id_account;
	protected $id_year;

	protected $_types = [
		'id'          => 'int',
		'label'       => 'string',
		'description' => '?string',
		'amount'      => '?int',
		'formula'     => '?string',
		'id_service'  => 'int',
		'id_account'  => '?int',
		'id_year'     => '?int',
	];

	public function filterUserValue(string $type, $value, string $key)
	{
		if ($key == 'amount' && $value !== null) {
			$value = Utils::moneyToInteger($value);
		}

		return $value;
	}

	public function importForm(array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (isset($source['account']) && is_array($source['account'])) {
			$source['id_account'] = (int)key($source['account']);
		}

		if (isset($source['amount_type'])) {
			if ($source['amount_type'] == 2) {
				$source['amount'] = null;
			}
			elseif ($source['amount_type'] == 1) {
				$source['formula'] = null;
			}
			else {
				$source['amount'] = $source['formula'] = null;
			}
		}

		if (empty($source['accounting'])) {
			$source['id_account'] = $source['id_year'] = null;
		}

		return parent::importForm($source);
	}

	public function selfCheck(): void
	{
		$db = DB::getInstance();
		parent::selfCheck();

		$this->assert(trim($this->label) !== '', 'Le libellé doit être renseigné');
		$this->assert(strlen($this->label) <= 200, 'Le libellé doit faire moins de 200 caractères');
		$this->assert(strlen($this->description) <= 2000, 'La description doit faire moins de 2000 caractères');
		$this->assert(null === $this->amount || $this->amount > 0, 'Le montant est invalide : ' . $this->amount);
		$this->assert($this->id_service, 'Aucun service n\'a été indiqué pour ce tarif.');
		$this->assert((null === $this->id_account && null === $this->id_year)
			|| (null !== $this->id_account && null !== $this->id_year), 'Le compte doit être indiqué avec l\'exercice');
		$this->assert(null === $this->id_account || $db->test(Account::TABLE, 'id = ?', $this->id_account), 'Le compte indiqué n\'existe pas');
		$this->assert(null === $this->id_year || $db->test(Year::TABLE, 'id = ?', $this->id_year), 'L\'exercice indiqué n\'existe pas');
		$this->assert(null === $this->formula || $this->checkFormula(), 'Formule de calcul invalide');
		$this->assert(null === $this->amount || null === $this->formula, 'Il n\'est pas possible de spécifier à la fois une formule et un montant');
	}

	public function getAmountForUser(int $user_id): ?int
	{
		if ($this->amount) {
			return $this->amount;
		}
		elseif ($this->formula) {
			$db = DB::getInstance();
			return (int) $db->firstColumn($this->getFormulaSQL(), $user_id);
		}

		return null;
	}

	protected function getFormulaSQL()
	{
		return sprintf('SELECT %s FROM membres WHERE id = ?;', $this->formula);
	}

	protected function checkFormula()
	{
		try {
			$db = DB::getInstance();
			$sql = $this->getFormulaSQL();
			$db->protectSelect(['membres' => null], $sql);
			return true;
		}
		catch (\Exception $e) {
			throw $e;
			return false;
		}
	}

	public function service()
	{
		return EntityManager::findOneById(Service::class, $this->id_service);
	}

	public function paidUsersList(): DynamicList
	{
		$identity = Config::getInstance()->get('champ_identite');
		$columns = [
			'id_user' => [
				'select' => 'su.id_user',
			],
			'identity' => [
				'label' => 'Membre',
				'select' => 'm.' . $identity,
				'order' => sprintf('transliterate_to_ascii(m.%s) COLLATE NOCASE', $identity),
			],
			'paid' => [
				'label' => 'Payé ?',
				'select' => 'su.paid',
			],
			'paid_amount' => [
				'label' => 'Montant payé',
				'select' => 'SUM(l.credit)',
			],
			'date' => [
				'label' => 'Date',
				'select' => 'su.date',
			],
		];

		$tables = 'services_users su
			INNER JOIN membres m ON m.id = su.id_user
			INNER JOIN services_fees sf ON sf.id = su.id_fee
			LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id
			LEFT JOIN acc_transactions_lines l ON l.id_transaction = tu.id_transaction';
		$conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND su.expiry_date >= date()', $this->id());

		$list = new DynamicList($columns, $tables, $conditions);
		$list->groupBy('su.id_user');
		$list->orderBy('date', true);
		$list->setCount('COUNT(DISTINCT su.id_user)');
		return $list;
	}

	public function unpaidUsersList(): DynamicList
	{
		$list = $this->paidUsersList();
		$conditions = sprintf('su.id_fee = %d AND su.paid = 0', $this->id());
		$list->setConditions($conditions);
		return $list;
	}

	public function expiredUsersList(): DynamicList
	{
		$list = $this->paidUsersList();
		$conditions = sprintf('su.id_fee = %d AND su.expiry_date < date()', $this->id());
		$list->setConditions($conditions);
		return $list;
	}
}

Added src/include/lib/Garradin/Entities/Services/Reminder.php version [308065935f].





































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

namespace Garradin\Entities\Services;

use Garradin\DynamicList;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Config;
use KD2\DB\EntityManager;

class Reminder extends Entity
{
	const TABLE = 'services_reminders';

	protected $id;
	protected $id_service;
	protected $delay;
	protected $subject;
	protected $body;

	protected $_types = [
		'id'         => 'int',
		'id_service' => 'int',
		'delay'      => 'int',
		'subject'    => 'string',
		'body'       => 'string',
	];

	public function selfCheck(): void
	{
		parent::selfCheck();
		$this->assert($this->id_service, 'Aucun service n\'a été indiqué pour ce tarif.');
		$this->assert(trim($this->subject) !== '', 'Le sujet doit être renseigné');
		$this->assert(strlen($this->subject) <= 200, 'Le sujet doit faire moins de 200 caractères');
		$this->assert(trim($this->body) !== '', 'Le corps du message doit être renseigné');
		$this->assert(strlen($this->body) <= 64000, 'Le corps du message doit faire moins de 64.000 caractères');
		$this->assert($this->delay !== null, 'Le délai de rappel doit être renseigné');
	}

	public function service()
	{
		return EntityManager::findOneById(Service::class, $this->id_service);
	}

	public function importForm(array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}


		if (isset($source['delay_type'])) {
			if (1 == $source['delay_type'] && !empty($source['delay_before'])) {
				$source['delay'] = (int)$source['delay_before'] * -1;
			}
			elseif (2 == $source['delay_type'] && !empty($source['delay_after'])) {
				$source['delay'] = (int)$source['delay_after'];
			}
			else {
				$source['delay'] = 0;
			}
		}

		parent::importForm($source);
	}

	public function sentList(): DynamicList
	{
		$identity = Config::getInstance()->get('champ_identite');
		$columns = [
			'id_user' => [
				'select' => 'srs.id_user',
			],
			'identity' => [
				'label' => 'Membre',
				'select' => 'm.' . $identity,
				'order' => sprintf('transliterate_to_ascii(m.%s) COLLATE NOCASE', $identity),
			],
			'email' => [
				'label' => 'Adresse e-mail',
				'select' => 'm.email',
			],
			'date' => [
				'label' => 'Date',
				'select' => 'srs.date',
			],
		];

		$tables = 'services_reminders_sent srs
			INNER JOIN membres m ON m.id = srs.id_user';
		$conditions = sprintf('srs.id_reminder = %d', $this->id());

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', true);
		return $list;
	}

}

Added src/include/lib/Garradin/Entities/Services/Service.php version [5fa29d7db5].













































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php

namespace Garradin\Entities\Services;

use Garradin\Config;
use Garradin\DynamicList;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;
use Garradin\Services\Fees;

class Service extends Entity
{
	const TABLE = 'services';

	protected $id;
	protected $label;
	protected $description;
	protected $duration;
	protected $start_date;
	protected $end_date;

	protected $_types = [
		'id'          => 'int',
		'label'       => 'string',
		'description' => '?string',
		'duration'    => '?int',
		'start_date'  => '?date',
		'end_date'    => '?date',
	];

	public function selfCheck(): void
	{
		parent::selfCheck();
		$this->assert(trim($this->label) !== '', 'Le libellé doit être renseigné');
		$this->assert(strlen($this->label) <= 200, 'Le libellé doit faire moins de 200 caractères');
		$this->assert(strlen($this->description) <= 2000, 'La description doit faire moins de 2000 caractères');
		$this->assert(!isset($this->duration, $this->start_date, $this->end_date) || $this->duration || ($this->start_date && $this->end_date), 'Seulement une option doit être choisie : durée ou dates de début et de fin de validité');
		$this->assert(null === $this->start_date || $this->start_date instanceof \DateTimeInterface);
		$this->assert(null === $this->end_date || $this->end_date instanceof \DateTimeInterface);
		$this->assert(null === $this->duration || (is_int($this->duration) && $this->duration > 0), 'La durée n\'est pas valide');
		$this->assert(null === $this->start_date || $this->end_date > $this->start_date, 'La date de fin de validité doit être après la date de début');
	}

	public function importForm(?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (isset($source['period'])) {
			if (1 == $source['period']) {
				$source['start_date'] = $source['end_date'] = null;
			}
			elseif (2 == $source['period']) {
				$source['duration'] = null;
			}
			else {
				$source['duration'] = $source['start_date'] = $source['end_date'] = null;
			}
		}

		parent::importForm($source);
	}

	public function fees()
	{
		return new Fees($this->id());
	}

	public function paidUsersList(): DynamicList
	{
		$identity = Config::getInstance()->get('champ_identite');
		$columns = [
			'id_user' => [
			],
			'end_date' => [
			],
			'identity' => [
				'label' => 'Membre',
				'select' => 'm.' . $identity,
				'order' => sprintf('transliterate_to_ascii(m.%s) COLLATE NOCASE', $identity),
			],
			'status' => [
				'label' => 'Statut',
				'select' => 'CASE WHEN su.expiry_date < date() THEN -1 WHEN su.expiry_date >= date() THEN 1 ELSE 0 END',
			],
			'paid' => [
				'label' => 'Payé ?',
				'select' => 'su.paid',
			],
			'expiry' => [
				'label' => 'Date d\'expiration',
				'select' => 'MAX(su.expiry_date)',
			],
			'fee' => [
				'label' => 'Tarif',
				'select' => 'sf.label',
			],
			'date' => [
				'label' => 'Date d\'inscription',
				'select' => 'su.date',
			],
		];

		$tables = 'services_users su
			INNER JOIN membres m ON m.id = su.id_user
			INNER JOIN services s ON s.id = su.id_service
			INNER JOIN services_fees sf ON sf.id = su.id_fee';
		$conditions = sprintf('su.id_service = %d AND su.paid = 1 AND su.expiry_date >= date()', $this->id());

		$list = new DynamicList($columns, $tables, $conditions);
		$list->groupBy('su.id_user');
		$list->orderBy('date', true);
		$list->setCount('COUNT(DISTINCT su.id_user)');
		return $list;
	}

	public function unpaidUsersList(): DynamicList
	{
		$list = $this->paidUsersList();
		$conditions = sprintf('su.id_service = %d AND su.paid = 0', $this->id());
		$list->setConditions($conditions);
		return $list;
	}

	public function expiredUsersList(): DynamicList
	{
		$list = $this->paidUsersList();
		$conditions = sprintf('su.id_service = %d AND su.expiry_date < date()', $this->id());
		$list->setConditions($conditions);
		return $list;
	}
}

Added src/include/lib/Garradin/Entities/Services/Service_User.php version [d694479106].































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php

namespace Garradin\Entities\Services;

use Garradin\DB;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Services\Fees;
use Garradin\Services\Services;
use Garradin\Entities\Accounting\Transaction;

class Service_User extends Entity
{
	const TABLE = 'services_users';

	protected $id;
	protected $id_user;
	protected $id_service;
	protected $id_fee;
	protected $paid;
	protected $expected_amount;
	protected $date;
	protected $expiry_date;

	protected $_types = [
		'id'          => 'int',
		'id_user'     => 'int',
		'id_service'  => 'int',
		'id_fee'      => '?int',
		'paid'        => 'bool',
		'expected_amount' => '?int',
		'date'        => 'date',
		'expiry_date' => '?date',
	];

	protected $_service, $_fee;

	public function selfCheck(): void
	{
		$this->paid = (bool) $this->paid;
		$this->assert(!$this->exists() && !DB::getInstance()->test(self::TABLE, 'id_user = ? AND id_service = ? AND date = ?', $this->id_user, $this->id_service, $this->date->format('Y-m-d')), 'Cette activité a déjà été enregistrée pour ce membre et cette date');
	}

	public function importForm(?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (!empty($source['id_service']) && empty($source['expiry_date'])) {
			$service = $this->_service = Services::get((int) $source['id_service']);

			if (!$service) {
				throw new \LogicException('The requested service is not found');
			}

			if ($service->duration) {
				$dt = new \DateTime;
				$dt->modify(sprintf('+%d days', $service->duration));
				$this->expiry_date = $dt;
			}
			elseif ($service->end_date) {
				$this->expiry_date = $service->end_date;
			}
			else {
				$this->expiry_date = null;
			}
		}

		return parent::importForm($source);
	}

	public function service()
	{
		if (null === $this->_service) {
			$this->_service = Services::get($this->id_service);
		}

		return $this->_service;
	}

	public function fee()
	{
		if (null === $this->_fee) {
			$this->_fee = Fees::get($this->id_fee);
		}

		return $this->_fee;
	}

	public function addPayment(int $user_id, ?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		$transaction = new Transaction;
		$transaction->id_creator = $user_id;
		$transaction->id_year = $this->fee()->id_year;

		$source['type'] = Transaction::TYPE_REVENUE;
		$key = sprintf('account_%d_', $source['type']);
		$source[$key . '0'] = [$this->fee()->id_account => ''];
		$source[$key . '1'] = isset($source['account']) ? $source['account'] : null;

		$source['label'] = 'Règlement activité - ' . $this->service()->label . ' - ' . $this->fee()->label;
		$source['date'] = $this->date->format('d/m/Y');

		$transaction->importFromNewForm($source);
		$transaction->save();
		$transaction->linkToUser($this->id_user, $this->id());

		return $transaction;
	}

	static public function saveFromForm(int $user_id, ?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		$db = DB::getInstance();
		$db->begin();

		$su = new self;
		$su->date = new \DateTime;
		$su->importForm($source);

		if ($su->id_fee && $su->fee()->id_account && $su->id_user) {
			$su->expected_amount = $su->fee()->getAmountForUser($su->id_user);
		}

		$su->save();

		if ($su->id_fee && $su->fee()->id_account && !empty($source['amount'])) {
			$su->addPayment($user_id, $source);
		}

		$db->commit();

		return $su;
	}
}

Added src/include/lib/Garradin/Entity.php version [de03b9076a].



















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php

namespace Garradin;

use Garradin\Form;
use KD2\DB\AbstractEntity;

class Entity extends AbstractEntity
{
	protected $_form_rules = [];

	/**
	 * Valider les champs avant enregistrement
	 * @throws ValidationException Si une erreur de validation survient
	 */
	public function importForm(array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		$form = new Form;

		if (!$form->validate($this->_form_rules, $source))
		{
			$messages = $form->getErrorMessages();

			throw new ValidationException(implode("\n", $messages));
		}

		return $this->import($source);
	}

	protected function filterUserValue(string $type, $value, string $key)
	{
		if ($type == 'date') {
			if (!trim($value)) {
				return null;
			}

			if (preg_match('!^\d{2}/\d{2}/\d{2}$!', $value)) {
				return \DateTime::createFromFormat('d/m/y', $value);
			}
			elseif (preg_match('!^\d{2}/\d{2}/\d{4}$!', $value)) {
				return \DateTime::createFromFormat('d/m/Y', $value);
			}
			elseif (null !== $value) {
				throw new ValidationException('Format de date invalide (merci d\'utiliser le format JJ/MM/AAAA) : ' . $value);
			}
		}
		else {
			return parent::filterUserValue($type, $value, $key);
		}
	}

	protected function assert(bool $test, string $message = null): void
	{
		if ($test) {
			return;
		}

		if (null === $message) {
			$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
			$caller_class = array_pop($backtrace);
			$caller = array_pop($backtrace);
			$message = sprintf('Entity assertion fail from class %s on line %d', $caller_class['class'], $caller['line']);
			throw new \UnexpectedValueException($message);
		}
		else {
			throw new ValidationException($message);
		}
	}
}

Modified src/include/lib/Garradin/Fichiers.php from [b62209a8ff] to [76103311d2].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43


44
45
46
47
48
49
50
<?php

namespace Garradin;

use KD2\Image;
use Garradin\Membres\Session;

class Fichiers
{
	public $id;
	public $nom;
	public $type;
	public $image;
	public $datetime;
	public $hash;
	public $taille;
	public $id_contenu;

	/**
	 * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes
	 * @var array
	 */
	protected static $allowed_thumb_sizes = [200, 500];

	const LIEN_COMPTA = 'compta_journal';
	const LIEN_WIKI = 'wiki_pages';
	const LIEN_MEMBRES = 'membres';

	/**
	 * Renvoie l'URL vers un fichier
	 * @param  integer $id   Numéro du fichier
	 * @param  string  $nom  Nom de fichier avec extension
	 * @param  integer $size Taille de la miniature désirée (pour les images)
	 * @return string        URL du fichier
	 */
	static public function _getURL($id, $nom, $size = false)
	{
		$url = WWW_URL . 'f/' . base_convert((int)$id, 10, 36) . '/' . $nom;

		if ($size)
		{
			$url .= '?' . self::_findThumbSize($size) . 'px';
		}



		return $url;
	}

	/**
	 * Renvoie la taille de miniature la plus proche de la taille demandée
	 * @param  integer $size Taille demandée




|



















|










|

|



|

>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace Garradin;

use KD2\Graphics\Image;
use Garradin\Membres\Session;

class Fichiers
{
	public $id;
	public $nom;
	public $type;
	public $image;
	public $datetime;
	public $hash;
	public $taille;
	public $id_contenu;

	/**
	 * Tailles de miniatures autorisées, pour ne pas avoir 500 fichiers générés avec 500 tailles différentes
	 * @var array
	 */
	protected static $allowed_thumb_sizes = [200, 500];

	const LIEN_COMPTA = 'acc_transactions';
	const LIEN_WIKI = 'wiki_pages';
	const LIEN_MEMBRES = 'membres';

	/**
	 * Renvoie l'URL vers un fichier
	 * @param  integer $id   Numéro du fichier
	 * @param  string  $nom  Nom de fichier avec extension
	 * @param  integer $size Taille de la miniature désirée (pour les images)
	 * @return string        URL du fichier
	 */
	static public function _getURL(int $id, string $nom, string $hash, $size = false): string
	{
		$url = sprintf('%sf/%s/%s?', WWW_URL, base_convert((int)$id, 10, 36), $nom);

		if ($size)
		{
			$url .= self::_findThumbSize($size) . 'px&';
		}

		$url .= substr($hash, 0, 10);

		return $url;
	}

	/**
	 * Renvoie la taille de miniature la plus proche de la taille demandée
	 * @param  integer $size Taille demandée
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

		return max(self::$allowed_thumb_sizes);
	}

	/**
	 * Constructeur de l'objet pour un fichier
	 * @param integer $id Numéro unique du fichier

	 */
	public function __construct($id, $data = null)
	{
		if (is_null($data))
		{
			$data = DB::getInstance()->first('SELECT fichiers.*, fc.hash, fc.taille,
				strftime(\'%s\', datetime) AS datetime
				FROM fichiers INNER JOIN fichiers_contenu AS fc ON fc.id = fichiers.id_contenu
				WHERE fichiers.id = ?;', (int)$id);
		}

		if (!$data)
		{
			throw new \InvalidArgumentException('Ce fichier n\'existe pas.');
		}

		foreach ($data as $key=>$value)
		{
			$this->$key = $value;
		}
	}

	/**
	 * Renvoie l'adresse d'accès au fichier
	 * @param  boolean $size Taille éventuelle de la miniature demandée
	 * @return string        URL d'accès au fichier
	 */
	public function getURL($size = false)
	{
		return self::_getURL($this->id, $this->nom, $size);
	}

	/**
	 * Lier un fichier à un contenu
	 * @param  string $type       Type de contenu (constantes LIEN_*)
	 * @param  integer $foreign_id ID du contenu lié
	 * @return boolean TRUE en cas de succès







>

|














|












|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

		return max(self::$allowed_thumb_sizes);
	}

	/**
	 * Constructeur de l'objet pour un fichier
	 * @param integer $id Numéro unique du fichier
	 * @param $data array|object File data to populate object
	 */
	public function __construct(int $id, $data = null)
	{
		if (is_null($data))
		{
			$data = DB::getInstance()->first('SELECT fichiers.*, fc.hash, fc.taille,
				strftime(\'%s\', datetime) AS datetime
				FROM fichiers INNER JOIN fichiers_contenu AS fc ON fc.id = fichiers.id_contenu
				WHERE fichiers.id = ?;', (int)$id);
		}

		if (!$data)
		{
			throw new \InvalidArgumentException('Ce fichier n\'existe pas.');
		}

		foreach ((array)$data as $key => $value)
		{
			$this->$key = $value;
		}
	}

	/**
	 * Renvoie l'adresse d'accès au fichier
	 * @param  boolean $size Taille éventuelle de la miniature demandée
	 * @return string        URL d'accès au fichier
	 */
	public function getURL($size = false)
	{
		return self::_getURL($this->id, $this->nom, $this->hash, $size);
	}

	/**
	 * Lier un fichier à un contenu
	 * @param  string $type       Type de contenu (constantes LIEN_*)
	 * @param  integer $foreign_id ID du contenu lié
	 * @return boolean TRUE en cas de succès
142
143
144
145
146
147
148



149


150

151
152


153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
















177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195


196

197

198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221




222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
			throw new \LogicException('Ce fichier est déjà lié à un autre contenu : ' . $check_type);
		}

		return $db->preparedQuery('INSERT OR IGNORE INTO fichiers_' . $type . ' (fichier, id) VALUES (?, ?);',
			[(int)$this->id, (int)$foreign_id]);
	}




	/**


	 * Vérifie que l'utilisateur a bien le droit d'accéder à ce fichier

	 * @param  mixed   $user Tableau contenant les infos sur l'utilisateur connecté, provenant de Session::getUser, ou false
	 * @return boolean       TRUE si l'utilisateur a le droit d'accéder au fichier, sinon FALSE


	 */
	public function checkAccess(Session $session)
	{
		$config = Config::getInstance();

		if ($config->get('image_fond') == $this->id)
		{
			return true;
		}

		$db = DB::getInstance();

		// On regarde déjà si le fichier n'est pas lié au wiki
		$query = sprintf('SELECT wp.droit_lecture FROM fichiers_%s AS link
			INNER JOIN wiki_pages AS wp ON wp.id = link.id
			WHERE link.fichier = ? LIMIT 1;', self::LIEN_WIKI);
		$wiki = $db->firstColumn($query, (int)$this->id);

		// Page wiki publique, aucune vérification à faire, seul cas d'accès à un fichier en dehors de l'espace admin
		if ($wiki !== false && $wiki == Wiki::LECTURE_PUBLIC)
		{
			return true;
		}
			
















		// Pas d'utilisateur connecté, pas d'accès aux fichiers de l'espace admin
		if (!$session->isLogged())
		{
			return false;
		}

		$user = $session->getUser();

		if ($wiki !== false)
		{
			// S'il n'a même pas droit à accéder au wiki c'est mort
			if (!$session->canAccess('wiki', Membres::DROIT_ACCES))
			{
				return false;
			}

			// On renvoie à l'objet Wiki pour savoir si l'utilisateur a le droit de lire ce fichier
			$_w = new Wiki;
			$_w->setRestrictionCategorie($user->id_categorie, $user->droit_wiki);


			return $_w->canReadPage($wiki);

		}


		// On regarde maintenant si le fichier est lié à la compta
		$query = sprintf('SELECT 1 FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_COMPTA);
		$compta = $db->firstColumn($query, (int)$this->id);

		if ($compta && $session->canAccess('compta', Membres::DROIT_ACCES))
		{
			// OK si accès à la compta
			return true;
		}

		// Enfin, si le fichier est lié à un membre
		$query = sprintf('SELECT id FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_MEMBRES);
		$membre = $db->firstColumn($query, (int)$this->id);

		if ($membre !== false)
		{
			// De manière évidente, l'utilisateur a le droit d'accéder aux fichiers liés à son profil
			if ((int)$membre == $user->id)
			{
				return true;
			}

			// Pour voir les fichiers des membres il faut pouvoir les gérer




			if ($session->canAccess('membres', Membres::DROIT_ECRITURE))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Supprime le fichier
	 * @return boolean TRUE en cas de succès
	 */
	public function remove()
	{
		$db = DB::getInstance();
		$db->begin();
		$db->delete('fichiers_compta_journal', 'fichier = ?', (int)$this->id);
		$db->delete('fichiers_wiki_pages', 'fichier = ?', (int)$this->id);
		$db->delete('fichiers_membres', 'fichier = ?', (int)$this->id);

		$db->delete('fichiers', 'id = ?', (int)$this->id);

		// Suppression du contenu s'il n'est pas utilisé par un autre fichier
		if (!$db->firstColumn('SELECT 1 FROM fichiers WHERE id_contenu = ? AND id != ? LIMIT 1;', 
			(int)$this->id_contenu, (int)$this->id))
		{







>
>
>
|
>
>
|
>
|
|
>
>
|
|








<
<




|






|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



















>
>
|
>
|
>





|


|















>
>
>
>
|
















|
|
|







145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
			throw new \LogicException('Ce fichier est déjà lié à un autre contenu : ' . $check_type);
		}

		return $db->preparedQuery('INSERT OR IGNORE INTO fichiers_' . $type . ' (fichier, id) VALUES (?, ?);',
			[(int)$this->id, (int)$foreign_id]);
	}

	public function getLinkedId(string $type)
	{
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}


		return DB::getInstance()->firstColumn(sprintf('SELECT id FROM fichiers_%s WHERE fichier = %d;', $type, $this->id));
	}

	public function isPublic(&$wiki = null): bool
	{
		$config = Config::getInstance();

		if ($config->get('image_fond') == $this->id)
		{
			return true;
		}



		// On regarde déjà si le fichier n'est pas lié au wiki
		$query = sprintf('SELECT wp.droit_lecture FROM fichiers_%s AS link
			INNER JOIN wiki_pages AS wp ON wp.id = link.id
			WHERE link.fichier = ? LIMIT 1;', self::LIEN_WIKI);
		$wiki = DB::getInstance()->firstColumn($query, (int)$this->id);

		// Page wiki publique, aucune vérification à faire, seul cas d'accès à un fichier en dehors de l'espace admin
		if ($wiki !== false && $wiki == Wiki::LECTURE_PUBLIC)
		{
			return true;
		}

		return false;
	}

	/**
	 * Vérifie que l'utilisateur a bien le droit d'accéder à ce fichier
	 * @param  mixed   $user Tableau contenant les infos sur l'utilisateur connecté, provenant de Session::getUser, ou false
	 * @return boolean       TRUE si l'utilisateur a le droit d'accéder au fichier, sinon FALSE
	 */
	public function checkAccess(Session $session, bool $require_admin = false)
	{
		$wiki = null;

		if ($this->isPublic($wiki) && !$require_admin) {
			return true;
		}

		// Pas d'utilisateur connecté, pas d'accès aux fichiers de l'espace admin
		if (!$session->isLogged())
		{
			return false;
		}

		$user = $session->getUser();

		if ($wiki !== false)
		{
			// S'il n'a même pas droit à accéder au wiki c'est mort
			if (!$session->canAccess('wiki', Membres::DROIT_ACCES))
			{
				return false;
			}

			// On renvoie à l'objet Wiki pour savoir si l'utilisateur a le droit de lire ce fichier
			$_w = new Wiki;
			$_w->setRestrictionCategorie($user->id_categorie, $user->droit_wiki);
			return $require_admin ? $_w->canWritePage($wiki) : $_w->canReadPage($wiki);
		}

		$level = $require_admin ? Membres::DROIT_ADMIN : Membres::DROIT_ACCES;

		$db = DB::getInstance();

		// On regarde maintenant si le fichier est lié à la compta
		$query = sprintf('SELECT 1 FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_COMPTA);
		$compta = $db->firstColumn($query, (int)$this->id);

		if ($compta)
		{
			// OK si accès à la compta
			return $session->canAccess('compta', $level);
		}

		// Enfin, si le fichier est lié à un membre
		$query = sprintf('SELECT id FROM fichiers_%s WHERE fichier = ? LIMIT 1;', self::LIEN_MEMBRES);
		$membre = $db->firstColumn($query, (int)$this->id);

		if ($membre !== false)
		{
			// De manière évidente, l'utilisateur a le droit d'accéder aux fichiers liés à son profil
			if ((int)$membre == $user->id)
			{
				return true;
			}

			// Pour voir les fichiers des membres il faut pouvoir les gérer
			if ($level == Membres::DROIT_ACCES) {
				$level = Membres::DROIT_ECRITURE;
			}

			if ($session->canAccess('membres', $level))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Supprime le fichier
	 * @return boolean TRUE en cas de succès
	 */
	public function remove()
	{
		$db = DB::getInstance();
		$db->begin();
		$db->delete('fichiers_' . self::LIEN_COMPTA, 'fichier = ?', (int)$this->id);
		$db->delete('fichiers_' . self::LIEN_WIKI, 'fichier = ?', (int)$this->id);
		$db->delete('fichiers_' . self::LIEN_MEMBRES, 'fichier = ?', (int)$this->id);

		$db->delete('fichiers', 'id = ?', (int)$this->id);

		// Suppression du contenu s'il n'est pas utilisé par un autre fichier
		if (!$db->firstColumn('SELECT 1 FROM fichiers WHERE id_contenu = ? AND id != ? LIMIT 1;', 
			(int)$this->id_contenu, (int)$this->id))
		{
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302





303
304
305
306
307
308
309

	/**
	 * Envoie le fichier au client HTTP
	 * @return void
	 */
	public function serve()
	{
		return $this->_serve($this->getFilePathFromCache(), $this->type, ($this->image ? false : $this->nom), $this->taille);
	}

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 * @return void
	 */
	public function serveThumbnail($width = self::TAILLE_MINIATURE)
	{
		if (!$this->image)
		{
			throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.');
		}






		if (!in_array($width, self::$allowed_thumb_sizes))
		{
			throw new UserException('Cette taille de miniature n\'est pas autorisée.');
		}

		$cache_id = 'fichiers.' . $this->id_contenu . '.thumb.' . (int)$width;







|






|





>
>
>
>
>







316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347

	/**
	 * Envoie le fichier au client HTTP
	 * @return void
	 */
	public function serve()
	{
		return $this->_serve($this->getFilePathFromCache(), $this->type, ($this->image ? false : $this->nom), $this->taille, $this->isPublic());
	}

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 * @return void
	 */
	public function serveThumbnail($width = null)
	{
		if (!$this->image)
		{
			throw new UserException('Il n\'est pas possible de fournir une miniature pour un fichier qui n\'est pas une image.');
		}

		if (!$width)
		{
			$width = reset(self::$allowed_thumb_sizes);
		}

		if (!in_array($width, self::$allowed_thumb_sizes))
		{
			throw new UserException('Cette taille de miniature n\'est pas autorisée.');
		}

		$cache_id = 'fichiers.' . $this->id_contenu . '.thumb.' . (int)$width;
329
330
331
332
333
334
335
336
337




338
339
340
341

342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
	 * Servir un fichier local en HTTP
	 * @param  string $path Chemin vers le fichier local
	 * @param  string $type Type MIME du fichier
	 * @param  string $name Nom du fichier avec extension
	 * @param  integer $size Taille du fichier en octets (facultatif)
	 * @return boolean TRUE en cas de succès
	 */
	protected function _serve($path, $type, $name = false, $size = null)
	{




		// Désactiver le cache
		header('Pragma: public');
		header('Expires: -1');
		header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0');


		header('Content-Type: '.$type);

		if ($name)
		{
			header('Content-Disposition: attachment; filename="' . $name . '"');
		}
		
		// Utilisation de XSendFile si disponible
		if (ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE']))
		{
			if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') 
				&& function_exists('apache_get_modules') 
				&& in_array('mod_xsendfile', apache_get_modules()))
			{







|

>
>
>
>
|
|
|
|
>







|







367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
	 * Servir un fichier local en HTTP
	 * @param  string $path Chemin vers le fichier local
	 * @param  string $type Type MIME du fichier
	 * @param  string $name Nom du fichier avec extension
	 * @param  integer $size Taille du fichier en octets (facultatif)
	 * @return boolean TRUE en cas de succès
	 */
	protected function _serve($path, $type, $name = false, $size = null, bool $public = false)
	{
		if ($public) {
			Utils::HTTPCache($this->hash, $this->datetime);
		}
		else {
			// Désactiver le cache
			header('Pragma: public');
			header('Expires: -1');
			header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0');
		}

		header('Content-Type: '.$type);

		if ($name)
		{
			header('Content-Disposition: attachment; filename="' . $name . '"');
		}

		// Utilisation de XSendFile si disponible
		if (ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE']))
		{
			if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') 
				&& function_exists('apache_get_modules') 
				&& in_array('mod_xsendfile', apache_get_modules()))
			{
373
374
375
376
377
378
379

380


381
382
383
384
385
386
387
		@ini_set('zlib.output_compression', 'Off');

		if ($size)
		{
			header('Content-Length: '. (int)$size);
		}


		ob_clean();


		flush();

		// Sinon on envoie le fichier à la mano
		return readfile($path);
	}

	/**







>
|
>
>







416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
		@ini_set('zlib.output_compression', 'Off');

		if ($size)
		{
			header('Content-Length: '. (int)$size);
		}

		if (@ob_get_length()) {
			@ob_clean();
		}

		flush();

		// Sinon on envoie le fichier à la mano
		return readfile($path);
	}

	/**
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
		}

		if (!is_uploaded_file($file['tmp_name']))
		{
			throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.');
		}

		$max_blob_size = self::getMaxBlobSize();

		// Vérifier que le fichier peut rentrer en base de données (dans PHP < 7.2 on n'utilise pas openBlob)
		if (null !== $max_blob_size && $file['size'] > $max_blob_size) {
			unlink($file['tmp_name']);
			throw new UserException('Taille du fichier supérieure au maximum autorisé en base de données');
		}

		$name = preg_replace('/\s+/', '_', $file['name']);
		$name = preg_replace('/[^\d\w._-]/ui', '', $name);

		return self::storeFile($name, $file['tmp_name']);
	}

    /**
     * Returns the maximum value size that can be handled by a bindValue
     * @return null|integer
     */
	static public function getMaxBlobSize()
	{
        $memory_limit = Utils::return_bytes(ini_get('memory_limit'));

        if (!$memory_limit) {
            return null;
        }

        return round(($memory_limit - memory_get_usage()) * 0.9);
	}

	/**
	 * Upload de fichier à partir d'une chaîne en base64
	 * @param  string $name
	 * @param  string $content
	 * @return Fichiers
	 */
	static public function storeFromBase64($name, $content)







<
<
<
<
<
<
<
<






<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







508
509
510
511
512
513
514








515
516
517
518
519
520















521
522
523
524
525
526
527
		}

		if (!is_uploaded_file($file['tmp_name']))
		{
			throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.');
		}









		$name = preg_replace('/\s+/', '_', $file['name']);
		$name = preg_replace('/[^\d\w._-]/ui', '', $name);

		return self::storeFile($name, $file['tmp_name']);
	}
















	/**
	 * Upload de fichier à partir d'une chaîne en base64
	 * @param  string $name
	 * @param  string $content
	 * @return Fichiers
	 */
	static public function storeFromBase64($name, $content)
547
548
549
550
551
552
553
554








555
556
557
558
559




560
561
562
563
564
565
566
567
568
569



570

571
572
573
574
575
576
577
578


579
580

581
582
583
584
585
586
587
		// Check that it's a real image
		if ($is_image) {
			try {
				if ($path && !$content) {
					$i = new Image($path);
				}
				else {
					$i = Image::createFromBlob($bytes);








				}

				unset($i);
			}
			catch (\RuntimeException $e) {




				throw new UserException('Fichier image invalide');
			}
		}

		$db = DB::getInstance();

		$db->begin();

		// Il peut arriver que l'on renvoie ici un fichier déjà stocké, auquel cas, ne pas le re-stocker
		if (!($id_contenu = $db->firstColumn('SELECT id FROM fichiers_contenu WHERE hash = ?;', $hash)))



		{

			$db->insert('fichiers_contenu', [
				'hash'		=>	$hash,
				'taille'	=>	(int)$size,
				'contenu'	=>	[\SQLITE3_BLOB, $content ?: file_get_contents($path)],
			]);

			// FIXME: utiliser Sqlite3::openBlob pour écrire quand dispo dans PHP
			// cf. https://github.com/php/php-src/pull/2528



			$id_contenu = $db->lastInsertRowID();

		}

		$db->insert('fichiers', [
			'id_contenu'	=>	(int)$id_contenu,
			'nom'			=>	$name,
			'type'			=>	$type,
			'image'			=>	(int)$is_image,







|
>
>
>
>
>
>
>
>





>
>
>
>









|
>
>
>
|
>
|
|
<
|
|
|
<
<
>
>
|
|
>







570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611

612
613
614


615
616
617
618
619
620
621
622
623
624
625
626
		// Check that it's a real image
		if ($is_image) {
			try {
				if ($path && !$content) {
					$i = new Image($path);
				}
				else {
					$i = Image::createFromBlob($content);
				}

				// Recompress PNG files from base64, assuming they are coming
				// from JS canvas which doesn't know how to gzip (d'oh!)
				if ($i->format() == 'png' && null !== $content) {
					$content = $i->output('png', true);
					$hash = sha1($content);
					$size = strlen($content);
				}

				unset($i);
			}
			catch (\RuntimeException $e) {
				if (strstr($e->getMessage(), 'No suitable image library found')) {
					throw new UserException('Le serveur n\'a aucune bibliothèque de gestion d\'image installée, et ne peut donc pas accepter les images. Installez Imagick ou GD.');
				}

				throw new UserException('Fichier image invalide');
			}
		}

		$db = DB::getInstance();

		$db->begin();

		// Il peut arriver que l'on renvoie ici un fichier déjà stocké, auquel cas, ne pas le re-stocker
		if (!($id_contenu = $db->firstColumn('SELECT id FROM fichiers_contenu WHERE hash = ?;', $hash))) {
			$db->preparedQuery('INSERT INTO fichiers_contenu (hash, taille, contenu) VALUES (?, ?, zeroblob(?));',
				[$hash, (int)$size, (int)$size]);
			$id_contenu = $db->lastInsertRowID();

			// Écrire le contenu
			$blob = $db->openBlob('fichiers_contenu', 'contenu', $id_contenu, 'main', SQLITE3_OPEN_READWRITE);


			if (null !== $content) {
				fwrite($blob, $content);
			}


			else{
				fwrite($blob, file_get_contents($path));
			}

			fclose($blob);
		}

		$db->insert('fichiers', [
			'id_contenu'	=>	(int)$id_contenu,
			'nom'			=>	$name,
			'type'			=>	$type,
			'image'			=>	(int)$is_image,
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
	}

	/**
	 * Récupère la liste des fichiers liés à une ressource
	 * 
	 * @param  string  $type    Type de ressource
	 * @param  integer $id      Numéro de ressource
	 * @param  boolean $images  TRUE pour retourner seulement les images,
	 * FALSE pour retourner les fichiers sans images, NULL pour tout retourner
	 * @return array          Liste des fichiers
	 */
	static public function listLinkedFiles($type, $id, $images = false)
	{
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}







|



|







662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
	}

	/**
	 * Récupère la liste des fichiers liés à une ressource
	 * 
	 * @param  string  $type    Type de ressource
	 * @param  integer $id      Numéro de ressource
	 * @param  boolean|null $images  TRUE pour retourner seulement les images,
	 * FALSE pour retourner les fichiers sans images, NULL pour tout retourner
	 * @return array          Liste des fichiers
	 */
	static public function listLinkedFiles($type, $id, $images = null)
	{
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}
649
650
651
652
653
654
655
656
657
658
659
660
661





































662
663
664
665
666
667
668
			WHERE fwp.id = ? %s
			ORDER BY fichiers.nom COLLATE NOCASE;', $type, $images);

		$files = DB::getInstance()->get($query, (int)$id);

		foreach ($files as &$file)
		{
			$file->url = self::_getURL($file->id, $file->nom);
			$file->thumb = $file->image ? self::_getURL($file->id, $file->nom, 200) : false;
		}

		return $files;
	}






































	/**
	 * Enlève d'une liste de fichiers ceux qui sont mentionnés dans un texte wiki
	 * @param  array $files Liste de fichiers
	 * @param  string $text  texte wiki
	 * @return array        Un tableau qui ne contient pas les fichiers mentionnés dans $text
	 */







|
|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
			WHERE fwp.id = ? %s
			ORDER BY fichiers.nom COLLATE NOCASE;', $type, $images);

		$files = DB::getInstance()->get($query, (int)$id);

		foreach ($files as &$file)
		{
			$file->url = self::_getURL($file->id, $file->nom, $file->hash);
			$file->thumb = $file->image ? self::_getURL($file->id, $file->nom, $file->hash, 200) : false;
		}

		return $files;
	}

	static public function deleteLinkedFiles($type, int $id)
	{
		static $check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}

		$files = DB::getInstance()->delete('fichiers_' . $type, 'id = ?', $id);
		return self::deleteUnlinkedFiles();
	}

	static public function deleteUnlinkedFiles()
	{
		static $all = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];
		$id_background = Config::getInstance()->get('image_fond');

		$list = DB::getInstance()->iterate(sprintf('SELECT f.id, f.id_contenu FROM fichiers f
			LEFT JOIN fichiers_%s a ON a.fichier = f.id
			LEFT JOIN fichiers_%s b ON b.fichier = f.id
			LEFT JOIN fichiers_%s c ON c.fichier = f.id
			WHERE a.id IS NULL AND b.id IS NULL AND c.id IS NULL;',
			self::LIEN_MEMBRES,
			self::LIEN_WIKI,
			self::LIEN_COMPTA));

		foreach ($list as $file) {
			if ($file->id == $id_background) { // FIXME: want to use something cleaner here!
				continue;
			}

			$f = new Fichiers($file->id, (array) $file);
			$f->remove();
		}
	}

	/**
	 * Enlève d'une liste de fichiers ceux qui sont mentionnés dans un texte wiki
	 * @param  array $files Liste de fichiers
	 * @param  string $text  texte wiki
	 * @return array        Un tableau qui ne contient pas les fichiers mentionnés dans $text
	 */
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
	 * @param  string $text Texte wiki
	 * @return array       Liste des IDs de fichiers mentionnés
	 */
	static public function listFilesUsedInText($text)
	{
		preg_match_all('/<<?(?:fichier|image)\s*(?:\|\s*)?(\d+)/', $text, $match, PREG_PATTERN_ORDER);
		preg_match_all('/(?:fichier|image):\/\/(\d+)/', $text, $match2, PREG_PATTERN_ORDER);
		
		return array_merge($match[1], $match2[1]);
	}

	/**
	 * Callback utilisé pour l'extension <<fichier>> dans le wiki-texte
	 * @param array $args    Arguments passés à l'extension
	 * @param string $content Contenu éventuel (en mode bloc)







|







756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
	 * @param  string $text Texte wiki
	 * @return array       Liste des IDs de fichiers mentionnés
	 */
	static public function listFilesUsedInText($text)
	{
		preg_match_all('/<<?(?:fichier|image)\s*(?:\|\s*)?(\d+)/', $text, $match, PREG_PATTERN_ORDER);
		preg_match_all('/(?:fichier|image):\/\/(\d+)/', $text, $match2, PREG_PATTERN_ORDER);

		return array_merge($match[1], $match2[1]);
	}

	/**
	 * Callback utilisé pour l'extension <<fichier>> dans le wiki-texte
	 * @param array $args    Arguments passés à l'extension
	 * @param string $content Contenu éventuel (en mode bloc)

Modified src/include/lib/Garradin/Form.php from [4384bc9995] to [024879329f].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

































23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

namespace Garradin;

class Form
{
	protected $errors = [];

	public function __construct()
	{
		// Valide un montant de monnaie valide (deux décimales, ne peut être négatif)
		\KD2\Form::registerValidationRule('money', function ($name, $params, $value) {
			return preg_match('/^\d+(?:\.\d{1,2})?$/', $value) && $value >= 0;
		});

		// Test si la valeur existe dans cette table
		// in_table:compta_categories,id
		\KD2\Form::registerValidationRule('in_table', function ($name, $params, $value) {
			$db = DB::getInstance();
			return $db->test($params[0], $db->where($params[1], $value));
		});
	}


































	public function check($token_action = '', Array $rules = null)
	{
		if (!\KD2\Form::tokenCheck($token_action))
		{
			$this->errors[] = 'Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.';
			return false;
		}

		if (!is_null($rules) && !$this->validate($rules))
		{
			return false;
		}

		return true;
	}

	public function validate(Array $rules)
	{
		return \KD2\Form::validate($rules, $this->errors, $_POST);
	}

	public function hasErrors()
	{
		return (count($this->errors) > 0);
	}













|









>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

















|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php

namespace Garradin;

class Form
{
	protected $errors = [];

	public function __construct()
	{
		// Valide un montant de monnaie valide (deux décimales, ne peut être négatif)
		\KD2\Form::registerValidationRule('money', function ($name, $params, $value) {
			return preg_match('/^\d+(?:[.,]\d{1,2})?$/', $value) && $value >= 0;
		});

		// Test si la valeur existe dans cette table
		// in_table:compta_categories,id
		\KD2\Form::registerValidationRule('in_table', function ($name, $params, $value) {
			$db = DB::getInstance();
			return $db->test($params[0], $db->where($params[1], $value));
		});
	}

	public function run(callable $fn, ?string $csrf_key = null, ?string $redirect = null): bool
	{
		if (null !== $csrf_key && !$this->check($csrf_key)) {
			return false;
		}

		try {
			call_user_func($fn);

			if (null !== $redirect) {
				Utils::redirect($redirect);
			}

			return true;
		}
		catch (UserException $e) {
			$this->addError($e->getMessage());
			return false;
		}
	}

	public function runIf($condition, callable $fn, ?string $csrf_key = null, ?string $redirect = null): ?bool
	{
		if (is_string($condition) && empty($_POST[$condition])) {
			return null;
		}
		elseif (is_bool($condition) && !$condition) {
			return null;
		}

		return $this->run($fn, $csrf_key, $redirect);
	}

	public function check($token_action = '', Array $rules = null)
	{
		if (!\KD2\Form::tokenCheck($token_action))
		{
			$this->errors[] = 'Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.';
			return false;
		}

		if (!is_null($rules) && !$this->validate($rules))
		{
			return false;
		}

		return true;
	}

	public function validate(Array $rules, array $source = null)
	{
		return \KD2\Form::validate($rules, $this->errors, $source);
	}

	public function hasErrors()
	{
		return (count($this->errors) > 0);
	}

92
93
94
95
96
97
98


99
100
101
102
103
104
105
		{
			case '_id': return 'identifiant';
			case 'passe': return 'mot de passe';
			case 'debut': return 'date de début';
			case 'fin': return 'date de fin';
			case 'duree': return 'durée';
			case 'passe_check': return 'vérification de mot de passe';


			default: return $name;
		}
	}

	protected function getErrorMessage($rule, $element, Array $params)
	{
		$element = $this->getFieldName($element);







>
>







125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
		{
			case '_id': return 'identifiant';
			case 'passe': return 'mot de passe';
			case 'debut': return 'date de début';
			case 'fin': return 'date de fin';
			case 'duree': return 'durée';
			case 'passe_check': return 'vérification de mot de passe';
			case 'id_account': return 'compte';
			case 'label': return 'libellé';
			default: return $name;
		}
	}

	protected function getErrorMessage($rule, $element, Array $params)
	{
		$element = $this->getFieldName($element);
125
126
127
128
129
130
131

132
133
134
135
136
137
138
139
140
141
142
			case 'date_format':
				return sprintf('Format de date invalide dans le champ %s.', $element);
			case 'numeric':
				return sprintf('Le champ %s doit être un nombre.', $element);
			case 'money':
				return sprintf('Le champ %s n\'est pas un nombre valide.', $element);
			case 'in':

				return sprintf('Valeur invalide dans le champ \'%s\'.', $element);
			default:
				return sprintf('Erreur "%s" dans le champ "%s"', $rule, $element);
		}
	}

	public function __invoke($key)
	{
		return \KD2\Form::get($key);
	}
}







>











160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
			case 'date_format':
				return sprintf('Format de date invalide dans le champ %s.', $element);
			case 'numeric':
				return sprintf('Le champ %s doit être un nombre.', $element);
			case 'money':
				return sprintf('Le champ %s n\'est pas un nombre valide.', $element);
			case 'in':
			case 'in_table':
				return sprintf('Valeur invalide dans le champ \'%s\'.', $element);
			default:
				return sprintf('Erreur "%s" dans le champ "%s"', $rule, $element);
		}
	}

	public function __invoke($key)
	{
		return \KD2\Form::get($key);
	}
}

Modified src/include/lib/Garradin/Install.php from [3140a79786] to [a5c31079aa].

1
2
3
4




5
6
7
8
9
10
11
<?php

namespace Garradin;





/**
 * Pour procéder à l'installation de l'instance Garradin
 * Utile pour automatiser l'installation sans passer par la page d'installation
 */
class Install
{
	static public function reset(Membres\Session $session, $password, array $options = [])




>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Chart;
use Garradin\Entities\Accounting\Year;

/**
 * Pour procéder à l'installation de l'instance Garradin
 * Utile pour automatiser l'installation sans passer par la page d'installation
 */
class Install
{
	static public function reset(Membres\Session $session, $password, array $options = [])
21
22
23
24
25
26
27


28


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
		(new Sauvegarde)->create(date('Y-m-d-His-') . 'avant-remise-a-zero');

		DB::getInstance()->close();
		Config::deleteInstance();

		unlink(DB_FILE);



		$ok = self::install($config->nom_asso, $config->adresse_asso, $config->email_asso, 'Bureau', $user->identite, $user->email, $password, $config->site_asso);



		if ($ok)
		{
			// Force l'installation de plugin système
			Plugin::checkAndInstallSystemPlugins();
		}

		return $ok;
	}

	static public function install($nom_asso, $adresse_asso, $email_asso, $nom_categorie, $nom_membre, $email_membre, $passe_membre, $site_asso = WWW_URL)
	{
		$db = DB::getInstance(true);

		// Taille de la page de DB, on force à 4096 (défaut dans les dernières
		// versions de SQLite mais pas les vieilles)
		$db->exec('PRAGMA page_size = 4096;');
		$db->exec('VACUUM;');

		// Création de la base de données
		$db->begin();
		$db->exec('PRAGMA application_id = ' . DB::APPID . ';');
		$db->exec(file_get_contents(DB_SCHEMA));
		$db->commit();

		// Configuration de base
		// c'est dans Config::set que sont vérifiées les données utilisateur (renvoie UserException)
		$config = Config::getInstance();
		$config->set('nom_asso', $nom_asso);
		$config->set('adresse_asso', $adresse_asso);
		$config->set('email_asso', $email_asso);
		$config->set('site_asso', $site_asso);
		$config->set('monnaie', '€');
		$config->set('pays', 'FR');
		$config->setVersion(garradin_version());

		$champs = Membres\Champs::importInstall();
		$champs->save(false); // Pas de copie car pas de table membres existante








>
>
|
>
>










|


















<
|
|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
		(new Sauvegarde)->create(date('Y-m-d-His-') . 'avant-remise-a-zero');

		DB::getInstance()->close();
		Config::deleteInstance();

		unlink(DB_FILE);

		// We can't use the real password, as it might not be valid (too short or compromised)
		$ok = self::install($config->nom_asso, $user->identite, $user->email, md5($password . SECRET_KEY));

		// Restore password
		DB::getInstance()->preparedQuery('UPDATE membres SET passe = ? WHERE id = 1;', [$session::hashPassword($password)]);

		if ($ok)
		{
			// Force l'installation de plugin système
			Plugin::checkAndInstallSystemPlugins();
		}

		return $ok;
	}

	static public function install($nom_asso, $nom_membre, $email_membre, $passe_membre)
	{
		$db = DB::getInstance(true);

		// Taille de la page de DB, on force à 4096 (défaut dans les dernières
		// versions de SQLite mais pas les vieilles)
		$db->exec('PRAGMA page_size = 4096;');
		$db->exec('VACUUM;');

		// Création de la base de données
		$db->begin();
		$db->exec('PRAGMA application_id = ' . DB::APPID . ';');
		$db->exec(file_get_contents(DB_SCHEMA));
		$db->commit();

		// Configuration de base
		// c'est dans Config::set que sont vérifiées les données utilisateur (renvoie UserException)
		$config = Config::getInstance();
		$config->set('nom_asso', $nom_asso);

		$config->set('email_asso', $email_membre);
		$config->set('site_asso', WWW_URL);
		$config->set('monnaie', '€');
		$config->set('pays', 'FR');
		$config->setVersion(garradin_version());

		$champs = Membres\Champs::importInstall();
		$champs->save(false); // Pas de copie car pas de table membres existante

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
			'droit_compta' => Membres::DROIT_AUCUN,
			'droit_config' => Membres::DROIT_AUCUN,
			'droit_connexion' => Membres::DROIT_AUCUN,
			'cacher' => 1,
		]);

		$id = $cats->add([
			'nom' => ucfirst($nom_categorie),
			'droit_inscription' => Membres::DROIT_AUCUN,
			'droit_wiki' => Membres::DROIT_ADMIN,
			'droit_membres' => Membres::DROIT_ADMIN,
			'droit_compta' => Membres::DROIT_ADMIN,
			'droit_config' => Membres::DROIT_ADMIN,
		]);








|







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
			'droit_compta' => Membres::DROIT_AUCUN,
			'droit_config' => Membres::DROIT_AUCUN,
			'droit_connexion' => Membres::DROIT_AUCUN,
			'cacher' => 1,
		]);

		$id = $cats->add([
			'nom' => 'Administrateurs',
			'droit_inscription' => Membres::DROIT_AUCUN,
			'droit_wiki' => Membres::DROIT_ADMIN,
			'droit_membres' => Membres::DROIT_ADMIN,
			'droit_compta' => Membres::DROIT_ADMIN,
			'droit_config' => Membres::DROIT_ADMIN,
		]);

129
130
131
132
133
134
135
136
137




138
139

140



141

142

143
144
145
146



147
148

149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168



























169
170
171
172
173
174
175
		$wiki->editRevision($id_page, 0, [
			'id_auteur' =>  $id_membre,
			'contenu'   =>  "Bienvenue dans l'administration de ".$nom_asso." !\n\n"
				.   "Utilisez le menu à gauche pour accéder aux différentes rubriques.",
		]);
		$config->set('accueil_connexion', $page);

		// Mise en place compta
		$comptes = new Compta\Comptes;




		$comptes->importPlan();


		$comptes = new Compta\Categories;



		$comptes->importCategories();



		$ex = new Compta\Exercices;
		$ex->add([
			'libelle'   =>  'Premier exercice',
			'debut'     =>  date('Y-01-01'),



			'fin'       =>  date('Y-12-31')
		]);


		// Ajout d'une recherche avancée en exemple
		$query = [
			'query' => [[
				'operator' => 'AND',
				'conditions' => [
					[
						'column'   => 'lettre_infos',
						'operator' => '= 1',
						'values'   => [],
					],
				],
			]],
			'order' => 'numero',
			'desc' => true,
			'limit' => '10000',
		];

		$recherche = new Recherche;
		$recherche->add('Membres inscrits à la lettre d\'information', null, $recherche::TYPE_JSON, 'membres', $query);




























		return $config->save();
	}

	static public function checkAndCreateDirectories()
	{
		// Vérifier que les répertoires vides existent, sinon les créer







|
|
>
>
>
>
|

>
|
>
>
>
|
>

>
|
|
|
|
>
>
>
|
|
>

|
|

















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
		$wiki->editRevision($id_page, 0, [
			'id_auteur' =>  $id_membre,
			'contenu'   =>  "Bienvenue dans l'administration de ".$nom_asso." !\n\n"
				.   "Utilisez le menu à gauche pour accéder aux différentes rubriques.",
		]);
		$config->set('accueil_connexion', $page);

        // Import plan comptable
        $chart = new Chart;
        $chart->label = 'Plan comptable associatif 2018';
        $chart->country = 'FR';
        $chart->code = 'PCA2018';
        $chart->save();
        $chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv');

        // Premier exercice
        $year = new Year;
        $year->label = sprintf('Exercice %d', date('Y'));
        $year->start_date = new \DateTime('January 1st');
        $year->end_date = new \DateTime('December 31');
        $year->id_chart = $chart->id();
        $year->save();

        // Compte bancaire
        $account = new Account;
        $account->import([
        	'label' => 'Compte courant',
        	'code' => '512A',
        	'type' => Account::TYPE_BANK,
        	'position' => Account::ASSET_OR_LIABILITY,
        	'id_chart' => $chart->id(),
        	'user' => 1,
        ]);
        $account->save();

		// Ajout d'une recherche avancée en exemple (membres)
		$query = (object) [
			'query' => [[
				'operator' => 'AND',
				'conditions' => [
					[
						'column'   => 'lettre_infos',
						'operator' => '= 1',
						'values'   => [],
					],
				],
			]],
			'order' => 'numero',
			'desc' => true,
			'limit' => '10000',
		];

		$recherche = new Recherche;
		$recherche->add('Membres inscrits à la lettre d\'information', null, $recherche::TYPE_JSON, 'membres', $query);

		// Ajout d'une recherche avancée en exemple (compta)
		$query = (object) [
			'query' => [[
				'operator' => 'AND',
				'conditions' => [
					[
						'column'   => 'a2.code',
						'operator' => 'IS NULL',
						'values'   => [],
					],
				],
			]],
			'order' => 't.id',
			'desc' => false,
			'limit' => '100',
		];

		$recherche = new Recherche;
		$recherche->add('Écritures sans projet', null, $recherche::TYPE_JSON, 'compta', $query);

		// Install welcome plugin if available
		$has_welcome_plugin = Plugin::getPath('welcome', false);

		if ($has_welcome_plugin) {
			Plugin::install('welcome', true);
		}

		return $config->save();
	}

	static public function checkAndCreateDirectories()
	{
		// Vérifier que les répertoires vides existent, sinon les créer

Modified src/include/lib/Garradin/Membres.php from [1c694ab262] to [312c7859a9].

288
289
290
291
292
293
294
295
296


297

298
299

300

301
302
303
304
305

306
307

308


309
310
311
312
313
314
315
316
317
318
319
320
    }

    public function getIDWithNumero($numero)
    {
        return DB::getInstance()->firstColumn('SELECT id FROM membres WHERE numero = ?;', (int) $numero);
    }

    public function getSearchHeaderFields(array $result)
    {


        if (!count($result))

        {
            return false;

        }


        $champs = Config::getInstance()->get('champs_membres');
        $fields = [];

        foreach (reset($result) as $field=>$value)

        {
            if ($config = $champs->get($field))

            {


                $fields[$field] = $config;
            }
        }

        return $fields;
    }

    public function sendMessage(array $recipients, $subject, $message, $send_copy)
    {
        $config = Config::getInstance();

        foreach ($recipients as $key => $recipient)







|

>
>
|
>

|
>

>
|
<
|
|
<
>

|
>
|
>
>
|
|
|
|
|







288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306

307
308

309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
    }

    public function getIDWithNumero($numero)
    {
        return DB::getInstance()->firstColumn('SELECT id FROM membres WHERE numero = ?;', (int) $numero);
    }

    public function quickSearch(string $query)
    {
        $identity = Config::getInstance()->get('champ_identite');
        $operator = 'LIKE';

        if (is_numeric(trim($query)))
        {
            $column = 'numero';
            $operator = '= ?';
        }
        elseif (strpos($query, '@') !== false)
        {

            $column = 'email';
        }

        else
        {
            $column = $identity;
        }

        if ($operator == 'LIKE') {
            $query = '%' . str_replace(['%', '_'], '\\', $query) . '%';
            $operator = 'LIKE ? ESCAPE \'\\\'';
        }

        $sql = sprintf('SELECT id, numero, %s AS identite FROM membres WHERE %s %s ORDER BY %1$s LIMIT 50;', $identity, $column, $operator);
        return DB::getInstance()->get($sql, $query);
    }

    public function sendMessage(array $recipients, $subject, $message, $send_copy)
    {
        $config = Config::getInstance();

        foreach ($recipients as $key => $recipient)

Modified src/include/lib/Garradin/Membres/Categories.php from [cfad189ba8] to [2ef27a72a7].

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
        'connexion' =>  Membres::DROIT_ACCES,
        'membres'   =>  Membres::DROIT_AUCUN,
        'compta'    =>  Membres::DROIT_AUCUN,
        'wiki'      =>  Membres::DROIT_ACCES,
        'config'    =>  Membres::DROIT_AUCUN,
    ];

    static public function getDroitsDefaut()
    {
        return $this->droits;
    }

    protected function _checkData(&$data)
    {
        $db = DB::getInstance();

        if (!isset($data['nom']) || !trim($data['nom']))
        {
            throw new UserException('Le nom de catégorie ne peut rester vide.');
        }

        if (!empty($data['id_cotisation_obligatoire']) 
            && !$db->test('cotisations', 'id = ?', (int)$data['id_cotisation_obligatoire']))
        {
            throw new UserException('Numéro de cotisation inconnu.');
        }

        if (isset($data['id_cotisation_obligatoire']) && empty($data['id_cotisation_obligatoire']))
        {
            $data['id_cotisation_obligatoire'] = null;
        }
    }

    public function add($data)
    {
        $this->_checkData($data);

        foreach ($this->droits as $key=>$value)







<
<
<
<
<








<
<
<
<
<
<
<
<
<
<
<







15
16
17
18
19
20
21





22
23
24
25
26
27
28
29











30
31
32
33
34
35
36
        'connexion' =>  Membres::DROIT_ACCES,
        'membres'   =>  Membres::DROIT_AUCUN,
        'compta'    =>  Membres::DROIT_AUCUN,
        'wiki'      =>  Membres::DROIT_ACCES,
        'config'    =>  Membres::DROIT_AUCUN,
    ];






    protected function _checkData(&$data)
    {
        $db = DB::getInstance();

        if (!isset($data['nom']) || !trim($data['nom']))
        {
            throw new UserException('Le nom de catégorie ne peut rester vide.');
        }











    }

    public function add($data)
    {
        $this->_checkData($data);

        foreach ($this->droits as $key=>$value)

Deleted src/include/lib/Garradin/Membres/Cotisations.php version [f8b5327ae2].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
<?php

namespace Garradin\Membres;

use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Plugin;
use Garradin\Membres;
use Garradin\Compta\Comptes;

class Cotisations
{
	const ITEMS_PER_PAGE = 100;

	/**
	 * Vérification des champs fournis pour la modification de donnée
	 * @param  array $data Tableau contenant les champs à ajouter/modifier
	 * @return void
	 */
	protected function _checkFields(&$data)
	{
		$db = DB::getInstance();

		if (empty($data['date']) || !Utils::checkDate($data['date']))
		{
			throw new UserException('Date vide ou invalide.');
		}

		if (empty($data['id_cotisation']) 
			|| !$db->firstColumn('SELECT 1 FROM cotisations WHERE id = ?;', (int) $data['id_cotisation']))
		{
			throw new UserException('Cotisation inconnue.');
		}

		$data['id_cotisation'] = (int) $data['id_cotisation'];

		if (!empty($data['numero_membre']))
		{
			$data['id_membre'] = (new Membres)->getIDWithNumero($data['numero_membre']);
			unset($data['numero_membre']);
		}

		if (empty($data['id_membre']) 
			|| !$db->firstColumn('SELECT 1 FROM membres WHERE id = ?;', (int) $data['id_membre']))
		{
			throw new UserException('Membre inconnu ou invalide.');
		}

		$data['id_membre'] = (int) $data['id_membre'];
	}

	/**
	 * Enregistrer un événement de cotisation
	 * @param array $data Tableau des champs à insérer
	 * @return integer ID de l'événement créé
	 */
	public function add($data, array $data_compta)
	{
		$db = DB::getInstance();

		$co = $db->first('SELECT * FROM cotisations WHERE id = ?;', (int)$data['id_cotisation']);

		$this->_checkFields($data);

		$check = $db->firstColumn('SELECT 1 FROM cotisations_membres 
			WHERE id_cotisation = ? AND id_membre = ? AND date = ?;', 
			(int)$data['id_cotisation'], (int)$data['id_membre'], $data['date']);

		if ($check)
		{
			throw new UserException('Cette cotisation a déjà été enregistrée pour ce jour-ci et ce membre-ci.');
		}

		$db->begin();

		$db->insert('cotisations_membres', [
			'date'				=>	$data['date'],
			'id_cotisation'		=>	$data['id_cotisation'],
			'id_membre'			=>	$data['id_membre'],
			]);

		$id = $db->lastInsertRowId();

		if ($co->id_categorie_compta)
		{
			$membre = (new Membres)->getNom($data['id_membre']);

			try {
				$data_compta = array_merge($data_compta, [
					'id_categorie' => $co->id_categorie_compta,
					'libelle'      => sprintf('%s - %s', $co->intitule, $membre),
					'date'         => $data['date'],
					'id_auteur'    => $data['id_auteur'],
					'id_membre'    => $data['id_membre'],
				]);

				$id_operation = $this->addOperationCompta($id, $data_compta);
			}
			catch (\Exception $e)
			{
				$db->rollback();
				throw $e;
			}
		}

		$db->commit();

		Plugin::fireSignal('cotisation.ajout', array_merge(['id' => $id], $data));

		return $data['id_membre'];
	}

	/**
	 * Supprimer un événement de cotisation
	 * @param  integer $id ID de l'événement à supprimer
	 * @return integer true en cas de succès
	 */
	public function delete($id)
	{
		$db = DB::getInstance();
		return $db->delete('cotisations_membres', 'id = ' . (int)$id);
	}

	public function get($id)
	{
		return DB::getInstance()->first('SELECT * FROM cotisations_membres WHERE id = ?;', (int)$id);
	}

	/**
	 * Renvoie une liste des écritures comptables liées à une cotisation
	 * @param  int $id Numéro de la cotisation membre
	 * @return array Liste des écritures
	 */
	public function listOperationsCompta($id)
	{
		return DB::getInstance()->get('SELECT * FROM compta_journal
			WHERE id IN (SELECT id_operation FROM membres_operations
				WHERE id_cotisation = ?);', (int)$id);
	}

	/**
	 * Compte les opérations comptables liées à cette cotisation
	 */
	public function countOperationsCompta($id)
	{
		return DB::getInstance()->firstColumn('SELECT COUNT(*) FROM membres_operations WHERE id_cotisation = ?;', (int)$id);
	}

	/**
	 * Ajouter une écriture comptable pour un paiemement membre
	 * @param int $id Numéro de la cotisation membre
	 * @param array $data Données
	 */
	public function addOperationCompta($id, $data)
	{
		$journal = new \Garradin\Compta\Journal;
		$db = DB::getInstance();

		if (!isset($data['moyen_paiement']) || trim($data['moyen_paiement']) === '')
		{
			throw new UserException('Moyen de paiement inconnu ou invalide.');
		}

		if ($data['moyen_paiement'] != 'ES')
		{
			if (trim($data['banque']) == '')
			{
				throw new UserException('Le compte bancaire choisi est invalide.');
			}

			if (!$db->firstColumn('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;', $data['banque']))
			{
				throw new UserException('Le compte bancaire choisi n\'existe pas.');
			}
		}

		if (!isset($data['montant']) || !is_numeric($data['montant']) || $data['montant'] < 0)
		{
			throw new UserException('Le montant indiqué n\'est pas un nombre valide : doit être supérieur ou égal à zéro.');
		}

		if (!isset($data['libelle']) || trim($data['libelle']) == '')
		{
			throw new UserException('Le libellé ne peut rester vide.');
		}

		$data['libelle'] = trim($data['libelle']);

		if (!isset($data['montant']) || !is_numeric($data['montant']) || (float)$data['montant'] < 0)
		{
			throw new UserException('Le montant doit être un nombre positif et valide.');
		}

		$data['montant'] = (float) $data['montant'];

		if (!empty($data['a_encaisser']) && ($data['moyen_paiement'] == 'CH' || $data['moyen_paiement'] == 'CB'))
		{
            $debit = $data['moyen_paiement'] == 'CH' 
                ? Comptes::CHEQUE_A_ENCAISSER
                : Comptes::CARTE_A_ENCAISSER;
		}
		elseif ($data['moyen_paiement'] != 'ES')
		{
			$debit = $data['banque'];
		}
		else
		{
			$debit = \Garradin\Compta\Comptes::CAISSE;
		}

		$credit = $db->firstColumn('SELECT compte FROM compta_categories WHERE id = ?;', 
			$data['id_categorie']);

		$id_operation = $journal->add([
			'libelle'        => $data['libelle'],
			'montant'        => $data['montant'],
			'date'           => $data['date'],
			'moyen_paiement' => $data['moyen_paiement'],
			'numero_cheque'  => isset($data['numero_cheque']) ? $data['numero_cheque'] : null,
			'compte_debit'   => $debit,
			'compte_credit'  => $credit,
			'id_categorie'   => (int)$data['id_categorie'],
			'id_auteur'      => (int)$data['id_auteur'],
			'remarques'      => isset($data['remarques']) ? $data['remarques'] : null,
			'numero_piece'   => isset($data['numero_piece']) ? $data['numero_piece'] : null,
		]);

		$db->insert('membres_operations', [
			'id_operation'  => $id_operation,
			'id_membre'     => $data['id_membre'],
			'id_cotisation' => (int)$id,
		]);

		return $id_operation;
	}

	/**
	 * Nombre de membres pour une cotisation
	 * @param  integer $id Numéro de la cotisation
	 * @return integer     Nombre d'événements pour cette cotisation
	 */
	public function countMembersForCotisation($id)
	{
		$db = DB::getInstance();
		return $db->firstColumn('SELECT COUNT(DISTINCT cm.id_membre)
			FROM cotisations_membres  AS cm
				INNER JOIN membres AS m ON m.id = cm.id_membre
			WHERE cm.id_cotisation = ?
			AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1);',
			(int)$id);
	}

	/**
	 * Liste des membres qui sont inscrits à une cotisation
	 * @param  integer $id Numéro de la cotisation
	 * @return array     Liste des membres
	 */
	public function listMembersForCotisation($id, $include_category, $page = 1, $order = null, $desc = true)
	{
		$begin = ($page - 1) * self::ITEMS_PER_PAGE;

		$db = DB::getInstance();
		$champ_id = Config::getInstance()->get('champ_identite');

		if (empty($order))
			$order = 'date';

		switch ($order)
		{
			case 'a_jour':
				break;
			case 'date':
				$order = 'cm.date';
				break;
			case 'identite':
				$order = 'transliterate_to_ascii(m.'.$champ_id.') COLLATE NOCASE';
				break;
			default:
				$order = 'cm.id_membre';
				break;
		}

		$desc = $desc ? 'DESC' : 'ASC';

		// Renvoyer la liste avec tous les membres des catégories dont la cotisation obligatoire est celle-ci
		if ($include_category)
		{
			$cats_obligatoires = $db->getAssoc('SELECT id, id FROM membres_categories WHERE id_cotisation_obligatoire = ? AND cacher = 0;', $id);

			return $db->get('SELECT m.id AS id_membre, MAX(cm.date) AS date, cm.id, m.numero,
				m.'.$champ_id.' AS nom, c.montant,
				CASE WHEN cm.id IS NULL THEN 0
				WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
				WHEN c.fin IS NOT NULL THEN (cm.date <= c.fin AND cm.date >= c.debut)
				ELSE 1 END AS a_jour
				FROM membres AS m
					LEFT JOIN cotisations_membres AS cm ON cm.id_membre = m.id AND cm.id_cotisation = ?
					LEFT JOIN cotisations AS c ON c.id = cm.id_cotisation
				WHERE
					'.$db->where('m.id_categorie', $cats_obligatoires) . '
				GROUP BY m.id ORDER BY '.$order.' '.$desc.' LIMIT ?,?;',
				$id, $begin, self::ITEMS_PER_PAGE);
		}

		return $db->get('SELECT cm.id_membre, MAX(cm.date) AS date, cm.id, m.numero,
			m.'.$champ_id.' AS nom, c.montant,
			CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
			WHEN c.fin IS NOT NULL THEN (cm.date <= c.fin AND cm.date >= c.debut)
			ELSE 1 END AS a_jour
			FROM cotisations_membres AS cm
				INNER JOIN cotisations AS c ON c.id = cm.id_cotisation
				INNER JOIN membres AS m ON m.id = cm.id_membre
			WHERE
				cm.id_cotisation = ?
				AND m.id_categorie NOT IN (SELECT mc.id FROM membres_categories AS mc WHERE mc.cacher = 1)
			GROUP BY cm.id_membre ORDER BY '.$order.' '.$desc.' LIMIT ?,?;',
			(int)$id, $begin, self::ITEMS_PER_PAGE);
	}

	/**
	 * Liste des événements d'un membre
	 * @param  integer $id Numéro de membre
	 * @return array     Liste des événements de cotisation fait par ce membre
	 */
	public function listForMember($id)
	{
		// TODO: récupérer ici le solde payé pour une cotisation, pour savoir si tout a été payé
		// (pour gérer par exemple les paiements effectués en plusieurs versements)
		// mais pour le moment le fonctionnement de compta_journal est trop compliqué pour arriver
		// à récupérer un solde dans une requête simple, la requête serait trop lourde donc on laisse tomber
		$db = DB::getInstance();
		return $db->get('SELECT cm.*, c.intitule, c.duree, c.debut, c.fin, c.montant,
			(SELECT COUNT(*) FROM membres_operations WHERE id_cotisation = cm.id) AS nb_operations
			FROM cotisations_membres AS cm
				LEFT JOIN cotisations AS c ON c.id = cm.id_cotisation
			WHERE cm.id_membre = ? ORDER BY cm.date DESC;', (int)$id);
	}

	/**
	 * Liste des cotisations / activités en cours pour ce membre
	 * @param  integer $id Numéro de membre
	 * @return array     Liste des cotisations en cours de validité
	 */
	public function listSubscriptionsForMember($id)
	{
		$db = DB::getInstance();
		return $db->get('SELECT c.*,
			MAX(cm.date) AS date,
			CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
			WHEN c.fin IS NOT NULL THEN (cm.id IS NOT NULL AND cm.date <= c.fin AND cm.date >= c.debut)
			WHEN cm.id IS NOT NULL THEN 1 ELSE 0 END AS a_jour,
			CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
			WHEN c.fin IS NOT NULL THEN c.fin ELSE 1 END AS expiration,
			(julianday(date()) - julianday(CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
			WHEN c.fin IS NOT NULL THEN c.fin END)) AS nb_jours
			FROM cotisations_membres AS cm
				INNER JOIN cotisations AS c ON c.id = cm.id_cotisation
			WHERE cm.id_membre = ?
				AND ((c.fin IS NOT NULL AND cm.date <= c.fin AND cm.date >= c.debut) OR c.fin IS NULL)
			GROUP BY cm.id_cotisation
			ORDER BY cm.date DESC;', (int)$id);
	}

	/**
	 * Ce membre est-il à jour sur cette cotisation ?
	 * @param  integer  $id             Numéro de membre
	 * @param  integer  $id_cotisation  Numéro de cotisation
	 * @return array 					Infos sur la cotisation, et champ expiration
	 * (si NULL = cotisation jamais enregistrée, si 1 = cotisation ponctuelle enregistrée, sinon date d'expiration)
	 */
	public function isMemberUpToDate($id, $id_cotisation)
	{
		$db = DB::getInstance();
		return $db->first('SELECT c.*,
			CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
			WHEN c.fin IS NOT NULL THEN (cm.id IS NOT NULL)
			WHEN cm.id IS NOT NULL THEN 1 ELSE 0 END AS a_jour,
			CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
			WHEN c.fin IS NOT NULL THEN c.fin ELSE 1 END AS expiration
			FROM cotisations AS c 
				LEFT JOIN cotisations_membres AS cm ON cm.id_cotisation = c.id AND cm.id_membre = ?
			WHERE c.id = ? ORDER BY cm.date DESC;',
			(int)$id, (int)$id_cotisation);
	}

	public function countForMember($id)
	{
		$db = DB::getInstance();
		return $db->firstColumn('SELECT COUNT(DISTINCT id_cotisation) FROM cotisations_membres 
			WHERE id_membre = ?;', (int)$id);
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































































































































































































































































































































































































































































































































































































































































































































































































Modified src/include/lib/Garradin/Membres/Import.php from [5800dd1bd4] to [99caaf5adf].

1
2
3
4
5
6
7
8


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
<?php

namespace Garradin\Membres;

use Garradin\Membres;
use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;


use Garradin\UserException;

class Import
{
	public function getCSVAsArray($path)
	{
		if (!file_exists($path) || !is_readable($path))
		{
			throw new \RuntimeException('Fichier inconnu : '.$path);
		}

		$fp = Utils::open_csv_file($path);

		if (!$fp)
		{
			return false;
		}

		$delim = Utils::find_csv_delim($fp);
		Utils::skip_bom($fp);

		$line = 0;
		$out = [];
		$nb_columns = null;

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);
			$line++;

			if (empty($row))
			{
				continue;
			}

			if (null === $nb_columns)
			{
				$nb_columns = count($row);
			}

			if (count($row) != $nb_columns)
			{
				throw new UserException('Erreur sur la ligne ' . $line . ' : incohérence dans le nombre de colonnes avec la première ligne.');
			}

			$out[$line] = $row;
		}

		fclose($fp);

		return $out;
	}

	/**
	 * Importer un CSV générique
	 * @param  string $path              Chemin vers le CSV
	 * @param  array  $translation_table Tableau indiquant la correspondance à effectuer entre les colonnes
	 * du CSV et les champs de Garradin. Par exemple : ['Date création fiche' => 'date_inscription']
	 * @return boolean                   TRUE en cas de succès
	 */
	public function fromArray(array $table, $translation_table, $current_user_id, $skip_lines = 0)
	{
		$db = DB::getInstance();
		$db->begin();
		$membres = new Membres;
		$champs = Config::getInstance()->get('champs_membres');

		$nb_columns = count($translation_table);

		if ($skip_lines)
		{
			$table = array_slice($table, $skip_lines, null, true);
		}

		foreach ($table as $line => $row)
		{
			if (empty($row))
			{
				continue;
			}

			if (count($row) != $nb_columns)
			{
				$db->rollback();
				throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
			}

			$data = [];

			foreach ($translation_table as $column_index => $garradin_field)
			{
				// Champs qu'on ne veut pas importer
				if (empty($garradin_field))
				{
					continue;
				}

				// Concaténer plusieurs champs, si on choisit d'indiquer plusieurs fois
				// le même champ pour plusieurs colonnes (par exemple pour mettre nom et prénom
				// dans un seul champ)
				if (isset($data[$garradin_field]))
				{
					$champ = $champs->get($garradin_field);

					if ($champ->type == 'text')
					{
						$data[$garradin_field] .= ' ' . $row[$column_index];
					}
					elseif ($champ->type == 'textarea')
					{
						$data[$garradin_field] .= "\n" . $row[$column_index];
					}
					else
					{
						throw new UserException(sprintf('Erreur sur la ligne %d : impossible de concaténer des colonnes avec le champ %s : n\'est pas un champ de type texte', $line, $champ->title));
					}
				}
				else
				{
					$data[$garradin_field] = $row[$column_index];
				}
			}

			if (!empty($data['numero']) && $data['numero'] > 0)
			{
				$numero = (int)$data['numero'];
			}
			else
			{
				unset($data['numero']);
				$numero = false;
			}

			try {
				if ($numero && ($id = $membres->getIDWithNumero($numero)))
				{
					if ($id === $current_user_id)
					{
						// Ne pas modifier le membre courant, on risque de se tirer une balle dans le pied
						continue;
					}

					$membres->edit($id, $data);
				}
				else
				{
					$membres->add($data, false);
				}
			}
			catch (UserException $e)
			{
				$db->rollback();
				throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage());
			}








>
>




<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


<
<
<


|




<

<
<
<
<
<
<
<
|

|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|



|












|



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14

















































15
16



17
18
19
20
21
22
23

24







25
26
27
28















































29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php

namespace Garradin\Membres;

use Garradin\Membres;
use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\CSV;
use Garradin\CSV_Custom;
use Garradin\UserException;

class Import
{

















































	/**
	 * Importer un CSV générique



	 * @return boolean                   TRUE en cas de succès
	 */
	public function fromCustomCSV(CSV_Custom $csv, int $current_user_id)
	{
		$db = DB::getInstance();
		$db->begin();
		$membres = new Membres;









		foreach ($csv->iterate() as $line => $row)
		{
			if (!empty($row->numero) && $row->numero > 0)
			{















































				$numero = (int)$row->numero;
			}
			else
			{
				unset($row->numero);
				$numero = false;
			}

			try {
				if ($numero && ($id = $membres->getIDWithNumero($numero)))
				{
					if ($id === $current_user_id)
					{
						// Ne pas modifier le membre courant, on risque de se tirer une balle dans le pied
						continue;
					}

					$membres->edit($id, (array)$row);
				}
				else
				{
					$membres->add((array)$row, false);
				}
			}
			catch (UserException $e)
			{
				$db->rollback();
				throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage());
			}
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
	public function fromGarradinCSV($path, $current_user_id)
	{
		if (!file_exists($path) || !is_readable($path))
		{
			throw new \RuntimeException('Fichier inconnu : '.$path);
		}

		$fp = Utils::open_csv_file($path);

		if (!$fp)
		{
			return false;
		}

		$db = DB::getInstance();
		$db->begin();
		$membres = new Membres;

		// On récupère les champs qu'on peut importer
		$champs_membres = Config::getInstance()->get('champs_membres');
		$champs_multiples = $champs_membres->getMultiples();
		$champs = $champs_membres->getKeys();
		$champs[] = 'date_inscription';
		//$champs[] = 'date_connexion';
		//$champs[] = 'id';
		//$champs[] = 'id_categorie';

		$line = 0;
		$delim = Utils::find_csv_delim($fp);
		Utils::skip_bom($fp);

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);

			$line++;








|




















|
|







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
	public function fromGarradinCSV($path, $current_user_id)
	{
		if (!file_exists($path) || !is_readable($path))
		{
			throw new \RuntimeException('Fichier inconnu : '.$path);
		}

		$fp = CSV::open($path);

		if (!$fp)
		{
			return false;
		}

		$db = DB::getInstance();
		$db->begin();
		$membres = new Membres;

		// On récupère les champs qu'on peut importer
		$champs_membres = Config::getInstance()->get('champs_membres');
		$champs_multiples = $champs_membres->getMultiples();
		$champs = $champs_membres->getKeys();
		$champs[] = 'date_inscription';
		//$champs[] = 'date_connexion';
		//$champs[] = 'id';
		//$champs[] = 'id_categorie';

		$line = 0;
		$delim = CSV::findDelimiter($fp);
		CSV::skipBOM($fp);

		while (!feof($fp))
		{
			$row = fgetcsv($fp, 4096, $delim);

			$line++;

306
307
308
309
310
311
312
313






314
315

316
317
318
319

320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344

	protected function export(array $list = null)
	{
		$db = DB::getInstance();

		$champs = Config::getInstance()->get('champs_membres')->getKeys();
		$champs = array_map([$db, 'quoteIdentifier'], $champs);
		$champs_sql = 'm.' . implode(', m.', $champs);






		$where = $list ? 'WHERE ' . $db->where('m.id', $list) : '';


		$res = $db->iterate('SELECT ' . $champs_sql . ', c.nom AS "Catégorie membre" FROM membres AS m
			INNER JOIN membres_categories AS c ON m.id_categorie = c.id
			' . $where . '
			ORDER BY c.id;');


		return [
			array_keys((array) $res->current()),
			$res,
			sprintf('Export membres - %s - %s', Config::getInstance()->get('nom_asso'), date('Y-m-d')),
		];
	}

	public function toCSV(array $list = null)
	{
		list($champs, $result, $name) = $this->export($list);
		return Utils::toCSV($name, $result, $champs, [$this, 'exportRow']);
	}

	public function toODS(array $list = null)
	{
		list($champs, $result, $name) = $this->export($list);
		return Utils::toODS($name, $result, $champs, [$this, 'exportRow']);
	}

	public function exportRow(\stdClass $row) {
		// Pas hyper efficace, il faudrait ne pas récupérer la liste pour chaque ligne... FIXME
		$champs_multiples = Config::getInstance()->get('champs_membres')->getMultiples();

		// convertir les champs à choix multiple de binaire vers liste séparée par des points virgules







|
>
>
>
>
>
>
|
|
>
|

|
|
>











|





|







201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247

	protected function export(array $list = null)
	{
		$db = DB::getInstance();

		$champs = Config::getInstance()->get('champs_membres')->getKeys();
		$champs = array_map([$db, 'quoteIdentifier'], $champs);
		$fields = 'm.' . implode(', m.', $champs);

		if ($list) {
			$list = array_map('intval', $list);
			$where = sprintf('WHERE m.%s', $db->where('id', $list));
		}
		else {
			$where = '';
		}

		$sql = sprintf('SELECT %s, c.nom AS "Catégorie membre" FROM membres AS m
			INNER JOIN membres_categories AS c ON m.id_categorie = c.id
			%s ORDER BY c.id;', $fields, $where);

		$res = $db->iterate($sql);

		return [
			array_keys((array) $res->current()),
			$res,
			sprintf('Export membres - %s - %s', Config::getInstance()->get('nom_asso'), date('Y-m-d')),
		];
	}

	public function toCSV(array $list = null)
	{
		list($champs, $result, $name) = $this->export($list);
		return CSV::toCSV($name, $result, $champs, [$this, 'exportRow']);
	}

	public function toODS(array $list = null)
	{
		list($champs, $result, $name) = $this->export($list);
		return CSV::toODS($name, $result, $champs, [$this, 'exportRow']);
	}

	public function exportRow(\stdClass $row) {
		// Pas hyper efficace, il faudrait ne pas récupérer la liste pour chaque ligne... FIXME
		$champs_multiples = Config::getInstance()->get('champs_membres')->getMultiples();

		// convertir les champs à choix multiple de binaire vers liste séparée par des points virgules

Modified src/include/lib/Garradin/Membres/Session.php from [fa679095c9] to [db8cb5f1d1].

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;

use KD2\Security;
use KD2\Security_OTP;
use KD2\QRCode;
use KD2\HTTP;

class Session extends \KD2\UserSession
{
	// Personalisation de la config de UserSession
	protected $cookie_name = 'gdin';
	protected $remember_me_cookie_name = 'gdinp';







|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;

use KD2\Security;
use KD2\Security_OTP;
use KD2\Graphics\QRCode;
use KD2\HTTP;

class Session extends \KD2\UserSession
{
	// Personalisation de la config de UserSession
	protected $cookie_name = 'gdin';
	protected $remember_me_cookie_name = 'gdinp';
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
	}

	// Extension des méthodes de UserSession
	public function __construct()
	{
		$url = parse_url(ADMIN_URL);

		//throw new \Exception('lol');

		parent::__construct(DB::getInstance(), [
			'cookie_domain' => $url['host'],
			'cookie_path'   => preg_replace('!/admin/$!', '/', $url['path']),
			'cookie_secure' => (\Garradin\PREFER_HTTPS >= 2) ? true : false,
		]);
	}

	/**
	 * Suppression anciens cookies qui avaient un chemin incorrect
	 * FIXME supprimer en 2019
	 * @return void
	 */
	public function cleanOldCookies()
	{
		setcookie($this->cookie_name, null, -1, '/admin/', $this->cookie_domain, $this->cookie_secure, true);
		setcookie($this->remember_me_cookie_name, null, -1, '/admin/', $this->cookie_domain, $this->cookie_secure, true);
	}

	protected function getUserForLogin($login)
	{
		$champ_id = Config::getInstance()->get('champ_identifiant');

		// Ne renvoie un membre que si celui-ci a le droit de se connecter
		$query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret
			FROM membres AS m







<
<







<
<
<
<
<
<
<
<
<
<
<







57
58
59
60
61
62
63


64
65
66
67
68
69
70











71
72
73
74
75
76
77
	}

	// Extension des méthodes de UserSession
	public function __construct()
	{
		$url = parse_url(ADMIN_URL);



		parent::__construct(DB::getInstance(), [
			'cookie_domain' => $url['host'],
			'cookie_path'   => preg_replace('!/admin/$!', '/', $url['path']),
			'cookie_secure' => (\Garradin\PREFER_HTTPS >= 2) ? true : false,
		]);
	}












	protected function getUserForLogin($login)
	{
		$champ_id = Config::getInstance()->get('champ_identifiant');

		// Ne renvoie un membre que si celui-ci a le droit de se connecter
		$query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret
			FROM membres AS m
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
	{
		$logged = parent::isLogged();

		if (!$disable_local_login && defined('\Garradin\LOCAL_LOGIN'))
		{
			$login_id = \Garradin\LOCAL_LOGIN;

			// On va chercher le premier membre avec le droit de gérer les membres
			if (-1 === $login_id) {
				$login_id = $this->db->firstColumn('SELECT id FROM membres
					WHERE id_categorie = (SELECT id FROM membres_categories WHERE droit_membres = ? LIMIT 1)
					LIMIT 1', Membres::DROIT_ADMIN);
			}

			if ($login_id > 0 && (!$logged || ($logged && $this->user->id != $login_id)))
			{
				$logged = $this->create($login_id);
			}







|


|







137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
	{
		$logged = parent::isLogged();

		if (!$disable_local_login && defined('\Garradin\LOCAL_LOGIN'))
		{
			$login_id = \Garradin\LOCAL_LOGIN;

			// On va chercher le premier membre avec le droit de gérer la config
			if (-1 === $login_id) {
				$login_id = $this->db->firstColumn('SELECT id FROM membres
					WHERE id_categorie IN (SELECT id FROM membres_categories WHERE droit_config = ?)
					LIMIT 1', Membres::DROIT_ADMIN);
			}

			if ($login_id > 0 && (!$logged || ($logged && $this->user->id != $login_id)))
			{
				$logged = $this->create($login_id);
			}

Modified src/include/lib/Garradin/Plugin.php from [de7f4cd5b0] to [245a731876].

183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
	/**
	 * Inclure un fichier depuis le plugin (dynamique ou statique)
	 * @param  string $file Chemin du fichier à aller chercher : si c'est un .php il sera inclus,
	 * sinon il sera juste affiché
	 * @return void
	 * @throws UserException Si le fichier n'existe pas ou fait partie des fichiers qui ne peuvent
	 * être appelés que par des méthodes de Plugin.
	 * @throws RuntimeException Si le chemin indiqué tente de sortir du contexte du PHAR
	 */
	public function call($file)
	{
		$file = preg_replace('!^[./]*!', '', $file);

		if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file))
		{







|







183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
	/**
	 * Inclure un fichier depuis le plugin (dynamique ou statique)
	 * @param  string $file Chemin du fichier à aller chercher : si c'est un .php il sera inclus,
	 * sinon il sera juste affiché
	 * @return void
	 * @throws UserException Si le fichier n'existe pas ou fait partie des fichiers qui ne peuvent
	 * être appelés que par des méthodes de Plugin.
	 * @throws \RuntimeException Si le chemin indiqué tente de sortir du contexte du PHAR
	 */
	public function call($file)
	{
		$file = preg_replace('!^[./]*!', '', $file);

		if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file))
		{
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
				'{Membres::DROIT_ECRITURE}' => Membres::DROIT_ECRITURE,
				'{Membres::DROIT_ADMIN}' => Membres::DROIT_ADMIN,
			]);

			$condition = preg_replace_callback('/\{\$user\.(\w+)\}/', function ($m) use ($user) { return $user->{$m[1]}; }, $condition);
			$query = 'SELECT 1 WHERE ' . $condition . ';';

			$st = $db->userSelectStatement($query);
			$res = $st->execute();

			if (!$res->fetchArray(\SQLITE3_NUM))
			{
				unset($list[$id]);
				continue;
			}

			$row = $row->nom;
		}







<
|

|







439
440
441
442
443
444
445

446
447
448
449
450
451
452
453
454
455
				'{Membres::DROIT_ECRITURE}' => Membres::DROIT_ECRITURE,
				'{Membres::DROIT_ADMIN}' => Membres::DROIT_ADMIN,
			]);

			$condition = preg_replace_callback('/\{\$user\.(\w+)\}/', function ($m) use ($user) { return $user->{$m[1]}; }, $condition);
			$query = 'SELECT 1 WHERE ' . $condition . ';';


			$res = $db->protectSelect(['membres' => []], $query);

			if (!$db->firstColumn($query))
			{
				unset($list[$id]);
				continue;
			}

			$row = $row->nom;
		}
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551





552
553
554
555
556
557
558
					'disable_compression'	=>	true,
				]
			];

			$context = stream_context_create($context_options);

			try {
				$result = file_get_contents(PLUGINS_URL, NULL, $context);
			}
			catch (\Exception $e)
			{
				throw new UserException('Le téléchargement de la liste des plugins a échoué : ' . $e->getMessage());
			}

			Static_Cache::store('plugins_list', $result);
		}
		else
		{
			$result = Static_Cache::get('plugins_list');
		}

		$list = json_decode($result, true);
		return $list;
	}






	/**
	 * Vérifier le hash du plugin $id pour voir s'il correspond au hash du fichier téléchargés
	 * @param  string $id Identifiant du plugin
	 * @return boolean    TRUE si le hash correspond (intégrité OK), sinon FALSE
	 */
	static public function checkHash($id)







|
















>
>
>
>
>







527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
					'disable_compression'	=>	true,
				]
			];

			$context = stream_context_create($context_options);

			try {
				$result = file_get_contents(PLUGINS_URL, false, $context);
			}
			catch (\Exception $e)
			{
				throw new UserException('Le téléchargement de la liste des plugins a échoué : ' . $e->getMessage());
			}

			Static_Cache::store('plugins_list', $result);
		}
		else
		{
			$result = Static_Cache::get('plugins_list');
		}

		$list = json_decode($result, true);
		return $list;
	}

	static public function fetchOfficialList()
	{
		return []; // FIXME
	}

	/**
	 * Vérifier le hash du plugin $id pour voir s'il correspond au hash du fichier téléchargés
	 * @param  string $id Identifiant du plugin
	 * @return boolean    TRUE si le hash correspond (intégrité OK), sinon FALSE
	 */
	static public function checkHash($id)
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
		return array_key_exists($id, $list);
	}

	/**
	 * Télécharge un plugin depuis le repository officiel, et l'installe
	 * @param  string $id Identifiant du plugin
	 * @return boolean    TRUE si ça marche
	 * @throws LogicException Si le plugin n'est pas dans la liste des plugins officiels
	 * @throws UserException Si le plugin est déjà installé ou que le téléchargement a échoué
	 * @throws RuntimeException Si l'archive téléchargée est corrompue (intégrité du hash ne correspond pas)
	 */
	static public function download($id)
	{
		$list = self::fetchOfficialList();

		if (!array_key_exists($id, $list))
		{







|

|







582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
		return array_key_exists($id, $list);
	}

	/**
	 * Télécharge un plugin depuis le repository officiel, et l'installe
	 * @param  string $id Identifiant du plugin
	 * @return boolean    TRUE si ça marche
	 * @throws \LogicException Si le plugin n'est pas dans la liste des plugins officiels
	 * @throws UserException Si le plugin est déjà installé ou que le téléchargement a échoué
	 * @throws \RuntimeException Si l'archive téléchargée est corrompue (intégrité du hash ne correspond pas)
	 */
	static public function download($id)
	{
		$list = self::fetchOfficialList();

		if (!array_key_exists($id, $list))
		{
756
757
758
759
760
761
762




763
764
765
766
767


768
769
770
771
772
773


774
775
776
777
778
779
780
781
782
783
784
	static public function fireSignal($signal, $params = null, &$callback_return = null)
	{
		$list = DB::getInstance()->get('SELECT * FROM plugins_signaux WHERE signal = ?;', $signal);

		if (!count($list)) {
			return null;
		}





		$system = explode(',', PLUGINS_SYSTEM);

		foreach ($list as $row)
		{


			// Ne pas appeler les plugins dont le code n'existe pas/plus,
			// SAUF si c'est un plugin système (auquel cas ça fera une erreur)
			if (!self::getPath($row->plugin, in_array($row->plugin, $system)))
			{
				continue;
			}



			$return = call_user_func_array('Garradin\\Plugin\\' . $row->callback, [&$params, &$callback_return]);

			if (true === $return) {
				return true;
			}
		}

		return false;
	}
}







>
>
>
>





>
>


|



>
>











760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
	static public function fireSignal($signal, $params = null, &$callback_return = null)
	{
		$list = DB::getInstance()->get('SELECT * FROM plugins_signaux WHERE signal = ?;', $signal);

		if (!count($list)) {
			return null;
		}

		if (null === $params) {
			$params = [];
		}

		$system = explode(',', PLUGINS_SYSTEM);

		foreach ($list as $row)
		{
			$path = self::getPath($row->plugin, in_array($row->plugin, $system));

			// Ne pas appeler les plugins dont le code n'existe pas/plus,
			// SAUF si c'est un plugin système (auquel cas ça fera une erreur)
			if (!$path)
			{
				continue;
			}

			$params['plugin_root'] = $path;

			$return = call_user_func_array('Garradin\\Plugin\\' . $row->callback, [&$params, &$callback_return]);

			if (true === $return) {
				return true;
			}
		}

		return false;
	}
}

Deleted src/include/lib/Garradin/Rappels_Envoyes.php version [fcaa354186].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<?php

namespace Garradin;

class Rappels_Envoyes
{
	/**
	 * Types de médias
	 */
	const MEDIA_EMAIL = 1;
	const MEDIA_COURRIER = 2;
	const MEDIA_TELEPHONE = 3;
	const MEDIA_AUTRE = 4;

	/**
	 * Nombre d'items par page dans les listes
	 */
	const ITEMS_PER_PAGE = 50;

	/**
	 * Vérification des champs fournis pour la modification de donnée
	 * @param  array $data Tableau contenant les champs à ajouter/modifier
	 * @return void
	 */
	protected function _checkFields(&$data)
	{
		$db = DB::getInstance();

		if (!array_key_exists('id_rappel', $data) 
			|| (!is_null($data['id_rappel']) && (empty($data['id_rappel']) || !$db->firstColumn('SELECT 1 FROM rappels WHERE id = ?;', (int) $data['id_rappel']))))
		{
			throw new \LogicException('ID rappel non fourni ou inexistant dans la table rappels');
		}

        if (isset($data['id_cotisation']))
        {
        	if (!$db->firstColumn('SELECT 1 FROM cotisations WHERE id = ?;', (int) $data['id_cotisation']))
	        {
	            throw new UserException('Cotisation inconnue.');
	        }

	        $data['id_cotisation'] = (int) $data['id_cotisation'];
	    }

        if (empty($data['id_membre'])
        	|| !$db->firstColumn('SELECT 1 FROM membres WHERE id = ?;', (int) $data['id_membre']))
        {
            throw new UserException('Membre inconnu.');
        }

		$data['id_membre'] = (int) $data['id_membre'];

		if (empty($data['media']) || !is_numeric($data['media']) 
			|| !in_array((int)$data['media'], [self::MEDIA_EMAIL, self::MEDIA_COURRIER, self::MEDIA_TELEPHONE, self::MEDIA_AUTRE]))
		{
			throw new UserException('Média invalide.');
		}

		$data['media'] = (int) $data['media'];

		if (empty($data['date']) || !Utils::checkDate($data['date']))
		{
			throw new UserException('La date indiquée n\'est pas valide.');
		}
	}

	/**
	 * Enregistrer un rappel
	 * @param array $data Données du rappel
	 * @return integer Numéro ID du rappel créé
	 */
	public function add($data)
	{
		$db = DB::getInstance();

		$this->_checkFields($data);

		$db->insert('rappels_envoyes', $data);

		return $db->lastInsertRowId();
	}

	/**
	 * Supprimer un rappel enregistré
	 * @param  integer $id Numéro du rappel
	 * @return boolean     TRUE en cas de succès
	 */
	public function delete($id)
	{
		$db = DB::getInstance();
		$db->delete('rappels_envoyes', $db->where('id', (int) $id));
		return true;
	}

	/**
	 * Renvoie les données sur un rappel
	 * @param  integer $id Numéro du rappel
	 * @return array     Données du rappel
	 */
	public function get($id)
	{
		return DB::getInstance()->first('SELECT * FROM rappels_envoyes WHERE id = ?;', (int)$id);
	}

	/**
	 * Remplacer les tags dans le contenu/sujet du mail
	 * @param  string $content Chaîne à traiter
	 * @param  array  $data    Données supplémentaires à utiliser comme tags (tableau associatif)
	 * @return string          $content dont les tags ont été remplacés par le contenu correct
	 */
	public function replaceTagsInContent($content, $data = null)
	{
		$config = Config::getInstance();
		$tags = [
			'#NOM_ASSO'		=>	$config->get('nom_asso'),
			'#ADRESSE_ASSO'	=>	$config->get('adresse_asso'),
			'#EMAIL_ASSO'	=>	$config->get('email_asso'),
			'#SITE_ASSO'	=>	$config->get('site_asso'),
			'#URL_RACINE'	=>	WWW_URL,
			'#URL_SITE'		=>	WWW_URL,
			'#URL_ADMIN'	=>	ADMIN_URL,
		];

		if (!empty($data) && is_array($data))
		{
			foreach ($data as $key=>$value)
			{
				$key = '#' . strtoupper($key);
				$tags[$key] = $value;
			}
		}

		return strtr($content, $tags);
	}

	/**
	 * Envoi de mail pour rappel automatisé
	 * @param  array $data Données du rappel automatisé
	 * @return boolean     TRUE
	 */
	public function sendAuto($data)
	{
		$replace = (array) $data;
		$replace['date_rappel'] = Utils::sqliteDateToFrench($replace['date_rappel']);
		$replace['date_expiration'] = Utils::sqliteDateToFrench($replace['expiration']);
		$replace['nb_jours'] = abs($replace['nb_jours']);
		$replace['delai'] = abs($replace['delai']);

		$subject = $this->replaceTagsInContent($data->sujet, $replace);
		$text = $this->replaceTagsInContent($data->texte, $replace);

		// Envoi du mail
		Utils::sendEmail(Utils::EMAIL_CONTEXT_PRIVATE, $data->email, $subject, $text, $data->id);

		// Enregistrement en DB
		$this->add([
			'id_cotisation' => $data->id_cotisation,
			'id_membre'     => $data->id,
			'id_rappel'     => $data->id_rappel,
			'media'         => Rappels_Envoyes::MEDIA_EMAIL,
			// On enregistre la date de mise en œuvre du rappel
			// et non pas la date d'envoi effective du rappel
			// car l'envoi du rappel peut ne pas être effectué
			// le jour où il aurait dû être envoyé (la magie des cron)
			'date'          => $data->date_rappel,
		]);

		Plugin::fireSignal('rappels.auto', $data);

		return true;
	}

	/**
	 * Liste des rappels envoyés à un membre
	 * @param integer $id Numéro du membre
	 * @return array Liste des rappels
	 */
	public function listForMember($id)
	{
		return DB::getInstance()->get('SELECT
			re.*, c.intitule, c.montant
			FROM rappels_envoyes AS re 
				INNER JOIN cotisations AS c ON c.id = re.id_cotisation 
			WHERE re.id_membre = ?
			ORDER BY re.date DESC;', (int)$id);
	}

	/**
	 * Liste des rappels pour une cotisation donnée
	 * @param  integer $id Numéro de la cotisation
	 * @param  integer $page Numéro de page de liste
	 * @return array     Liste des rappels
	 */
	public function listForCotisation($id, $page = 1)
	{
		$begin = ($page - 1) * self::ITEMS_PER_PAGE;

		return DB::getInstance()->get('SELECT * FROM rappels_envoyes
			WHERE id_rappel IN (SELECT id FROM rappels WHERE id_cotisation = ?)
			ORDER BY date DESC
			LIMIT ?, ?;', (int)$id, $begin, self::ITEMS_PER_PAGE);
	}

	/**
	 * Nombre de rappels pour une cotisation donnée
	 * @param  integer $id Numéro de la cotisation
	 * @return integer Nombre de rappels envoyés
	 */
	public function countForCotisation($id)
	{
		return DB::getInstance()->firstColumn('SELECT COUNT(*) FROM rappels_envoyes
			WHERE id_rappel IN (SELECT id FROM rappels WHERE id_cotisation = ?);',
			(int)$id);
	}

	/**
	 * Liste des rappels envoyés pour un rappel automatique
	 * @param  integer $id Numéro du rappel
	 * @param  integer $page Numéro de page de liste
	 * @return array Liste des rappels envoyés
	 */
	public function listForRappel($id, $page = 1)
	{
		$begin = ($page - 1) * self::ITEMS_PER_PAGE;

		return DB::getInstance()->get('SELECT * FROM rappels_envoyes 
			WHERE id_rappel = ? ORDER BY date DESC LIMIT ?,?;',
			(int)$id, (int)$begin, self::ITEMS_PER_PAGE);
	}

	/**
	 * Nombre de rappels envoyés pour un rappel automatique
	 * @param  integer $id Numéro du rappel
	 * @return integer Nombre de rappels envoyés pour ce rappel
	 */
	public function countForRappel($id)
	{
		return DB::getInstance()->firstColumn('SELECT COUNT(*) FROM rappels_envoyes 
			WHERE id_rappel = ?;', (int)$id);
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































































































































































































































































































































































































Modified src/include/lib/Garradin/Recherche.php from [98002e1aea] to [5f2af851fc].

1
2
3
4


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;



class Recherche
{
	const TYPE_JSON = 'json';
	const TYPE_SQL = 'sql';

	const TARGETS = [
		'membres',
		'compta_journal',
	];

	protected function _checkFields($data)
	{
		$db = DB::getInstance();

		if (array_key_exists('intitule', $data) && trim($data['intitule']) === '')




>
>







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace Garradin;

use Garradin\Entities\Accounting\Transaction;

class Recherche
{
	const TYPE_JSON = 'json';
	const TYPE_SQL = 'sql';

	const TARGETS = [
		'membres',
		'compta',
	];

	protected function _checkFields($data)
	{
		$db = DB::getInstance();

		if (array_key_exists('intitule', $data) && trim($data['intitule']) === '')
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77






78
79
80
81
82
83
84
				throw new \InvalidArgumentException('Recherche invalide pour le type SQL');
			}

			$query = $data['contenu'];

			if ($data['type']  == self::TYPE_JSON)
			{
				if (!is_array($query))
				{
					throw new \InvalidArgumentException('Recherche invalide pour le type JSON');
				}

				$query = json_encode($query);

				if (!json_decode($query))
				{
					throw new \InvalidArgumentException('JSON invalide pour le type JSON');
				}
			}
		}

		return $query;
	}







	public function edit($id, $data)
	{
		$allowed = ['intitule', 'id_membre', 'type', 'cible', 'contenu'];

		// Supprimer les champs qui ne sont pas ceux de la BDD
		$data = array_intersect_key($data, array_flip($allowed));







|















>
>
>
>
>
>







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
				throw new \InvalidArgumentException('Recherche invalide pour le type SQL');
			}

			$query = $data['contenu'];

			if ($data['type']  == self::TYPE_JSON)
			{
				if (!is_object($query))
				{
					throw new \InvalidArgumentException('Recherche invalide pour le type JSON');
				}

				$query = json_encode($query);

				if (!json_decode($query))
				{
					throw new \InvalidArgumentException('JSON invalide pour le type JSON');
				}
			}
		}

		return $query;
	}

	public function duplicate(int $id)
	{
		DB::getInstance()->preparedQuery('INSERT INTO recherches (id_membre, intitule, cible, type, contenu)
			SELECT id_membre, \'Copie de : \' || intitule, cible, type, contenu FROM recherches WHERE id = ?;', [$id]);
	}

	public function edit($id, $data)
	{
		$allowed = ['intitule', 'id_membre', 'type', 'cible', 'contenu'];

		// Supprimer les champs qui ne sont pas ceux de la BDD
		$data = array_intersect_key($data, array_flip($allowed));
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
		return DB::getInstance()->delete('recherches', 'id = ?', (int) $id);
	}

	public function get($id)
	{
		$r = DB::getInstance()->first('SELECT * FROM recherches WHERE id = ?;', (int) $id);

		if ($r && $r->type == self::TYPE_JSON)
		{
			$q = json_decode($r->contenu, true);

			$r->query = $q['query'];
			$r->order = $q['order'];
			$r->desc = $q['desc'];
			$r->limit = $q['limit'];

			unset($q);
		}

		return $r;
	}

	public function getList($id_membre, $cible)
	{







|
<
|
<
<
<
<
<
<
<







118
119
120
121
122
123
124
125

126







127
128
129
130
131
132
133
		return DB::getInstance()->delete('recherches', 'id = ?', (int) $id);
	}

	public function get($id)
	{
		$r = DB::getInstance()->first('SELECT * FROM recherches WHERE id = ?;', (int) $id);

		if ($r && $r->type == self::TYPE_JSON) {

			$r->query = (object) json_decode($r->contenu, true);







		}

		return $r;
	}

	public function getList($id_membre, $cible)
	{
146
147
148
149
150
151
152

153
154
155
156
157























158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
		if (!$search)
		{
			return false;
		}

		if ($search->type == self::TYPE_JSON)
		{

			$search->contenu = $this->buildQuery($search->cible, $search->query, $search->order, $search->desc, $no_limit ? 10000 : $search->limit);
		}

		return $this->searchSQL($search->cible, $search->contenu, $force_select, $no_limit);
	}
























	/**
	 * Renvoie la liste des colonnes d'une cible
	 */
	public function getColumns($target)
	{
		$columns = [];
		$db = DB::getInstance();

		if ($target == 'membres')
		{
			$champs = Config::getInstance()->get('champs_membres');

			$columns['id_categorie'] = (object) [
					'realType' => 'select',
					'textMatch'=> false,
					'label'    => 'Catégorie',
					'type'     => 'enum',
					'null'     => false,
					'values'   => $db->getAssoc('SELECT id, nom FROM membres_categories ORDER BY nom;'),
				];

			foreach ($champs->getList() as $champ => $config)
			{
				$column = (object) [
					'realType' => $config->type,
					'textMatch'=> $champs->isText($champ),
					'label'    => $config->title,
					'type'     => 'text',
					'null'     => true,
				];

				if ($config->type == 'checkbox')







>
|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>














<










<







146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195

196
197
198
199
200
201
202
203
204
205

206
207
208
209
210
211
212
		if (!$search)
		{
			return false;
		}

		if ($search->type == self::TYPE_JSON)
		{
			$query = $search->query;
			$search->contenu = $this->buildQuery($search->cible, $query->query, $query->order, $query->desc, $no_limit ? 10000 : $query->limit);
		}

		return $this->searchSQL($search->cible, $search->contenu, $force_select, $no_limit);
	}

	public function getResultHeader(string $target, array $result)
	{
		if (!count($result)) {
			return [];
		}

		$out = [];
		$columns = $this->getColumns($target);

		foreach (reset($result) as $key => $v) {
			foreach ($columns as $ckey => $config) {
				if ($ckey == $key) {
					$out[$key] = $config->label;
				}
				elseif (isset($config->alias) && $config->alias == $key) {
					$out[$config->alias] = $config->label;
				}
			}
		}

		return $out;
	}

	/**
	 * Renvoie la liste des colonnes d'une cible
	 */
	public function getColumns($target)
	{
		$columns = [];
		$db = DB::getInstance();

		if ($target == 'membres')
		{
			$champs = Config::getInstance()->get('champs_membres');

			$columns['id_categorie'] = (object) [

					'textMatch'=> false,
					'label'    => 'Catégorie',
					'type'     => 'enum',
					'null'     => false,
					'values'   => $db->getAssoc('SELECT id, nom FROM membres_categories ORDER BY nom;'),
				];

			foreach ($champs->getList() as $champ => $config)
			{
				$column = (object) [

					'textMatch'=> $champs->isText($champ),
					'label'    => $config->title,
					'type'     => 'text',
					'null'     => true,
				];

				if ($config->type == 'checkbox')
205
206
207
208
209
210
211
212




213
214













































































































215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243





244
245
246
247
248
249
250
				{
					$column->type = $config->type;
				}
				elseif ($config->type == 'number' || $champ == 'numero')
				{
					$column->type = 'integer';
				}





				$columns[$champ] = $column;
			}













































































































		}

		return $columns;
	}

	/**
	 * Construire une recherche SQL à partir d'un objet généré par QueryBuilder
	 * @param  string  $target Cible de la requête : membres, compta_journal, etc.
	 * @param  array   $groups Groupes de critères
	 * @param  string  $order  Ordre de tri
	 * @param  boolean $desc   Inverser le tri
	 * @param  integer $limit  Limite
	 * @return string Chaîne SQL
	 */
	public function buildQuery($target, array $groups, $order, $desc = false, $limit = 100)
	{
		if (!in_array($target, self::TARGETS, true))
		{
			throw new \InvalidArgumentException('Cible inconnue : ' . $target);
		}

		if ($target == 'membres')
		{
			$config = Config::getInstance();
			$champs = $config->get('champs_membres');
		}

		$db = DB::getInstance();
		$target_columns = $this->getColumns($target);





		$query_columns = [];

		$query_groups = [];

		static $no_transform_operators = ['IS NULL', 'IS NOT NULL', '= 0', '= 1', '&'];

		foreach ($groups as $group)








>
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>














|














>
>
>
>
>







227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
				{
					$column->type = $config->type;
				}
				elseif ($config->type == 'number' || $champ == 'numero')
				{
					$column->type = 'integer';
				}

				if ($config->type == 'tel') {
					$column->originalType = 'tel';
				}

				$columns[$champ] = $column;
			}
		}
		elseif ($target === 'compta') {
			$columns['t.id'] = (object) [
				'textMatch'=> false,
				'label'    => 'Numéro écriture',
				'type'     => 'integer',
				'null'     => false,
				'alias'    => 'id',
			];

			$columns['t.date'] = (object) [
				'textMatch'=> false,
				'label'    => 'Date',
				'type'     => 'date',
				'null'     => false,
				'alias'    => 'date',
			];

			$columns['t.label'] = (object) [
				'textMatch'=> true,
				'label'    => 'Libellé écriture',
				'type'     => 'text',
				'null'     => false,
				'alias'    => 'label',
			];

			$columns['t.reference'] = (object) [
				'textMatch'=> true,
				'label'    => 'Numéro pièce comptable',
				'type'     => 'text',
				'null'     => true,
				'alias'    => 'reference',
			];

			$columns['t.notes'] = (object) [
				'textMatch'=> true,
				'label'    => 'Notes',
				'type'     => 'text',
				'null'     => true,
				'alias'    => 'notes',
			];

			$columns['l.label'] = (object) [
				'textMatch'=> true,
				'label'    => 'Libellé ligne',
				'type'     => 'text',
				'null'     => true,
				'alias'    => 'line_label',
			];

			$columns['l.debit'] = (object) [
				'textMatch'=> false,
				'label'    => 'Débit',
				'type'     => 'integer',
				'null'     => false,
				'alias'    => 'debit',
				'originalType' => 'money',
			];

			$columns['l.credit'] = (object) [
				'textMatch'=> false,
				'label'    => 'Crédit',
				'type'     => 'integer',
				'null'     => false,
				'alias'    => 'credit',
				'originalType' => 'money',
			];

			$columns['l.reference'] = (object) [
				'textMatch'=> true,
				'label'    => 'Référence ligne écriture',
				'type'     => 'text',
				'null'     => true,
				'alias'    => 'line_reference',
			];

			$columns['t.type'] = (object) [
				'textMatch'=> false,
				'label'    => 'Type d\'écriture',
				'type'     => 'enum',
				'null'     => false,
				'values'   => Transaction::TYPES_NAMES,
				'alias'    => 'type',
			];

			$columns['a.code'] = (object) [
				'textMatch'=> true,
				'label'    => 'Numéro de compte',
				'type'     => 'text',
				'null'     => false,
				'alias'    => 'code',
			];

			$columns['t.id_year'] = (object) [
				'textMatch'=> false,
				'label'    => 'Exercice',
				'type'     => 'enum',
				'null'     => false,
				'values'   => $db->getAssoc('SELECT id, label FROM acc_years ORDER BY end_date;'),
				'alias'    => 'id_year',
			];

			$columns['a2.code'] = (object) [
				'textMatch'=> true,
				'label'    => 'N° de compte projet',
				'type'     => 'text',
				'null'     => true,
				'alias'    => 'id_analytical',
			];
		}

		return $columns;
	}

	/**
	 * Construire une recherche SQL à partir d'un objet généré par QueryBuilder
	 * @param  string  $target Cible de la requête : membres, compta_journal, etc.
	 * @param  array   $groups Groupes de critères
	 * @param  string  $order  Ordre de tri
	 * @param  boolean $desc   Inverser le tri
	 * @param  integer $limit  Limite
	 * @return string Chaîne SQL
	 */
	public function buildQuery(string $target, array $groups, string $order, bool $desc = false, int $limit = 100)
	{
		if (!in_array($target, self::TARGETS, true))
		{
			throw new \InvalidArgumentException('Cible inconnue : ' . $target);
		}

		if ($target == 'membres')
		{
			$config = Config::getInstance();
			$champs = $config->get('champs_membres');
		}

		$db = DB::getInstance();
		$target_columns = $this->getColumns($target);

		if (!isset($target_columns[$order])) {
			throw new UserException('Colonne de tri inconnue : ' . $order);
		}

		$query_columns = [];

		$query_groups = [];

		static $no_transform_operators = ['IS NULL', 'IS NOT NULL', '= 0', '= 1', '&'];

		foreach ($groups as $group)
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294

295
296
297
298




299
300
301
302
303
304
305
				}

				if (!array_key_exists($condition['column'], $target_columns))
				{
					// Ignorer une condition qui se rapporte à une colonne
					// qui n'existe pas, cas possible si on reprend une recherche
					// après avoir modifié les fiches de membres
					throw new UserException('Cette recherche fait référence à un champ qui n\'existe plus dans les fiches de membres.');
				}

				$query_columns[] = $condition['column'];
				$column = $target_columns[$condition['column']];

				if ($column->textMatch == 'text' && !in_array($condition['operator'], $no_transform_operators))
				{
					$query = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE %s', $db->quoteIdentifier($condition['column']), $condition['operator']);
				}
				else
				{
					$query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']);
				}

				$values = isset($condition['values']) ? $condition['values'] : [];

				$values = array_map(['Garradin\Utils', 'transliterateToAscii'], $values);


				if ($column->type == 'tel')
				{
					// Normaliser le numéro de téléphone
					$values = array_map(['Garradin\Utils', 'normalizePhoneNumber'], $values);




				}

				// L'opérateur binaire est un peu spécial
				if ($condition['operator'] == '&')
				{
					$new_query = [];








|


















>
|
<
|
|
>
>
>
>







409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436

437
438
439
440
441
442
443
444
445
446
447
448
449
				}

				if (!array_key_exists($condition['column'], $target_columns))
				{
					// Ignorer une condition qui se rapporte à une colonne
					// qui n'existe pas, cas possible si on reprend une recherche
					// après avoir modifié les fiches de membres
					throw new UserException('Cette recherche fait référence à une colonne qui n\'existe pas : ' . $condition['column']);
				}

				$query_columns[] = $condition['column'];
				$column = $target_columns[$condition['column']];

				if ($column->textMatch == 'text' && !in_array($condition['operator'], $no_transform_operators))
				{
					$query = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE %s', $db->quoteIdentifier($condition['column']), $condition['operator']);
				}
				else
				{
					$query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']);
				}

				$values = isset($condition['values']) ? $condition['values'] : [];

				$values = array_map(['Garradin\Utils', 'transliterateToAscii'], $values);

				if (!empty($column->originalType)) {
					if ($column->originalType == 'tel') {

						// Normaliser le numéro de téléphone
						$values = array_map(['Garradin\Utils', 'normalizePhoneNumber'], $values);
					}
					elseif ($column->originalType == 'money') {
						$values = array_map(['Garradin\Utils', 'moneyToInteger'], $values);
					}
				}

				// L'opérateur binaire est un peu spécial
				if ($condition['operator'] == '&')
				{
					$new_query = [];

315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
				{
					$values = array_map([$db, 'quote'], $values);
					$query = str_replace('??', implode(', ', $values), $query);
				}
				// Remplacement de recherche LIKE
				elseif (preg_match('/%\?%|%\?|\?%/', $query, $match))
				{
					$value = str_replace(['%_'], ['\\%', '\\_'], reset($values));
					$value = str_replace('?', $value, $match[0]);
					$query = str_replace($match[0], sprintf('%s ESCAPE \'\\\'', $db->quote($value)), $query);
				}
				// Remplacement de paramètre
				elseif (strpos($query, '?') !== false)
				{
					$expected = substr_count($query, '?');







|







459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
				{
					$values = array_map([$db, 'quote'], $values);
					$query = str_replace('??', implode(', ', $values), $query);
				}
				// Remplacement de recherche LIKE
				elseif (preg_match('/%\?%|%\?|\?%/', $query, $match))
				{
					$value = str_replace(['%', '_'], ['\\%', '\\_'], reset($values));
					$value = str_replace('?', $value, $match[0]);
					$query = str_replace($match[0], sprintf('%s ESCAPE \'\\\'', $db->quote($value)), $query);
				}
				// Remplacement de paramètre
				elseif (strpos($query, '?') !== false)
				{
					$expected = substr_count($query, '?');
352
353
354
355
356
357
358
359
360
361





362
363
364
365
366
367
368
369
370
371
372
373
374
375
376


377


378
379
380
381
382
383








384






385
386
387
388
389
390
391

		if (!count($query_groups))
		{
			throw new UserException('Aucune clause trouvée dans la recherche.');
		}

		// Ajout du champ identité si pas présent
		if ($target == 'membres' && !in_array($config->get('champ_identite'), $query_columns))
		{
			array_unshift($query_columns, $config->get('champ_identite'));





		}

		$query_columns[] = $order;

		if ($target_columns[$order]->textMatch)
		{
			$order = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE', $db->quoteIdentifier($order));
		}
		else
		{
			$order = $db->quoteIdentifier($order);
		}

		$query_columns = array_unique($query_columns);
		$query_columns = array_map([$db, 'quoteIdentifier'], $query_columns);





		$sql_query = sprintf('SELECT id, %s FROM %s WHERE %s ORDER BY %s %s LIMIT %d;',
			implode(', ', $query_columns),
			$target,
			'(' . implode(') AND (', $query_groups) . ')',
			$order,
			$desc ? 'DESC' : 'ASC',








			(int) $limit);







		return $sql_query;
	}

	/**
	 * Lancer une recherche SQL
	 */







|

|
>
>
>
>
>














|
>
>
|
>
>
|
|
|
|
|
|
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>







496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558

		if (!count($query_groups))
		{
			throw new UserException('Aucune clause trouvée dans la recherche.');
		}

		// Ajout du champ identité si pas présent
		if ($target == 'membres')
		{
			$query_columns = array_merge(['id', $config->get('champ_identite')], $query_columns);
		}
		// Ajout de champs compta si pas présents
		elseif ($target == 'compta')
		{
			$query_columns = array_merge(['t.id', 't.date', 't.label', 'l.debit', 'l.credit', 'a.code'], $query_columns);
		}

		$query_columns[] = $order;

		if ($target_columns[$order]->textMatch)
		{
			$order = sprintf('transliterate_to_ascii(%s) COLLATE NOCASE', $db->quoteIdentifier($order));
		}
		else
		{
			$order = $db->quoteIdentifier($order);
		}

		$query_columns = array_unique($query_columns);
		$query_columns = array_map(function ($column) use ($target_columns, $db) {
			if (isset($target_columns[$column]->alias)) {
				return sprintf('%s AS %s', $db->quoteIdentifier($column), $db->quote($target_columns[$column]->alias));
			}
			return $db->quoteIdentifier($column);
		}, $query_columns);

		$query_columns = implode(', ', $query_columns);

		$query_groups = '(' . implode(') AND (', $query_groups) . ')';

		$desc = $desc ? 'DESC' : 'ASC';

		if ('compta' === $target) {
			$sql_query = sprintf('SELECT %s
				FROM acc_transactions AS t
				INNER JOIN acc_transactions_lines AS l ON l.id_transaction = t.id
				INNER JOIN acc_accounts AS a ON l.id_account = a.id
				LEFT JOIN acc_accounts AS a2 ON l.id_analytical = a2.id
				WHERE %s GROUP BY t.id ORDER BY %s %s LIMIT %d;',
				$query_columns, $query_groups, $order, $desc, (int) $limit);
			$sql_query = preg_replace('/"(a|a2|l|t)\./', '"$1"."', $sql_query);
		}
		else {
			$sql_query = sprintf('SELECT id, %s FROM %s WHERE %s ORDER BY %s %s LIMIT %d;',
				$query_columns, $target, $query_groups, $order, $desc, (int) $limit);
		}

		return $sql_query;
	}

	/**
	 * Lancer une recherche SQL
	 */
404
405
406
407
408
409
410
411







412
413
414
415
416
417
418
419
420
421
422
423
424











































425
426
427
428
429
430
431
432
433






434
435
436
437
438
439
		if (!$no_limit && !preg_match('/LIMIT\s+\d+/i', $query))
		{
			$query = preg_replace('/;?\s*$/', '', $query);
			$query .= ' LIMIT 100';
		}

		try {
			return DB::getInstance()->userSelectGet($query);







		}
		catch (\Exception $e) {
			$message = 'Erreur dans la requête : ' . $e->getMessage();

			if (null !== $force_select)
			{
				$message .= "\nVérifiez que votre requête sélectionne bien les colonnes suivantes : " . implode(', ', $force_select);
			}

			throw new UserException($message);
		}
	}












































	public function schema($target)
	{
		$db = DB::getInstance();

		if ($target == 'membres')
		{
			$tables = [
				'membres'   =>  $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'),
				'categories'=>  $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'),






			];
		}

		return $tables;
	}
}







|
>
>
>
>
>
>
>













>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|
<

|
|
>
>
>
>
>
>






571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646

647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
		if (!$no_limit && !preg_match('/LIMIT\s+\d+/i', $query))
		{
			$query = preg_replace('/;?\s*$/', '', $query);
			$query .= ' LIMIT 100';
		}

		try {
			$db = DB::getInstance();
			static $allowed = [
				'compta' => ['acc_transactions' => null, 'acc_transactions_lines' => null, 'acc_accounts' => null],
				'membres' => ['membres' => null],
			];

			$db->protectSelect($allowed[$target], $query);
			return $db->get($query);
		}
		catch (\Exception $e) {
			$message = 'Erreur dans la requête : ' . $e->getMessage();

			if (null !== $force_select)
			{
				$message .= "\nVérifiez que votre requête sélectionne bien les colonnes suivantes : " . implode(', ', $force_select);
			}

			throw new UserException($message);
		}
	}

	public function searchQuery(string $table, $query, $order, $desc = false, $limit = 100)
	{
        $sql_query = $this->buildQuery($table, $query, $order, $desc, $limit);
        return $this->searchSQL($table, $sql_query);
	}

	public function buildSimpleMemberQuery(string $query)
	{
	    $operator = 'LIKE %?%';

	    if (is_numeric(trim($query)))
	    {
	        $column = 'numero';
	        $operator = '= ?';
	    }
	    elseif (strpos($query, '@') !== false)
	    {
	        $column = 'email';
	    }
	    else
	    {
	        $column = Config::getInstance()->get('champ_identite');
	    }

	    $query = [[
	        'operator' => 'AND',
	        'conditions' => [
	            [
	                'column'   => $column,
	                'operator' => $operator,
	                'values'   => [$query],
	            ],
	        ],
	    ]];

	    return (object) [
	    	'query' => $query,
	    	'order' => $column,
	    	'desc' => false,
	    	'limit' => 50,
	    ];
	}

	public function schema($target)
	{
		$db = DB::getInstance();

		if ($target == 'membres') {

			$tables = [
				'membres'    => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'),
				'categories' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'),
			];
		}
		elseif ($target == 'compta') {
			$tables = [
				'acc_transactions'       => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'acc_transactions\';'),
				'acc_transactions_lines' => $db->firstColumn('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'acc_transactions_lines\';'),
			];
		}

		return $tables;
	}
}

Modified src/include/lib/Garradin/Sauvegarde.php from [adf221aa11] to [7f664d6dd4].

179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
	}

	/**
	 * Restaure une sauvegarde locale
	 * @param  string $file Le nom de fichier à utiliser comme point de restauration
	 * @return boolean true si la restauration a fonctionné, false sinon
	 */
	public function restoreFromLocal($file)
	{
		if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._ -]+$!iu', $file))
		{
			throw new UserException('Nom de fichier non valide.');
		}

		if (!file_exists(DATA_ROOT . '/' . $file))
		{
			throw new UserException('Le fichier fourni n\'existe pas.');
		}

		return $this->restoreDB(DATA_ROOT . '/' . $file);
	}

	/**
	 * Restaure une copie distante (fichier envoyé)
	 * @param  array   $file    Tableau provenant de $_FILES
	 * @param  integer $user_id ID du membre actuellement connecté, utilisé pour 
	 * vérifier qu'il est toujours administrateur dans la sauvegarde







|











|







179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
	}

	/**
	 * Restaure une sauvegarde locale
	 * @param  string $file Le nom de fichier à utiliser comme point de restauration
	 * @return boolean true si la restauration a fonctionné, false sinon
	 */
	public function restoreFromLocal($file, bool $do_backup = true)
	{
		if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._ -]+$!iu', $file))
		{
			throw new UserException('Nom de fichier non valide.');
		}

		if (!file_exists(DATA_ROOT . '/' . $file))
		{
			throw new UserException('Le fichier fourni n\'existe pas.');
		}

		return $this->restoreDB(DATA_ROOT . '/' . $file, false, false, false);
	}

	/**
	 * Restaure une copie distante (fichier envoyé)
	 * @param  array   $file    Tableau provenant de $_FILES
	 * @param  integer $user_id ID du membre actuellement connecté, utilisé pour 
	 * vérifier qu'il est toujours administrateur dans la sauvegarde
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
			}
			elseif ($integrity === false)
			{
				throw new UserException('Le fichier fourni a été modifié par un programme externe.', self::INTEGRITY_FAIL);
			}
		}

		$r = $this->restoreDB($file['tmp_name'], $user_id);

		if ($r)
		{
			Utils::safe_unlink($file['tmp_name']);
		}

		return $r;
	}

	/**
	 * Vérifie l'intégrité d'une sauvegarde Garradin
	 * @param  string $file Chemin absolu vers la base de donnée
	 * @return boolean
	 */
	protected function checkIntegrity($file_path, $remove_hash = true)
	{
		$size = filesize($file_path);
		$fp = fopen($file_path, 'r+');

		$header = fread($fp, 16);







|











|
|







223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
			}
			elseif ($integrity === false)
			{
				throw new UserException('Le fichier fourni a été modifié par un programme externe.', self::INTEGRITY_FAIL);
			}
		}

		$r = $this->restoreDB($file['tmp_name'], $user_id, true);

		if ($r)
		{
			Utils::safe_unlink($file['tmp_name']);
		}

		return $r;
	}

	/**
	 * Vérifie l'intégrité d'une sauvegarde Garradin
	 * @param  string $file_path Chemin absolu vers la base de donnée
	 * @return boolean|null
	 */
	protected function checkIntegrity($file_path, $remove_hash = true)
	{
		$size = filesize($file_path);
		$fp = fopen($file_path, 'r+');

		$header = fread($fp, 16);
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305

	/**
	 * Restauration de base de données, la fonction qui le fait vraiment
	 * @param  string $file Chemin absolu vers la base de données à utiliser
	 * @return mixed 		true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB
	 * ne correspond pas à la version de Garradin (mise à jour nécessaire).
	 */
	protected function restoreDB($file, $user_id = false)
	{
		$return = 1;

		// Essayons déjà d'ouvrir la base de données à restaurer en lecture
		try {
			$db = new \SQLite3($file, SQLITE3_OPEN_READONLY);
		}
		catch (\Exception $e)
		{
			throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
				'Message d\'erreur de SQLite : ' . $e->getMessage(), self::NOT_A_DB);
		}








|





|







285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305

	/**
	 * Restauration de base de données, la fonction qui le fait vraiment
	 * @param  string $file Chemin absolu vers la base de données à utiliser
	 * @return mixed 		true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB
	 * ne correspond pas à la version de Garradin (mise à jour nécessaire).
	 */
	protected function restoreDB($file, $user_id = false, $check_foreign_keys = false, $do_backup = true)
	{
		$return = 1;

		// Essayons déjà d'ouvrir la base de données à restaurer en lecture
		try {
			$db = new \SQLite3($file, \SQLITE3_OPEN_READONLY);
		}
		catch (\Exception $e)
		{
			throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
				'Message d\'erreur de SQLite : ' . $e->getMessage(), self::NOT_A_DB);
		}

314
315
316
317
318
319
320










321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337




338
339
340
341
342
343
344
345
346
347
348
349
350
				'Message d\'erreur de SQLite : ' . $e->getMessage(), self::NOT_A_DB);
		}

		if (strtolower(trim($check)) != 'ok')
		{
			throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.');
		}











		// On ne peut pas faire de vérifications très poussées sur la structure de la base de données,
		// celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base
		// un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin
		$table = $db->querySingle('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';');

		if (!$table)
		{
			throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.');
		}

		// On récupère la version
		$version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';');

		// rification de l'AppID pour les versions récentes
		if (version_compare($version, '0.8.0', '>='))
		{




			$appid = $db->querySingle('PRAGMA application_id;', false);

			if ($appid !== DB::APPID)
			{
				throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID);
			}
		}

		if ($user_id)
		{
			// Empêchons l'admin de se tirer une balle dans le pied
			$is_still_admin = $db->querySingle('SELECT 1 FROM membres_categories
				WHERE id = (SELECT id_categorie FROM membres WHERE id = ' . (int) $user_id . ')







>
>
>
>
>
>
>
>
>
>














|
|

>
>
>
>
|

|
|
|
<







314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356

357
358
359
360
361
362
363
				'Message d\'erreur de SQLite : ' . $e->getMessage(), self::NOT_A_DB);
		}

		if (strtolower(trim($check)) != 'ok')
		{
			throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.');
		}

		if ($check_foreign_keys)
		{
			$check = $db->querySingle('PRAGMA foreign_key_check;');

			if ($check)
			{
				throw new UserException('Le fichier fourni est corrompu. Certaines clés étrangères référencent des lignes qui n\'existent pas.');
			}
		}

		// On ne peut pas faire de vérifications très poussées sur la structure de la base de données,
		// celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base
		// un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin
		$table = $db->querySingle('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';');

		if (!$table)
		{
			throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.');
		}

		// On récupère la version
		$version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';');

		// On ne permet pas de restaurer une vieille version
		if (version_compare($version, '0.9.8', '<'))
		{
			throw new UserException(sprintf('Ce fichier a été créé avec une version trop ancienne (%s), il n\'est pas possible de le restaurer.', $version));
		}

		// Vérification de l'AppID pour les versions récentes
		$appid = $db->querySingle('PRAGMA application_id;', false);

		if ($appid !== DB::APPID)
		{
			throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID);

		}

		if ($user_id)
		{
			// Empêchons l'admin de se tirer une balle dans le pied
			$is_still_admin = $db->querySingle('SELECT 1 FROM membres_categories
				WHERE id = (SELECT id_categorie FROM membres WHERE id = ' . (int) $user_id . ')
370
371
372
373
374
375
376




377
378
379
380
381
382
383
		}

		if (!copy($file, DB_FILE))
		{
			rename($backup, DB_FILE);
			throw new \RuntimeException('Unable to copy backup DB to main location.');
		}





		if ($return & self::NOT_AN_ADMIN)
		{
			// Forcer toutes les catégories à pouvoir gérer les droits
			$db = DB::getInstance();
			$db->update('membres_categories', [
				'droit_membres' => Membres::DROIT_ADMIN,







>
>
>
>







383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
		}

		if (!copy($file, DB_FILE))
		{
			rename($backup, DB_FILE);
			throw new \RuntimeException('Unable to copy backup DB to main location.');
		}

		if (!$do_backup) {
			unlink($backup);
		}

		if ($return & self::NOT_AN_ADMIN)
		{
			// Forcer toutes les catégories à pouvoir gérer les droits
			$db = DB::getInstance();
			$db->update('membres_categories', [
				'droit_membres' => Membres::DROIT_ADMIN,

Added src/include/lib/Garradin/Services/Fees.php version [40efe50c3b].









































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php

namespace Garradin\Services;

use Garradin\Config;
use Garradin\DB;
use Garradin\Entities\Services\Fee;
use KD2\DB\EntityManager;

class Fees
{
	protected $service_id;

	public function __construct(int $id)
	{
		$this->service_id = $id;
	}

	static public function get(int $id)
	{
		return EntityManager::findOneById(Fee::class, $id);
	}

	/**
	 * If $user_id is specified, then it will return a column 'user_amount' containing the amount that this specific user should pay
	 */
	static public function listAllByService(?int $user_id = null)
	{
		$db = DB::getInstance();

		$sql = 'SELECT *, CASE WHEN amount THEN amount ELSE NULL END AS user_amount
			FROM services_fees ORDER BY id_service, label COLLATE NOCASE;';
		$result = $db->get($sql);

		if (!$user_id) {
			return $result;
		}

		foreach ($result as &$row) {
			if ($row->formula) {
				$sql = sprintf('SELECT %s FROM membres WHERE id = %d;', $row->formula, $user_id);
				$row->user_amount = $db->firstColumn($sql);
			}
		}

		usort($result, function ($a, $b) {
			if ($a->user_amount == $b->user_amount) {
				return 0;
			}

			return $a->user_amount > $b->user_amount ? 1 : -1;
		});

		return $result;
	}

	public function listWithStats()
	{
		$db = DB::getInstance();
		return $db->get('SELECT f.*,
			(SELECT COUNT(DISTINCT id_user) FROM services_users WHERE id_fee = f.id AND expiry_date >= date() AND paid = 1) AS nb_users_ok,
			(SELECT COUNT(DISTINCT id_user) FROM services_users WHERE id_fee = f.id AND expiry_date < date()) AS nb_users_expired,
			(SELECT COUNT(DISTINCT id_user) FROM services_users WHERE id_fee = f.id AND paid = 0) AS nb_users_unpaid
			FROM services_fees f
			WHERE id_service = ?
			ORDER BY amount, transliterate_to_ascii(label) COLLATE NOCASE;', $this->service_id);
	}
}

Modified src/include/lib/Garradin/Services/Reminders.php from [ac12c6b477] to [09c61c980b].

1
2
3
4



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25
26


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

43



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
87


88





89
90
91
92
93
94

95
96


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121





122

123


124
125
126
127
128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164

165
166
167
168
169

170
171
172
173
174
175

176




177
178

179
180
181


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<?php

namespace Garradin;




class Rappels
{
	/**
	 * Vérification des champs fournis pour la modification de donnée
	 * @param  array $data Tableau contenant les champs à ajouter/modifier
	 * @return void
	 */
	protected function _checkFields(&$data)
	{
		$db = DB::getInstance();

        if (empty($data['id_cotisation'])
        	|| !$db->firstColumn('SELECT 1 FROM cotisations WHERE id = ?;', (int) $data['id_cotisation']))
        {
            throw new UserException('Cotisation inconnue.');
        }

		$data['id_cotisation'] = (int) $data['id_cotisation'];

		if ((trim($data['delai']) === '') || !is_numeric($data['delai']))

		{
			throw new UserException('Délai avant rappel invalide : doit être indiqué en nombre de jours.');


		}

		$data['delai'] = (int) $data['delai'];

		if (!isset($data['sujet']) || trim($data['sujet']) === '')
		{
			throw new UserException('Le sujet du rappel ne peut être vide.');
		}

		$data['sujet'] = trim($data['sujet']);

		if (!isset($data['texte']) || trim($data['texte']) === '')
		{
			throw new UserException('Le contenu du rappel ne peut être vide.');
		}


		$data['texte'] = trim($data['texte']);



	}

	/**
	 * Ajouter un rappel
	 * @param array $data Données du rappel
	 * @return integer Numéro ID du rappel créé
	 */
	public function add($data)
	{
		$db = DB::getInstance();

		$this->_checkFields($data);

		$db->insert('rappels', $data);

		return $db->lastInsertRowId();

	}

	/**
	 * Modifier un rappel automatique
	 * @param  integer 	$id   Numéro du rappel
	 * @param  array 	$data Données du rappel
	 * @return boolean        TRUE si tout s'est bien passé
	 * @throws UserException  En cas d'erreur dans une donnée à modifier
	 */
	public function edit($id, $data)
	{
		$db = DB::getInstance();

		$this->_checkFields($data);

		return $db->update('rappels', $data, 'id = ' . (int)$id);
	}

	/**
	 * Supprimer un rappel automatique

	 * @param  integer $id Numéro du rappel
	 * @param  boolean $delete_history Effacer aussi l'historique des rappels envoyés
	 * @return boolean     TRUE en cas de succès
	 */
	public function delete($id, $delete_history = false)
	{
		$db = DB::getInstance();



		$db->begin();






		if ($delete_history)
		{
			$db->delete('rappels_envoyes', 'id_rappel = ?', (int) $id);
		}
		else

		{
			$db->update('rappels_envoyes', ['id_rappel' => null], 'id_rappel = ' . (int)$id);


		}

		$db->delete('rappels', 'id = ?', (int) $id);
		
		$db->commit();

		return true;
	}

	/**
	 * Renvoie les données sur un rappel
	 * @param  integer $id Numéro du rappel
	 * @return array     Données du rappel
	 */
	public function get($id)
	{
		return DB::getInstance()->first('SELECT * FROM rappels WHERE id = ?;', (int)$id);
	}

	/**
	 * Renvoie le nombre de rappels automatiques enregistrés
	 * @return integer Nombre de rappels
	 */
	public function countAll()
	{





		return DB::getInstance()->firstColumn('SELECT COUNT(*) FROM rappels;');

	}



	/**
	 * Liste des rappels triés par cotisation
	 * @return array Liste des rappels
	 */
	public function listByCotisation()
	{
		return DB::getInstance()->get('SELECT r.*,
			c.intitule, c.montant, c.duree, c.debut, c.fin
			FROM rappels AS r
			INNER JOIN cotisations AS c ON c.id = r.id_cotisation
			ORDER BY r.id_cotisation, r.delai, r.sujet;');

	}

	/**
	 * Liste des rappels pour une cotisation donnée
	 * @param  integer $id Numéro du rappel
	 * @return array     Liste des rappels
	 */
	public function listForCotisation($id)
	{
		return DB::getInstance()->get('SELECT * FROM rappels 
			WHERE id_cotisation = ? ORDER BY delai, sujet;', (int)$id);
	}

	/**
	 * Envoi des rappels automatiques par e-mail
	 * @return boolean TRUE en cas de succès
	 */
	public function sendPending()
	{
		$db = DB::getInstance();
		$config = Config::getInstance();

		// Requête compliquée qui fait tout le boulot
		// la logique est un JOIN des tables rappels, cotisations, cotisations_membres et membres
		// pour récupérer la liste des membres qui doivent recevoir une cotisation
		$query = '
		SELECT
			*,
			/* Nombre de jours avant ou après expiration */

			(julianday(date()) - julianday(expiration)) AS nb_jours,
			/* Date de mise en œuvre du rappel */
			date(expiration, delai || \' days\') AS date_rappel
		FROM (
			SELECT m.*, MIN(r.delai) AS delai, r.sujet, r.texte, r.id_cotisation, r.id AS id_rappel,

				m.'.$config->get('champ_identite').' AS identite,
				CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
				WHEN c.fin IS NOT NULL THEN c.fin ELSE 0 END AS expiration
			FROM rappels AS r
				INNER JOIN cotisations AS c ON c.id = r.id_cotisation
				INNER JOIN cotisations_membres AS cm ON cm.id_cotisation = c.id

				INNER JOIN membres AS m ON m.id = cm.id_membre




			WHERE
				/* Inutile de sélectionner les membres sans email */

				m.email IS NOT NULL AND m.email != \'\'
				/* Les cotisations ponctuelles ne comptent pas */
				AND (c.fin IS NOT NULL OR c.duree IS NOT NULL)


				/* Rien nest envoyé aux membres des catégories cachées, logique */
				AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)
    		/* Grouper par membre, pour n\'envoyer qu\'un seul rappel par membre/cotise */
	    	GROUP BY m.id, r.id_cotisation
			ORDER BY r.delai ASC
		)
		WHERE nb_jours >= delai
			/* Pour ne pas spammer on n\'envoie pas de rappel antérieur au dernier rappel déjà effectué */
			AND id NOT IN (SELECT id_membre FROM rappels_envoyes AS re
				WHERE id_cotisation = re.id_cotisation
				AND re.date >= date(expiration, delai || \' days\')
			)
			AND CASE WHEN delai < 0 THEN nb_jours < 0 WHEN delai > 0 THEN nb_jours > 0 ELSE nb_jours = 0 END
		ORDER BY nb_jours DESC;';

		$db->begin();
		$re = new Rappels_Envoyes;

		foreach ($db->iterate($query) as $row)
		{
			$re->sendAuto($row);
		}

		$db->commit();
		return true;
	}
}


|

>
>
>
|
<
<
<
|
|
<
<
|
|
|
<
<
|
<
<
|
<
|
<
>
|
<
>
>
|

<
|
<
|
<
<
|
<
|
<
|
|
<
|
>
|
>
>
>


<
<
<
<
<
|

|
|
<
|
<
|
<
>


<
<
<
<
<
<
<
|

<
|
<
|
<
|
<

<
>
|
|
|

|

|
|
>
>
|
>
>
>
>
>

|

<
<
<
>
|
<
>
>
|
|
<
|
<
<
|

<
<
<
<
<
<
<
|
<
<
<

|
<

|

>
>
>
>
>
|
>
|
>
>

|
<
<
<
|
|
|
|
|
|
|
>
|
|
<
<
<
<
<
<
|
|
<






|




<
<
<
<
|
<
<
>
|
<
<
<
|
>
|
<
<
|
|
|
>
|
>
>
>
>

<
>
|
<
<
>
>
<
<
<
<
<
|
<
<
<
<
<
<
<
<
|
<
<

|

|


<



1
2
3
4
5
6
7
8



9
10


11
12
13


14


15

16

17
18

19
20
21
22

23

24


25

26

27
28

29
30
31
32
33
34
35
36





37
38
39
40

41

42

43
44
45







46
47

48

49

50

51

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71



72
73

74
75
76
77

78


79
80







81



82
83

84
85
86
87
88
89
90
91
92
93
94
95
96
97
98



99
100
101
102
103
104
105
106
107
108






109
110

111
112
113
114
115
116
117
118
119
120
121




122


123
124



125
126
127


128
129
130
131
132
133
134
135
136
137

138
139


140
141





142








143


144
145
146
147
148
149

150
151
152
<?php

namespace Garradin\Services;

use Garradin\Config;
use Garradin\DB;
use Garradin\Plugin;
use Garradin\Utils;



use Garradin\Entities\Services\Reminder;
use KD2\DB\EntityManager;



use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;





class Reminders

{

	static public function list()
	{

		return DB::getInstance()->get('SELECT s.label AS service_label, sr.* FROM services_reminders sr INNER JOIN services s ON s.id = sr.id_service
			ORDER BY s.label COLLATE NOCASE;');
	}


	static public function get(int $id)

	{


		return EntityManager::findOneById(Reminder::class, $id);

	}


	static public function listSentForUser(int $user_id)

	{
		return DB::getInstance()->get('SELECT rs.date AS sent_date, r.delay, s.label, rs.id AS sent_id, s.id AS service_id
			FROM services_reminders_sent rs
			INNER JOIN services_reminders r ON r.id = rs.id_reminder
			INNER JOIN services s ON s.id = rs.id_service
			WHERE rs.id_user = ?;', $user_id);
	}






	static public function listSentForReminder(int $reminder_id)
	{
		return DB::getInstance()->get('SELECT rs.date AS sent_date, r.delay, s.label, rs.id AS sent_id, s.id AS service_id
			FROM services_reminders_sent rs

			INNER JOIN services_reminders r ON r.id = rs.id_reminder

			INNER JOIN services s ON s.id = rs.id_service

			WHERE rs.id_reminder = ?;', $user_id);
	}








	static public function listForService(int $service_id)
	{

		return DB::getInstance()->get('SELECT * FROM services_reminders WHERE id_service = ? ORDER BY delay, subject;', $service_id);

	}



	/**

	 * Remplacer les tags dans le contenu/sujet du mail
	 * @param  string $content Chaîne à traiter
	 * @param  array  $data    Données supplémentaires à utiliser comme tags (tableau associatif)
	 * @return string          $content dont les tags ont été remplacés par le contenu correct
	 */
	static public function replaceTagsInContent(string $content, ?array $data = null)
	{
		$config = Config::getInstance();
		$tags = [
			'#NOM_ASSO'		=>	$config->get('nom_asso'),
			'#ADRESSE_ASSO'	=>	$config->get('adresse_asso'),
			'#EMAIL_ASSO'	=>	$config->get('email_asso'),
			'#SITE_ASSO'	=>	$config->get('site_asso'),
			'#URL_RACINE'	=>	WWW_URL,
			'#URL_SITE'		=>	WWW_URL,
			'#URL_ADMIN'	=>	ADMIN_URL,
		];

		if (!empty($data) && is_array($data))
		{



			foreach ($data as $key=>$value)
			{

				$key = '#' . strtoupper($key);
				$tags[$key] = $value;
			}
		}




		return strtr($content, $tags);
	}











	/**
	 * Envoi de mail pour rappel automati

	 */
	static public function sendAuto(\stdClass $reminder)
	{
		$replace = [
			'identite'        => $reminder->identity,
			'date_rappel'     => Utils::date_fr($reminder->reminder_date),
			'date_expiration' => Utils::date_fr($reminder->expiry_date),
			'nb_jours'        => $reminder->nb_days,
			'delai'           => $reminder->delay,
		];

		$subject = self::replaceTagsInContent($reminder->subject, $replace);
		$text = self::replaceTagsInContent($reminder->body, $replace);

		// Envoi du mail



		Utils::sendEmail(Utils::EMAIL_CONTEXT_PRIVATE, $reminder->email, $subject, $text, $reminder->id_user);

		$db = DB::getInstance();
		$db->insert('services_reminders_sent', [
			'id_service'  => $reminder->id_service,
			'id_user'     => $reminder->id_user,
			'id_reminder' => $reminder->id_reminder,
		]);

		Plugin::fireSignal('rappels.auto', $reminder);







		return true;

	}

	/**
	 * Envoi des rappels automatiques par e-mail
	 * @return boolean TRUE en cas de succès
	 */
	static public function sendPending()
	{
		$db = DB::getInstance();
		$config = Config::getInstance();





		$sql = 'SELECT


			date(su.expiry_date, sr.delay || \' days\') AS reminder_date,
			ABS(julianday(date()) - julianday(expiry_date)) AS nb_days,



			MAX(sr.delay) AS delay, sr.subject, sr.body, s.label, s.description,
			su.expiry_date, sr.id AS id_reminder, su.id_service, su.id_user,
			m.email, m.%s AS identity


			FROM services_users su
			INNER JOIN services s ON s.id = su.id_service
			INNER JOIN services_reminders sr ON sr.id_service = su.id_service
			-- Join with users, but not ones part of a hidden category
			INNER JOIN membres m ON su.id_user = m.id
				AND m.email IS NOT NULL
				AND (m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1))
			-- Join with sent reminders to exclude users that already have received this reminder
			LEFT JOIN services_reminders_sent srs ON srs.id_reminder = sr.id AND srs.id_user = su.id_user
			WHERE

				date() > date(su.expiry_date, sr.delay || \' days\')
				AND srs.id IS NULL


			GROUP BY su.id_user, s.id
			ORDER BY su.id_user;';














		$sql = sprintf($sql, $config->get('champ_identite'));



		foreach ($db->iterate($sql) as $row)
		{
			self::sendAuto($row);
		}


		return true;
	}
}

Added src/include/lib/Garradin/Services/Services.php version [8034954296].











































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

namespace Garradin\Services;

use Garradin\Config;
use Garradin\DB;
use Garradin\Entities\Services\Service;
use KD2\DB\EntityManager;

class Services
{
	static public function get(int $id)
	{
		return EntityManager::findOneById(Service::class, $id);
	}

	static public function listAssoc()
	{
		return DB::getInstance()->getAssoc('SELECT id, label FROM services ORDER BY label COLLATE NOCASE;');
	}

	static public function listGroupedWithFees(?int $user_id = null)
	{
		$services = DB::getInstance()->getGrouped('SELECT
			id, label, duration, start_date, end_date, description,
			CASE WHEN end_date IS NOT NULL THEN end_date WHEN duration IS NOT NULL THEN date(\'now\', \'+\'||duration||\' days\') ELSE NULL END AS expiry_date
			FROM services;');
		$fees = Fees::listAllByService($user_id);
		$out = [];

		foreach ($services as $service) {
			$out[$service->id] = $service;
			$out[$service->id]->fees = [];
		}

		foreach ($fees as $fee) {
			$out[$fee->id_service]->fees[] = $fee;
		}

		return $out;
	}

	static public function listWithStats()
	{
		$db = DB::getInstance();
		return $db->get('SELECT s.*,
			(SELECT COUNT(DISTINCT id_user) FROM services_users WHERE id_service = s.id AND expiry_date >= date() AND paid = 1) AS nb_users_ok,
			(SELECT COUNT(DISTINCT id_user) FROM services_users WHERE id_service = s.id AND expiry_date < date()) AS nb_users_expired,
			(SELECT COUNT(DISTINCT id_user) FROM services_users WHERE id_service = s.id AND paid = 0) AS nb_users_unpaid
			FROM services s
			ORDER BY transliterate_to_ascii(s.label) COLLATE NOCASE;');
	}
}

Added src/include/lib/Garradin/Services/Services_User.php version [2e5fb9387a].





































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php

namespace Garradin\Services;

use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Entities\Services\Service_User;
use KD2\DB\EntityManager;

class Services_User
{
	static public function get(int $id)
	{
		return EntityManager::findOneById(Service_User::class, $id);
	}

	static public function countForUser(int $user_id)
	{
		return DB::getInstance()->count(Service_User::TABLE, 'id_user = ?', $user_id);
	}

	static public function listDistinctForUser(int $user_id)
	{
		return DB::getInstance()->get('SELECT
			s.label, MAX(su.expiry_date) AS expiry_date, sf.label AS fee_label, su.paid, s.end_date,
			CASE WHEN su.expiry_date < date() THEN -1 WHEN su.expiry_date >= date() THEN 1 ELSE 0 END AS status
			FROM services_users su
			INNER JOIN services s ON s.id = su.id_service
			INNER JOIN services_fees sf ON sf.id = su.id_fee
			WHERE su.id_user = ?
			GROUP BY su.id_service ORDER BY expiry_date DESC;', $user_id);
	}

	static public function perUserList(int $user_id): DynamicList
	{
		$columns = [
			'id' => [
				'select' => 'su.id',
			],
			'id_account' => [
				'select' => 'sf.id_account',
			],
			'label' => [
				'select' => 's.label',
				'label' => 'Activité',
			],
			'date' => [
				'label' => 'Date d\'inscription',
				'select' => 'su.date',
			],
			'expiry' => [
				'label' => 'Date d\'expiration',
				'select' => 'MAX(su.expiry_date)',
			],
			'fee' => [
				'label' => 'Tarif',
				'select' => 'sf.label',
			],
			'paid' => [
				'label' => 'Payé',
				'select' => 'su.paid',
			],
			'amount' => [
				'label' => 'Reste à régler',
				'select' => 'expected_amount - SUM(tl.debit)',
			],
		];

		$tables = 'services_users su
			INNER JOIN services s ON s.id = su.id_service
			LEFT JOIN services_fees sf ON sf.id = su.id_fee
			LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id
			LEFT JOIN acc_transactions_lines tl ON tl.id_transaction = tu.id_transaction';
		$conditions = sprintf('su.id_user = %d', $user_id);

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', true);
		$list->groupBy('su.id');
		$list->setCount('COUNT(DISTINCT su.id)');
		return $list;
	}
}

Modified src/include/lib/Garradin/Squelette.php from [c51c3077ae] to [1036393224].

581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
                    default:
                        break;
                }
            }

            if (trim($loopContent))
            {
                $loop_start .= '$row[\'url\'] = Fichiers::_getURL($row[\'id\'], $row[\'nom\']); ';
                $loop_start .= '$row[\'miniature\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], 200) : \'\'; ';
                $loop_start .= '$row[\'moyenne\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], 500) : \'\'; ';
            }

            $query .= $where . ' ' . $order;

            if (!$limit || $limit > 100)
                $limit = 100;








|
|
|







581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
                    default:
                        break;
                }
            }

            if (trim($loopContent))
            {
                $loop_start .= '$row[\'url\'] = Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\']); ';
                $loop_start .= '$row[\'miniature\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\'], 200) : \'\'; ';
                $loop_start .= '$row[\'moyenne\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\'], 500) : \'\'; ';
            }

            $query .= $where . ' ' . $order;

            if (!$limit || $limit > 100)
                $limit = 100;

644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
            {
                $query = 'SELECT 0 LIMIT 0;';
            }
        }

        try {
            // Sécurité anti injection, à la compilation seulement
            $statement = $db->userSelectStatement($query);
        }
        catch (\Exception $e)
        {
            throw new \KD2\MiniSkelMarkupException("Erreur SQL dans la requête : ".$e->getMessage() . "\n " . $query);
        }

        $hash = sha1(uniqid(mt_rand(), true));







|







644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
            {
                $query = 'SELECT 0 LIMIT 0;';
            }
        }

        try {
            // Sécurité anti injection, à la compilation seulement
            $statement = $db->protectSelect(null, $query);
        }
        catch (\Exception $e)
        {
            throw new \KD2\MiniSkelMarkupException("Erreur SQL dans la requête : ".$e->getMessage() . "\n " . $query);
        }

        $hash = sha1(uniqid(mt_rand(), true));
819
820
821
822
823
824
825

826
827
828
829
830
831
832
        elseif (substr($uri, -1) == '/')
        {
            $skel = 'rubrique.html';
            $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1, -1);
        }
        elseif (preg_match('!^/admin/!', $uri))
        {

            throw new UserException('Cette page n\'existe pas.');
        }
        else
        {
            $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1);

            if (preg_match('!^[\w\d_-]+$!i', $_GET['uri'])







>







819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
        elseif (substr($uri, -1) == '/')
        {
            $skel = 'rubrique.html';
            $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1, -1);
        }
        elseif (preg_match('!^/admin/!', $uri))
        {
            http_response_code(404);
            throw new UserException('Cette page n\'existe pas.');
        }
        else
        {
            $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1);

            if (preg_match('!^[\w\d_-]+$!i', $_GET['uri'])

Modified src/include/lib/Garradin/Squelette_Filtres.php from [4ef78402d4] to [4a8a3bb75b].

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    static public function date($date, $format)
    {
        return strftime($format, $date);
    }

    static public function date_en_francais($date)
    {
        return ucfirst(strtolower(Utils::strftime_fr('%A %e %B %Y', $date)));
    }

    static public function heure_en_francais($date)
    {
        return Utils::strftime_fr('%Hh%M', $date);
    }

    static public function mois_en_francais($date)
    {
        return Utils::strftime_fr('%B %Y', $date);
    }

    static public function date_perso($date, $format)
    {
        return Utils::strftime_fr($format, $date);
    }

    static public function date_intelligente($date, $avec_heure = true)
    {
        $jour = null;
        $heure = date('H\hi', $date);








|




|




|




|







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    static public function date($date, $format)
    {
        return strftime($format, $date);
    }

    static public function date_en_francais($date)
    {
        return ucfirst(strtolower(Utils::strftime_fr($date, '%A %e %B %Y')));
    }

    static public function heure_en_francais($date)
    {
        return Utils::strftime_fr($date, '%Hh%M');
    }

    static public function mois_en_francais($date)
    {
        return Utils::strftime_fr($date, '%B %Y');
    }

    static public function date_perso($date, $format)
    {
        return Utils::strftime_fr($date, $format);
    }

    static public function date_intelligente($date, $avec_heure = true)
    {
        $jour = null;
        $heure = date('H\hi', $date);

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
        }
        elseif (date('Ymd', $date) == date('Ymd', strtotime('tomorrow')))
        {
            $jour = 'demain';
        }
        elseif (date('Y', $date) == date('Y'))
        {
            $jour = strtolower(Utils::strftime_fr('%A %e %B', $date));
        }
        else
        {
            $jour = strtolower(Utils::strftime_fr('%e %B %Y', $date));
        }

        if ($avec_heure)
        {
            return sprintf('%s, %s', $jour, $heure);
        }








|



|







96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
        }
        elseif (date('Ymd', $date) == date('Ymd', strtotime('tomorrow')))
        {
            $jour = 'demain';
        }
        elseif (date('Y', $date) == date('Y'))
        {
            $jour = strtolower(Utils::strftime_fr($date, '%A %e %B'));
        }
        else
        {
            $jour = strtolower(Utils::strftime_fr($date, '%e %B %Y'));
        }

        if ($avec_heure)
        {
            return sprintf('%s, %s', $jour, $heure);
        }

134
135
136
137
138
139
140
141



142



143
144

145
146
147
148
149
150
151
    }

    static public function proteger_contact($contact)
    {
        if (!trim($contact))
            return '';

        if (strpos($contact, '@'))



            return '<span style="unicode-bidi:bidi-override;direction: rtl;">'.htmlspecialchars(strrev($contact), ENT_QUOTES, 'UTF-8').'</span>';



        else
            return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>';

    }

    static public function entites_html($texte)
    {
        return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8');
    }








|
>
>
>
|
>
>
>
|

>







134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
    }

    static public function proteger_contact($contact)
    {
        if (!trim($contact))
            return '';

        if (strpos($contact, '@')) {
            $reversed = strrev($contact);
            // https://unicode-table.com/en/FF20/
            $reversed = strtr($reversed, ['@' => '@']);

            return sprintf('<a href="#error" onclick="this.href = (this.innerText + \':otliam\').split(\'\').reverse().join(\'\').replace(/@/, \'@\');"><span style="unicode-bidi:bidi-override;direction: rtl;">%s</span></a>',
                htmlspecialchars($reversed));
        }
        else {
            return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>';
        }
    }

    static public function entites_html($texte)
    {
        return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8');
    }

Modified src/include/lib/Garradin/Template.php from [f7f37fea68] to [290d250357].

1
2
3
4
5


6

7
8
9
10
11
12
13
<?php

namespace Garradin;

use KD2\Form;


use Garradin\Membres\Session;


class Template extends \KD2\Smartyer
{
	static protected $_instance = null;

	static public function getInstance()
	{





>
>

>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace Garradin;

use KD2\Form;
use KD2\HTTP;
use KD2\Translate;
use Garradin\Membres\Session;
use Garradin\Entities\Accounting\Account;

class Template extends \KD2\Smartyer
{
	static protected $_instance = null;

	static public function getInstance()
	{
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71
72
73




74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

117
118
119
120
121
122
123
124
125
126
127
128
129
130




















131
132
133
134
135
136
137
138
139
140
141




142
143
144
145
146
147
148
149
150
151
































































































































































































































































152
153
154
155
156
157
158
		// pour les éléments statiques (genre /admin/static/admin.css?v0.9.0)
		// car cela dévoilerait la version de Garradin utilisée, posant un souci
		// en cas de faille, on cache donc la version utilisée, chaque instance
		// aura sa propre version
		$this->assign('version_hash', substr(sha1(garradin_version() . garradin_manifest() . ROOT . SECRET_KEY), 0, 10));

		$this->assign('www_url', WWW_URL);
		$this->assign('self_url', Utils::getSelfUrl());
		$this->assign('self_url_no_qs', Utils::getSelfUrl(false));

		$this->assign('is_logged', false);

		$this->assign('password_pattern', sprintf('.{%d,}', Session::MINIMUM_PASSWORD_LENGTH));
		$this->assign('password_length', Session::MINIMUM_PASSWORD_LENGTH);

		$this->register_compile_function('continue', function ($pos, $block, $name, $raw_args) {
			if ($block == 'continue')
			{
				return 'continue;';
			}
		});

		$this->register_function('form_errors', [$this, 'formErrors']);
		$this->register_function('show_error', [$this, 'showError']);
		$this->register_function('form_field', [$this, 'formField']);
		$this->register_function('select_compte', [$this, 'formSelectCompte']);
		$this->register_function('html_champ_membre', [$this, 'formChampMembre']);


		$this->register_function('custom_colors', [$this, 'customColors']);
		$this->register_function('plugin_url', ['Garradin\Utils', 'plugin_url']);
		$this->register_function('diff', [$this, 'diff']);
		$this->register_function('pagination', [$this, 'pagination']);
		$this->register_function('format_droits', [$this, 'formatDroits']);

		$this->register_function('csrf_field', function ($params) {
			return Form::tokenHTML($params['key']);
		});





		$this->register_modifier('strlen', 'strlen');
		$this->register_modifier('dump', ['KD2\ErrorManager', 'dump']);
		$this->register_modifier('get_country_name', ['Garradin\Utils', 'getCountryName']);
		$this->register_modifier('format_sqlite_date_to_french', ['Garradin\Utils', 'sqliteDateToFrench']);
		$this->register_modifier('format_bytes', ['Garradin\Utils', 'format_bytes']);
		$this->register_modifier('format_tel', [$this, 'formatPhoneNumber']);
		$this->register_modifier('abs', 'abs');
		$this->register_modifier('display_champ_membre', [$this, 'displayChampMembre']);

		$this->register_modifier('get_nom_compte', function ($compte) {
			if (is_null($compte))
			{
				return '';
			}

			if (!isset($this->liste_comptes))
			{
				$this->liste_comptes = (new Compta\Comptes)->getListAll();
			}

			if (!isset($this->liste_comptes[$compte]))
			{
				return '';
			}

			return $this->liste_comptes[$compte];
		});

		$this->register_modifier('strftime_fr', function ($ts, $format) {
			return Utils::strftime_fr($format, $ts);
		});

		$this->register_modifier('date_fr', function ($ts, $format = 'd/m/Y H:i:s') {
			return Utils::date_fr($format, $ts);
		});

		$this->register_modifier('escape_money', function ($number) {
			return number_format((float)$number, 2, ',', ' ');
		});

		$this->register_modifier('html_money', function ($number) {
			return '<b class="money">' . number_format((float)$number, 2, ',', '&nbsp;') . '</b>';
		});


		$this->register_modifier('format_wiki', function ($str) {
			$str = Utils::SkrivToHTML($str);
			$str = Squelette_Filtres::typo_fr($str);
			return $str;
		});

		$this->register_modifier('liens_wiki', function ($str, $prefix) {
			return preg_replace_callback('!<a href="([^/.:@]+)">!i', function ($matches) use ($prefix) {
				return '<a href="' . $prefix . Wiki::transformTitleToURI($matches[1]) . '">';
			}, $str);
		});

	}





















	protected function formErrors($params)
	{
		$form = $this->getTemplateVars('form');

		if (!$form->hasErrors())
		{
			return '';
		}

		return '<div class="error"><ul><li>' . implode('</li><li>', $form->getErrorMessages(!empty($params['membre']) ? true : false)) . '</li></ul></div>';




	}

	protected function showError($params)
	{
		if (!$params['if'])
		{
			return '';
		}

		return '<p class="error">' . $this->escape($params['message']) . '</p>';
































































































































































































































































	}

	protected function formField(array $params, $escape = true)
	{
		if (!isset($params['name']))
		{
			throw new \BadFunctionCallException('name argument is mandatory');







|
|
















<

>











>
>
>
>



<
<




|
<
<
<
<
|
<
<
<
<
|
<
<
<
<
<
<
<
<
|
<
<

|
|


<
<
<
<
|
<
<
>














>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>










|
>
>
>
>









|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83


84
85
86
87
88




89




90








91


92
93
94
95
96




97


98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
		// pour les éléments statiques (genre /admin/static/admin.css?v0.9.0)
		// car cela dévoilerait la version de Garradin utilisée, posant un souci
		// en cas de faille, on cache donc la version utilisée, chaque instance
		// aura sa propre version
		$this->assign('version_hash', substr(sha1(garradin_version() . garradin_manifest() . ROOT . SECRET_KEY), 0, 10));

		$this->assign('www_url', WWW_URL);
		$this->assign('self_url', Utils::getSelfURI());
		$this->assign('self_url_no_qs', Utils::getSelfURI(false));

		$this->assign('is_logged', false);

		$this->assign('password_pattern', sprintf('.{%d,}', Session::MINIMUM_PASSWORD_LENGTH));
		$this->assign('password_length', Session::MINIMUM_PASSWORD_LENGTH);

		$this->register_compile_function('continue', function ($pos, $block, $name, $raw_args) {
			if ($block == 'continue')
			{
				return 'continue;';
			}
		});

		$this->register_function('form_errors', [$this, 'formErrors']);
		$this->register_function('show_error', [$this, 'showError']);
		$this->register_function('form_field', [$this, 'formField']);

		$this->register_function('html_champ_membre', [$this, 'formChampMembre']);
		$this->register_function('input', [$this, 'formInput']);

		$this->register_function('custom_colors', [$this, 'customColors']);
		$this->register_function('plugin_url', ['Garradin\Utils', 'plugin_url']);
		$this->register_function('diff', [$this, 'diff']);
		$this->register_function('pagination', [$this, 'pagination']);
		$this->register_function('format_droits', [$this, 'formatDroits']);

		$this->register_function('csrf_field', function ($params) {
			return Form::tokenHTML($params['key']);
		});

		$this->register_function('icon', [$this, 'widgetIcon']);
		$this->register_function('button', [$this, 'widgetButton']);
		$this->register_function('linkbutton', [$this, 'widgetLinkButton']);

		$this->register_modifier('strlen', 'strlen');
		$this->register_modifier('dump', ['KD2\ErrorManager', 'dump']);
		$this->register_modifier('get_country_name', ['Garradin\Utils', 'getCountryName']);


		$this->register_modifier('format_tel', [$this, 'formatPhoneNumber']);
		$this->register_modifier('abs', 'abs');
		$this->register_modifier('display_champ_membre', [$this, 'displayChampMembre']);

		$this->register_modifier('format_bytes', ['Garradin\Utils', 'format_bytes']);




		$this->register_modifier('strftime_fr', [Utils::class, 'strftime_fr']);




		$this->register_modifier('date_fr', [Utils::class, 'date_fr']);








		$this->register_modifier('date_long', [Utils::class, 'date_fr']);



		$this->register_modifier('date_short', function ($dt) {
			return Utils::date_fr($dt, 'd/m/Y');
		});





		$this->register_modifier('html_money', [$this, 'htmlMoney']);


		$this->register_modifier('money_currency', [$this, 'htmlMoneyCurrency']);

		$this->register_modifier('format_wiki', function ($str) {
			$str = Utils::SkrivToHTML($str);
			$str = Squelette_Filtres::typo_fr($str);
			return $str;
		});

		$this->register_modifier('liens_wiki', function ($str, $prefix) {
			return preg_replace_callback('!<a href="([^/.:@]+)">!i', function ($matches) use ($prefix) {
				return '<a href="' . $prefix . Wiki::transformTitleToURI($matches[1]) . '">';
			}, $str);
		});

	}

	protected function htmlMoney($number, bool $hide_empty = true): string
	{
		if ($hide_empty && !$number) {
			return '';
		}

		return sprintf('<b class="money">%s</b>', Utils::money_format($number, ',', '&nbsp;', $hide_empty));
	}

	protected function htmlMoneyCurrency($number, bool $hide_empty = true): string
	{
		$out = $this->htmlMoney($number, $hide_empty);

		if ($out !== '') {
			$out .= '&nbsp;' . Config::getInstance()->get('monnaie');
		}

		return $out;
	}

	protected function formErrors($params)
	{
		$form = $this->getTemplateVars('form');

		if (!$form->hasErrors())
		{
			return '';
		}

		$errors = $form->getErrorMessages(!empty($params['membre']) ? true : false);
		$errors = array_map([$this, 'escape'], $errors);
		$errors = array_map('nl2br', $errors);

		return '<div class="block error"><ul><li>' . implode('</li><li>', $errors) . '</li></ul></div>';
	}

	protected function showError($params)
	{
		if (!$params['if'])
		{
			return '';
		}

		return '<p class="block error">' . $this->escape($params['message']) . '</p>';
	}

	protected function widgetIcon(array $params): string
	{
		if (empty($params['href'])) {
			return sprintf('<b class="icn">%s</b>', Utils::iconUnicode($params['shape']));
		}

		return sprintf('<a href="%s" class="icn" title="%s">%s</a>', $this->escape(ADMIN_URL . $params['href']), $this->escape($params['label']), Utils::iconUnicode($params['shape']));
	}

	protected function widgetButton(array $params): string
	{
		$icon = Utils::iconUnicode($params['shape']);
		$label = isset($params['label']) ? $this->escape($params['label']) : '';
		unset($params['label'], $params['shape']);

		if (!isset($params['type'])) {
			$params['type'] = 'button';
		}

		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		if (isset($params['name']) && !isset($params['value'])) {
			$params['value'] = 1;
		}

		$params['class'] .= ' icn-btn';

		array_walk($params, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, $this->escape($v));
		});

		$params = implode(' ', $params);

		return sprintf('<button %s data-icon="%s">%s</button>', $params, $icon, $label);
	}

	protected function widgetLinkButton(array $params): string
	{
		$href = $params['href'];
		$shape = $params['shape'];
		$label = $params['label'];

		// href can be prefixed with '!' to make the URL relative to ADMIN_URL
		if (substr($href, 0, 1) == '!') {
			$href = ADMIN_URL . substr($params['href'], 1);
		}

		unset($params['href'], $params['shape'], $params['label']);

		array_walk($params, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, $this->escape($v));
		});

		$params = implode(' ', $params);

		return sprintf('<a class="icn-btn" data-icon="%s" href="%s" %s>%s</a>', Utils::iconUnicode($shape), $this->escape($href), $params, $this->escape($label));
	}

	protected function formInput(array $params)
	{
		static $params_list = ['value', 'default', 'type', 'help', 'label', 'name', 'options', 'source'];

		// Extract params and keep attributes separated
		$attributes = array_diff_key($params, array_flip($params_list));
		$params = array_intersect_key($params, array_flip($params_list));
		extract($params, \EXTR_SKIP);

		if (!isset($name, $type)) {
			throw new \InvalidArgumentException('Missing name or type');
		}

		$current_value = null;
		$current_value_from_user = false;

		if (isset($_POST[$name])) {
			$current_value = $_POST[$name];
			$current_value_from_user = true;
		}
		elseif (isset($source) && is_object($source) && isset($source->$name)) {
			$current_value = $source->$name;
		}
		elseif (isset($source) && is_array($source) && isset($source[$name])) {
			$current_value = $source[$name];
		}
		elseif (isset($default)) {
			$current_value = $default;
		}

		if ($type == 'date' && is_object($current_value) && $current_value instanceof \DateTimeInterface) {
			$current_value = $current_value->format('d/m/Y');
		}
		elseif ($type == 'date' && is_string($current_value)) {
			if ($v = \DateTime::createFromFormat('!Y-m-d', $current_value)) {
				$current_value = $v->format('d/m/Y');
			}
		}

		$attributes['id'] = 'f_' . $name;
		$attributes['name'] = $name;

		if (!isset($attributes['autocomplete']) && ($type == 'money' || $type == 'password')) {
			$attributes['autocomplete'] = 'off';
		}

		if ($type == 'radio' || $type == 'checkbox') {
			$attributes['id'] .= '_' . $value;

			if ($current_value == $value) {
				$attributes['checked'] = 'checked';
			}

			$attributes['value'] = $value;
		}
		elseif ($type == 'date') {
			$type = 'text';
			$attributes['placeholder'] = 'JJ/MM/AAAA';
			$attributes['data-input'] = 'date';
			$attributes['size'] = 12;
			$attributes['maxlength'] = 10;
			$attributes['pattern'] = '\d\d?/\d\d?/\d{4}';
		}

		// Create attributes string
		if (array_key_exists('required', $attributes)) {
			$attributes['required'] = 'required';
		}

		if (!empty($attributes['disabled'])) {
			$attributes['disabled'] = 'disabled';
			unset($attributes['required']);
		}
		else {
			unset($attributes['disabled']);
		}

		if (array_key_exists('required', $attributes) || array_key_exists('fake_required', $attributes)) {
			$required_label =  ' <b title="Champ obligatoire">(obligatoire)</b>';
		}
		else {
			$required_label =  ' <i>(facultatif)</i>';
		}

		// Fake required: doesn't set the required attribute, just the label
		// (useful for form elements that are hidden by JS)
		unset($attributes['fake_required']);

		$attributes_string = $attributes;

		array_walk($attributes_string, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, $v);
		});

		$attributes_string = implode(' ', $attributes_string);

		if ($type == 'select') {
			$input = sprintf('<select %s>', $attributes_string);

			foreach ($options as $_key => $_value) {
				$input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', $this->escape($_value));
			}

			$input .= '</select>';
		}
		elseif ($type == 'select_groups') {
			$input = sprintf('<select %s>', $attributes_string);

			foreach ($options as $optgroup => $suboptions) {
				$input .= sprintf('<optgroup label="%s">', $this->escape($optgroup));

				foreach ($suboptions as $_key => $_value) {
					$input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', $this->escape($_value));
				}

				$input .= '</optgroup>';
			}

			$input .= '</select>';
		}
		elseif ($type == 'textarea') {
			$input = sprintf('<textarea %s>%s</textarea>', $attributes_string, $this->escape($current_value));
		}
		elseif ($type == 'list') {
			$multiple = !empty($attributes['multiple']);
			$values = '';
			$delete_btn = $this->widgetButton(['shape' => 'delete']);

			if (null !== $current_value) {
				foreach ($current_value as $v => $l) {
					$values .= sprintf('<span class="label"><input type="hidden" name="%s[%s]" value="%s" /> %3$s %s</span>', $this->escape($name), $this->escape($v), $this->escape($l), $multiple ? $delete_btn : '');
				}
			}

			$button = $this->widgetButton([
				'shape' => $multiple ? 'plus' : 'menu',
				'value' => (substr($attributes['target'], 0, 4) === 'http') ? $attributes['target'] : ADMIN_URL . $attributes['target'],
				'label' => $multiple ? 'Ajouter' : 'Sélectionner',
				'data-multiple' => $multiple ? '1' : '0',
				'data-name' => $name,
			]);

			$input = sprintf('<span id="%s_container" class="input-list">%s%s</span>', $this->escape($attributes['id']), $button, $values);
		}
		elseif ($type == 'money') {
			if (null !== $current_value && !$current_value_from_user) {
				$current_value = Utils::money_format($current_value, ',', '');
			}

			$currency = Config::getInstance()->get('monnaie');
			$input = sprintf('<nobr><input type="text" pattern="[0-9]*([.,][0-9]{1,2})?" inputmode="decimal" size="8" class="money" %s value="%s" /><b>%s</b></nobr>', $attributes_string, $this->escape($current_value), $currency);
		}
		else {
			$value = isset($attributes['value']) ? '' : sprintf(' value="%s"', $this->escape($current_value));
			$input = sprintf('<input type="%s" %s %s />', $type, $attributes_string, $value);
		}

		// No label? then we only want the input without the widget
		if (empty($label)) {
			if (!array_key_exists('label', $params) && ($type == 'radio' || $type == 'checkbox')) {
				$input .= sprintf('<label for="%s"></label>', $attributes['id']);
			}

			return $input;
		}

		if ($type == 'file') {
			$input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize()));
		}

		$label = sprintf('<label for="%s">%s</label>', $attributes['id'], $this->escape($label));

		if ($type == 'radio' || $type == 'checkbox') {
			$out = sprintf('<dd>%s %s', $input, $label);

			if (isset($help)) {
				$out .= sprintf(' <em class="help">(%s)</em>', $this->escape($help));
			}

			$out .= '</dd>';
		}
		else {
			$out = sprintf('<dt>%s%s</dt><dd>%s</dd>', $label, $required_label, $input);

			if ($type == 'file') {
				$out .= sprintf('<dd class="help"><small>Taille maximale : %s</small></dd>', Utils::format_bytes(Utils::getMaxUploadSize()));
			}

			if (isset($help)) {
				$out .= sprintf('<dd class="help">%s</dd>', $this->escape($help));
			}
		}

		return $out;
	}

	protected function formField(array $params, $escape = true)
	{
		if (!isset($params['name']))
		{
			throw new \BadFunctionCallException('name argument is mandatory');
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257








258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
		$couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x'));

		$out = '
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
		}
		@media screen, handheld {
			.header .menu, body {
				background-image: url("%s");
			}
		}
		</style>';

		return sprintf($out, $couleur1, $couleur2, $image_fond);
	}

	protected function displayChampMembre($v, $config)
	{








		switch ($config->type)
		{
			case 'checkbox':
				return $v ? 'Oui' : 'Non';
			case 'email':
				return '<a href="mailto:' . rawurlencode($v) . '">' . htmlspecialchars($v) . '</a>';
			case 'tel':
				return '<a href="tel:' . rawurlencode($v) . '">' . htmlspecialchars($v) . '</a>';
			case 'url':
				return '<a href="' . htmlspecialchars($v) . '">' . htmlspecialchars($v) . '</a>';
			case 'country':
				return Utils::getCountryName($v);
			case 'date':
				return Utils::sqliteDateToFrench($v);
			case 'multiple':
				$out = [];

				foreach ($config->options as $b => $name)
				{
					if ($v & (0x01 << $b))
						$out[] = $name;







<
<
<
|
<






|

>
>
>
>
>
>
>
>













|







500
501
502
503
504
505
506



507

508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
		$couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x'));

		$out = '
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;



			--gBgImage: url("%s");

		}
		</style>';

		return sprintf($out, $couleur1, $couleur2, $image_fond);
	}

	protected function displayChampMembre($v, $config = null)
	{
		if (is_string($config)) {
			$config = Config::getInstance()->get('champs_membres')->get($config);
		}

		if (null === $config) {
			return htmlspecialchars($v);
		}

		switch ($config->type)
		{
			case 'checkbox':
				return $v ? 'Oui' : 'Non';
			case 'email':
				return '<a href="mailto:' . rawurlencode($v) . '">' . htmlspecialchars($v) . '</a>';
			case 'tel':
				return '<a href="tel:' . rawurlencode($v) . '">' . htmlspecialchars($v) . '</a>';
			case 'url':
				return '<a href="' . htmlspecialchars($v) . '">' . htmlspecialchars($v) . '</a>';
			case 'country':
				return Utils::getCountryName($v);
			case 'date':
				return Utils::date_fr($v);
			case 'multiple':
				$out = [];

				foreach ($config->options as $b => $name)
				{
					if ($v & (0x01 << $b))
						$out[] = $name;
335
336
337
338
339
340
341

342
343
344
345
346
347
348
349
		}

		if (!empty($config->mandatory))
		{
			$attributes .= 'required="required" ';
		}


		$attributes .= 'autocomplete="off" ';

		if (!empty($params['user_mode']) && empty($config->editable))
		{
			$out = '<dt>' . htmlspecialchars($config->title, ENT_QUOTES, 'UTF-8') . '</dt>';
			$out .= '<dd>' . (trim($value) === '' ? 'Non renseigné' : $this->displayChampMembre($value, $config)) . '</dd>';
			return $out;
		}







>
|







601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
		}

		if (!empty($config->mandatory))
		{
			$attributes .= 'required="required" ';
		}

		// Fix for autocomplete, lpignore is for Lastpass
		$attributes .= 'autocomplete="off" data-lpignore="true" ';

		if (!empty($params['user_mode']) && empty($config->editable))
		{
			$out = '<dt>' . htmlspecialchars($config->title, ENT_QUOTES, 'UTF-8') . '</dt>';
			$out .= '<dd>' . (trim($value) === '' ? 'Non renseigné' : $this->displayChampMembre($value, $config)) . '</dd>';
			return $out;
		}
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401

			// Forcer la valeur à être un entier (depuis PHP 7.1)
			$value = (int)$value;

			foreach ($options as $k=>$v)
			{
				$b = 0x01 << (int)$k;
				$field .= '<label><input type="checkbox" name="' 
					. htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '[' . (int)$k . ']" value="1" '
					. (($value & $b) ? 'checked="checked"' : '') . ' ' . $attributes . '/> ' 
					. htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '</label><br />';
			}
		}
		elseif ($type == 'textarea')
		{
			$field .= '<textarea ' . $attributes . 'cols="30" rows="5">' . htmlspecialchars($value, ENT_QUOTES) . '</textarea>';
		}
		else







|
<
|
<







651
652
653
654
655
656
657
658

659

660
661
662
663
664
665
666

			// Forcer la valeur à être un entier (depuis PHP 7.1)
			$value = (int)$value;

			foreach ($options as $k=>$v)
			{
				$b = 0x01 << (int)$k;
				$field .= sprintf('<input type="checkbox" name="%s[%d]" id="f_%1$s_%2$d" value="1" %s %s /> <label for="f_%1$s_%2$d">%s</label><br />',

					htmlspecialchars($params['name']), $k, ($value & $b) ? 'checked="checked"' : '', $attributes, htmlspecialchars($v));

			}
		}
		elseif ($type == 'textarea')
		{
			$field .= '<textarea ' . $attributes . 'cols="30" rows="5">' . htmlspecialchars($value, ENT_QUOTES) . '</textarea>';
		}
		else
432
433
434
435
436
437
438






439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
		$out .= '</dt>';

		if (!empty($config->help))
		{
			$out .= '
		<dd class="help">' . htmlspecialchars($config->help, ENT_QUOTES, 'UTF-8') . '</dd>';
		}







		if ($type != 'checkbox')
		{
			$out .= '
		<dd>' . $field . '</dd>';
		}

		return $out;
	}

	protected function formSelectCompte($params)
	{
		$name = $params['name'];
		$comptes = $params['comptes'];
		$data = isset($params['data']) ? (array) $params['data'] : null;
		$selected = isset($data[$params['name']]) ? $data[$params['name']] : f($name);

		$out = '<select name="'.$name.'" id="f_'.$name.'" class="large">';

		foreach ($comptes as $compte)
		{
			// Ne pas montrer les comptes désactivés
			if (!empty($compte->desactive))
				continue;

			if (!isset($compte->id[1]) && empty($params['create']))
			{
				$out.= '<optgroup label="'.htmlspecialchars($compte->libelle, ENT_QUOTES, 'UTF-8', false).'" class="niveau_1"></optgroup>';
			}
			elseif (!isset($compte->id[2]) && empty($params['create']))
			{
				if ($compte->id > 10)
					$out.= '</optgroup>';

				$out.= '<optgroup label="'.htmlspecialchars($compte->id . ' - ' . $compte->libelle, ENT_QUOTES, 'UTF-8', false).'" class="niveau_2">';
			}
			else
			{
				$out .= '<option value="'.htmlspecialchars($compte->id, ENT_QUOTES, 'UTF-8', false).'" class="niveau_'.strlen($compte->id).'"';

				if ($selected == $compte->id)
				{
					$out .= ' selected="selected"';
				}

				$out .= '>' . htmlspecialchars($compte->id . ' - ' . $compte->libelle, ENT_QUOTES, 'UTF-8', false);
				$out .= '</option>';
			}
		}

		$out .= '</optgroup>';
		$out .= '</select>';

		return $out;
	}

	protected function diff(array $params)
	{
		if (!isset($params['old']) || !isset($params['new']))
		{







>
>
>
>
>
>







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716














































717
718
719
720
721
722
723
		$out .= '</dt>';

		if (!empty($config->help))
		{
			$out .= '
		<dd class="help">' . htmlspecialchars($config->help, ENT_QUOTES, 'UTF-8') . '</dd>';
		}

		$id_field = Config::getInstance()->get('champ_identifiant');

		if ($params['name'] == $id_field && empty($params['user_mode'])) {
			$out .= '<dd class="help"><small>(Sera utilisé comme identifiant de connexion si le membre a le droit de se connecter.)</small></dd>';
		}

		if ($type != 'checkbox')
		{
			$out .= '
		<dd>' . $field . '</dd>';
		}















































		return $out;
	}

	protected function diff(array $params)
	{
		if (!isset($params['old']) || !isset($params['new']))
		{

Added src/include/lib/Garradin/Upgrade.php version [846a817db9].























































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<?php

namespace Garradin;

use Garradin\Membres\Session;

class Upgrade
{
	static public function preCheck(): bool
	{
		$config = Config::getInstance();
		$v = $config->getVersion();

		if (version_compare($v, garradin_version(), '>='))
		{
			return false;
		}

		if (!$v || version_compare($v, '0.9.8', '<'))
		{
			throw new UserException("Votre version de Garradin est trop ancienne pour être mise à jour. Mettez à jour vers Garradin 0.9.8 avant de faire la mise à jour vers cette version.");
		}

		Install::checkAndCreateDirectories();

		if (Static_Cache::exists('upgrade'))
		{
			$path = Static_Cache::getPath('upgrade');
			throw new UserException('Une mise à jour est déjà en cours.'
				. PHP_EOL . 'Si celle-ci a échouée et que vous voulez ré-essayer, supprimez le fichier suivant:'
				. PHP_EOL . $path);
		}

		// Voir si l'utilisateur est loggé, on le fait ici pour le cas où
		// il y aurait déjà eu des entêtes envoyés au navigateur plus bas
		$session = new Session;
		$user_is_logged = $session->isLogged(true);
		return true;
	}

	static public function upgrade()
	{
		$config = Config::getInstance();
		$v = $config->getVersion();

		$session = new Session;
		$user_is_logged = $session->isLogged(true);

		Static_Cache::store('upgrade', 'Mise à jour en cours.');

		$db = DB::getInstance();

		// Créer une sauvegarde automatique
		$backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version());

		try {
			if (version_compare($v, '1.0.0-alpha1', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0_migration.sql');
				$db->commitSchemaUpdate();

				// Import nouveau plan comptable
				$chart = new \Garradin\Entities\Accounting\Chart;
				$chart->label = 'Plan comptable associatif 2018';
				$chart->country = 'FR';
				$chart->code = 'PCA2018';
				$chart->save();
				$chart->accounts()->importCSV(ROOT . '/include/data/charts/fr_2018.csv');
			}

			if (version_compare($v, '1.0.0-beta1', '>=') && version_compare($v, '1.0.0-beta6', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-beta6_migration.sql');
				$db->commitSchemaUpdate();
			}

			if (version_compare($v, '1.0.0-beta6', '>=') && version_compare($v, '1.0.0-beta8', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-beta8_migration.sql');
				$db->commitSchemaUpdate();
			}

			if (version_compare($v, '1.0.0-beta1', '>=') && version_compare($v, '1.0.0-rc3', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-rc3_migration.sql');
				$db->commitSchemaUpdate();
			}

			// Vérification de la cohérence des clés étrangères
			$db->foreignKeyCheck();

			Utils::clearCaches();

			$config->setVersion(garradin_version());

			Static_Cache::remove('upgrade');

			// Réinstaller les plugins système si nécessaire
			Plugin::checkAndInstallSystemPlugins();

			// Mettre à jour les plugins si nécessaire
			foreach (Plugin::listInstalled() as $id=>$infos)
			{
				// Ne pas tenir compte des plugins dont le code n'est pas dispo
				if ($infos->disabled)
				{
					continue;
				}

				$plugin = new Plugin($id);

				if ($plugin->needUpgrade())
				{
					$plugin->upgrade();
				}

				unset($plugin);
			}
		}
		catch (\Exception $e)
		{
			$s = new Sauvegarde;
			$s->restoreFromLocal($backup_name);
			$s->remove($backup_name);
			Static_Cache::remove('upgrade');
			throw $e;
		}

		// Forcer à rafraîchir les données de la session si elle existe
		if ($user_is_logged)
		{
			$session->refresh();
		}
	}
}

Modified src/include/lib/Garradin/Utils.php from [4d9bf0c755] to [c164f7f4a8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



29












30



31


32
33
34

35
36
37
38
39
40
41






42
43
44
45
46

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119





























120
121
122
123
124
125
126
<?php

namespace Garradin;

use KD2\Security;
use KD2\Form;
use KD2\Translate;
use KD2\SMTP;
use KD2\ODSWriter;

class Utils
{
    const EMAIL_CONTEXT_BULK = 'bulk';
    const EMAIL_CONTEXT_PRIVATE = 'private';
    const EMAIL_CONTEXT_SYSTEM = 'system';

    static protected $skriv = null;

    static private $french_date_names = [
        'January'=>'Janvier', 'February'=>'Février', 'March'=>'Mars', 'April'=>'Avril', 'May'=>'Mai',
        'June'=>'Juin', 'July'=>'Juillet', 'August'=>'Août', 'September'=>'Septembre', 'October'=>'Octobre',
        'November'=>'Novembre', 'December'=>'Décembre', 'Monday'=>'Lundi', 'Tuesday'=>'Mardi', 'Wednesday'=>'Mercredi',
        'Thursday'=>'Jeudi','Friday'=>'Vendredi','Saturday'=>'Samedi','Sunday'=>'Dimanche',
        'Feb'=>'Fév','Apr'=>'Avr','May'=>'Mai','Jun'=>'Juin', 'Jul'=>'Juil','Aug'=>'Aout','Dec'=>'Déc',
        'Mon'=>'Lun','Tue'=>'Mar','Wed'=>'Mer','Thu'=>'Jeu','Fri'=>'Ven','Sat'=>'Sam','Sun'=>'Dim'];

    static public function strftime_fr($format=null, $ts=null)
    {



        if (is_null($format))












        {



            $format = '%d/%m/%Y à %H:%M';


        }

        $date = strftime($format, $ts);

        $date = strtr($date, self::$french_date_names);
        $date = strtolower($date);
        return $date;
    }

    static public function date_fr($format=null, $ts=null)
    {






        if (is_null($format))
        {
            $format = 'd/m/Y à H:i';
        }


        $date = date($format, $ts);
        $date = strtr($date, self::$french_date_names);
        $date = strtolower($date);
        return $date;
    }

    static public function sqliteDateToFrench($d, $short = false)
    {
        if (strlen($d) == 10 || $short)
        {
            $d = substr($d, 0, 10);
            $f = 'Y-m-d';
            $f2 = 'd/m/Y';
        }
        elseif (strlen($d) == 16)
        {
            $f = 'Y-m-d H:i';
            $f2 = 'd/m/Y H:i';
        }
        else
        {
            $f = 'Y-m-d H:i:s';
            $f2 = 'd/m/Y H:i';
        }
        
        if ($dt = \DateTime::createFromFormat($f, $d))
            return $dt->format($f2);
        else
            return $d;
    }

    static public function makeTimestampFromForm($d)
    {
        return mktime($d['h'], $d['min'], 0, $d['m'], $d['d'], $d['y']);
    }

    static public function modifyDate($str, $change, $as_timestamp = false)
    {
        $date = \DateTime::createFromFormat('Y-m-d', $str);
        $date->modify($change);
        return ($as_timestamp ? $date->getTimestamp() : $date->format('Y-m-d'));
    }

    static public function checkDate($str)
    {
        if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match))
            return false;

        if (!checkdate($match[2], $match[3], $match[1]))
            return false;

        return true;
    }




    static public function checkDateTime($str)
    {
        if (!preg_match('!^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})!', $str, $match))
            return false;

        if (!self::checkDate($match[1]))
            return false;

        if ((int) $match[2] < 0 || (int) $match[2] > 23)
            return false;

        if ((int) $match[3] < 0 || (int) $match[3] > 59)
            return false;
        
        if (isset($match[4]) && ((int) $match[4] < 0 || (int) $match[4] > 59))
            return false;

        return true;
    }






























    static public function getRequestURI()
    {
        if (!empty($_SERVER['REQUEST_URI']))
            return $_SERVER['REQUEST_URI'];
        else
            return false;






|
|
|









|




|


|

>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
|
>
>


|
>
|




|

>
>
>
>
>
>





>
|
|




<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
|
<
<
<
<
|
<
<
<
<
<
<
<











>
>
>













|





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80


















81





82




83







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<?php

namespace Garradin;

use KD2\Security;
use KD2\Form;
use KD2\HTTP;
use KD2\Translate;
use KD2\SMTP;

class Utils
{
    const EMAIL_CONTEXT_BULK = 'bulk';
    const EMAIL_CONTEXT_PRIVATE = 'private';
    const EMAIL_CONTEXT_SYSTEM = 'system';

    static protected $skriv = null;

    const FRENCH_DATE_NAMES = [
        'January'=>'Janvier', 'February'=>'Février', 'March'=>'Mars', 'April'=>'Avril', 'May'=>'Mai',
        'June'=>'Juin', 'July'=>'Juillet', 'August'=>'Août', 'September'=>'Septembre', 'October'=>'Octobre',
        'November'=>'Novembre', 'December'=>'Décembre', 'Monday'=>'Lundi', 'Tuesday'=>'Mardi', 'Wednesday'=>'Mercredi',
        'Thursday'=>'Jeudi','Friday'=>'Vendredi','Saturday'=>'Samedi','Sunday'=>'Dimanche',
        'Feb'=>'Fév','Apr'=>'Avr','Jun'=>'Juin', 'Jul'=>'Juil','Aug'=>'Aout','Dec'=>'Déc',
        'Mon'=>'Lun','Tue'=>'Mar','Wed'=>'Mer','Thu'=>'Jeu','Fri'=>'Ven','Sat'=>'Sam','Sun'=>'Dim'];

    static public function get_datetime($ts)
    {
        if (is_object($ts) && $ts instanceof \DateTimeInterface) {
            return $ts;
        }
        elseif (is_numeric($ts)) {
            return new \DateTime('@' . $ts);
        }
        elseif (strlen($ts) == 10) {
            return \DateTime::createFromFormat('!Y-m-d', $ts);
        }
        elseif (strlen($ts) == 19) {
            return \DateTime::createFromFormat('Y-m-d H:i:s', $ts);
        }
        else {
            return null;
        }
    }

    static public function strftime_fr($ts, $format)
    {
        $ts = self::get_datetime($ts);

        if (null === $ts) {
            return $ts;
        }

        $date = strftime($format, $ts->getTimestamp());

        $date = strtr($date, self::FRENCH_DATE_NAMES);
        $date = strtolower($date);
        return $date;
    }

    static public function date_fr($ts, $format = null)
    {
        $ts = self::get_datetime($ts);

        if (null === $ts) {
            return $ts;
        }

        if (is_null($format))
        {
            $format = 'd/m/Y à H:i';
        }

        $date = $ts->format($format);

        $date = strtr($date, self::FRENCH_DATE_NAMES);
        $date = strtolower($date);
        return $date;
    }



















    /**





     * @deprecated




     */







    static public function checkDate($str)
    {
        if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match))
            return false;

        if (!checkdate($match[2], $match[3], $match[1]))
            return false;

        return true;
    }

    /**
     * @deprecated
     */
    static public function checkDateTime($str)
    {
        if (!preg_match('!^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})!', $str, $match))
            return false;

        if (!self::checkDate($match[1]))
            return false;

        if ((int) $match[2] < 0 || (int) $match[2] > 23)
            return false;

        if ((int) $match[3] < 0 || (int) $match[3] > 59)
            return false;

        if (isset($match[4]) && ((int) $match[4] < 0 || (int) $match[4] > 59))
            return false;

        return true;
    }

    static public function moneyToInteger($value)
    {
        if (trim($value) === '') {
            return 0;
        }

        if (!preg_match('/^-?(\d+)(?:[,.](\d{1,2}))?$/', $value, $match)) {
            throw new UserException(sprintf('Le format du montant est invalide : %s. Format accepté, exemple : 142,02', $value));
        }

        $value = $match[1] . str_pad(@$match[2], 2, '0', STR_PAD_RIGHT);
        $value = (int) $value;
        return $value;
    }

    static public function money_format($number, string $dec_point = ',', string $thousands_sep = ' ', $zero_if_empty = true): string {
        if ($number == 0) {
            return $zero_if_empty ? '0' : '0,00';
        }

        $sign = $number < 0 ? '-' : '';
        $number = abs((int) $number);

        $decimals = substr('0' . $number, -2);
        $number = (int) substr($number, 0, -2);

        return sprintf('%s%s%s%s', $sign, number_format($number, 0, $dec_point, $thousands_sep), $dec_point, $decimals);
    }

    static public function getRequestURI()
    {
        if (!empty($_SERVER['REQUEST_URI']))
            return $_SERVER['REQUEST_URI'];
        else
            return false;
143
144
145
146
147
148
149










150
151
152
153
154
155
156
        if (is_array($qs))
        {
            $uri .= '?' . http_build_query($qs);
        }

        return str_replace('/admin', '', ADMIN_URL) . $uri;
    }











    public static function redirect($destination=false, $exit=true)
    {
        if (empty($destination) || !preg_match('/^https?:\/\//', $destination))
        {
            if (empty($destination))
                $destination = WWW_URL;







>
>
>
>
>
>
>
>
>
>







169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
        if (is_array($qs))
        {
            $uri .= '?' . http_build_query($qs);
        }

        return str_replace('/admin', '', ADMIN_URL) . $uri;
    }

    static public function getSelfURI(bool $qs = true)
    {
        return str_replace(substr(WWW_URL, 0, -1), '', self::getSelfURL($qs));
    }

    static public function getModifiedURL(string $new)
    {
        return HTTP::mergeURLs(self::getSelfURL(), $new);
    }

    public static function redirect($destination=false, $exit=true)
    {
        if (empty($destination) || !preg_match('/^https?:\/\//', $destination))
        {
            if (empty($destination))
                $destination = WWW_URL;
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
     * $listLength représente la longueur d'items de la pagination à génerer
     *
     * @param int $current
     * @param int $total
     * @param int $bypage
     * @param int $listLength
     * @param bool $showLast Toggle l'affichage du dernier élément de la pagination
     * @return array
     */
    public static function getGenericPagination($current, $total, $bypage, $listLength=11, $showLast = true)
    {
        if ($total <= $bypage)
            return false;

        $total = ceil($total / $bypage);

        if ($total < $current)
            return false;

        $length = ($listLength / 2);

        $begin = $current - ceil($length);
        if ($begin < 1)
        {
            $begin = 1;







|




|




|







255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
     * $listLength représente la longueur d'items de la pagination à génerer
     *
     * @param int $current
     * @param int $total
     * @param int $bypage
     * @param int $listLength
     * @param bool $showLast Toggle l'affichage du dernier élément de la pagination
     * @return array|null
     */
    public static function getGenericPagination($current, $total, $bypage, $listLength=11, $showLast = true)
    {
        if ($total <= $bypage)
            return null;

        $total = ceil($total / $bypage);

        if ($total < $current)
            return null;

        $length = ($listLength / 2);

        $begin = $current - ceil($length);
        if ($begin < 1)
        {
            $begin = 1;
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485

    static public function safe_unlink($path)
    {
        if (!@unlink($path))
        {
            return true;
        }
        
        if (!file_exists($path))
        {
            return true;
        }

        throw new \RuntimeException(sprintf('Impossible de supprimer le fichier %s: %s', $path, error_get_last()));

        return true;
    }

    static public function safe_mkdir($path, $mode = 0777, $recursive = false)
    {
        return @mkdir($path, $mode, $recursive) || is_dir($path);
    }

    static public function suggestPassword()
    {
        return Security::getRandomPassphrase(ROOT . '/include/data/dictionary.fr');
    }

    static public function checkIBAN($value)
    {
        // Enlever les caractères indésirables (espaces, tirets),
        $value = preg_replace('/[^A-Z0-9]/', '', strtoupper($value));
        
        // Supprimer les 4 premiers caractères et les replacer à la fin du compte
        $value = substr($value, 4) . substr($value, 0, 4);

        // Remplacer les lettres par des chiffres au moyen d'une table de conversion (A=10, B=11, C=12 etc.)
        $value = str_replace(range('A', 'Z'), range(10, 35), $value);

        // Diviser le nombre ainsi obtenu par 97
        // Si le reste n'est pas égal à 1 l'IBAN est incorrect : Modulo de 97 égal à 1.
        return (self::bcmod($value, 97) == 1);
    }

    /** 
     * my_bcmod - get modulus (substitute for bcmod) 
     * string my_bcmod ( string left_operand, int modulus ) 
     * left_operand can be really big, but be carefull with modulus :( 
     * by Andrius Baranauskas and Laurynas Butkus :) Vilnius, Lithuania 
     * @link https://php.net/manual/fr/function.bcmod.php#38474
     */ 
    static public function bcmod($x, $y)
    {
        if (function_exists('\bcmod'))
        {
            return \bcmod($x, $y);
        }

        // how many numbers to take at once? carefull not to exceed (int)
        $take = 5;
        $mod = '';

        do
        {
            $a = (int)$mod.substr( $x, 0, $take );
            $x = substr( $x, $take );
            $mod = $a % $y;
        } 
        while (strlen($x));

        return (int)$mod;
    }
    
    static public function checkBIC($bic)
    {
        return preg_match('!^[A-Z]{4}[A-Z]{2}[1-9A-Z]{2}(?:[A-Z\d]{3})?$!', $bic);
    }

    static public function normalizePhoneNumber($n)
    {
        return preg_replace('![^\d\+\(\)p#,;-]!', '', trim($n));
    }

    static public function write_ini_string($in)
    {







|




















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464


















































465
466
467
468
469
470
471

    static public function safe_unlink($path)
    {
        if (!@unlink($path))
        {
            return true;
        }

        if (!file_exists($path))
        {
            return true;
        }

        throw new \RuntimeException(sprintf('Impossible de supprimer le fichier %s: %s', $path, error_get_last()));

        return true;
    }

    static public function safe_mkdir($path, $mode = 0777, $recursive = false)
    {
        return @mkdir($path, $mode, $recursive) || is_dir($path);
    }

    static public function suggestPassword()
    {
        return Security::getRandomPassphrase(ROOT . '/include/data/dictionary.fr');
    }



















































    static public function normalizePhoneNumber($n)
    {
        return preg_replace('![^\d\+\(\)p#,;-]!', '', trim($n));
    }

    static public function write_ini_string($in)
    {
557
558
559
560
561
562
563
564

565
566
567
568
569
570
571
572
573
574
575
            case 'G': case 'g': return (int)$size_str * pow(1024, 3);
            case 'M': case 'm': return (int)$size_str * pow(1024, 2);
            case 'K': case 'k': return (int)$size_str * 1024;
            default: return $size_str;
        }
    }

    static public function format_bytes($size) {

        if ($size > (1024 * 1024))
            return str_replace('.', ',', round($size / 1024 / 1024, 2)) . ' Mo';
        elseif ($size > 1024)
            return str_replace('.', ',', round($size / 1024, 2)) . ' Ko';
        else
            return $size . ' o';
    }

    static public function deleteRecursive($path)
    {
        if (!file_exists($path))







|
>

|

|







543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
            case 'G': case 'g': return (int)$size_str * pow(1024, 3);
            case 'M': case 'm': return (int)$size_str * pow(1024, 2);
            case 'K': case 'k': return (int)$size_str * 1024;
            default: return $size_str;
        }
    }

    static public function format_bytes($size)
    {
        if ($size > (1024 * 1024))
            return number_format(round($size / 1024 / 1024, 2), 2, ',', '') . ' Mo';
        elseif ($size > 1024)
            return round($size / 1024) . ' Ko';
        else
            return $size . ' o';
    }

    static public function deleteRecursive($path)
    {
        if (!file_exists($path))
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
            if (is_dir($path . '/' . $file))
            {
                if (!self::deleteRecursive($path . '/' . $file))
                    return false;
            }
            else
            {
                utils::safe_unlink($path . '/' . $file);
            }
        }

        $dir->close();
        rmdir($path);

        return true;







|







573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
            if (is_dir($path . '/' . $file))
            {
                if (!self::deleteRecursive($path . '/' . $file))
                    return false;
            }
            else
            {
                self::safe_unlink($path . '/' . $file);
            }
        }

        $dir->close();
        rmdir($path);

        return true;
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
            if (!(is_numeric($params['query']) && (int)$params['query'] === 1) && $params['query'] !== true)
                $url .= $params['query'];
        }

        return $url;
    }

    static public function open_csv_file($file)
    {
        ini_set('auto_detect_line_endings', true);
        return fopen($file, 'r');
    }

    static public function find_csv_delim(&$fp)
    {
        $line = '';

        while ($line === '' && !feof($fp))
        {
            $line = fgets($fp, 4096);
        }

        if (strlen($line) >= 4095) {
            throw new UserException('Fichier CSV illisible : la première ligne est trop longue.');
        }

        // Delete the columns content
        $line = preg_replace('/".*?"/', '', $line);

        $delims = [
            ';' => substr_count($line, ';'),
            ',' => substr_count($line, ','),
            "\t"=> substr_count($line, "\t")
        ];

        arsort($delims);
        reset($delims);

        rewind($fp);

        return key($delims);
    }

    static public function skip_bom(&$fp)
    {
        // Skip BOM
        if (fgets($fp, 4) !== chr(0xEF) . chr(0xBB) . chr(0xBF))
        {
            fseek($fp, 0);
        }
    }

    static public function row_to_csv($row)
    {
        $row = (array) $row;

        array_walk($row, function (&$field) {
            $field = strtr($field, ['"' => '""', "\r\n" => "\n"]);
        });

        return sprintf("\"%s\"\r\n", implode('","', $row));
    }

    static public function toCSV($name, $iterator, $header = null, $row_map_callback = null)
    {
        header('Content-type: application/csv');
        header(sprintf('Content-Disposition: attachment; filename="%s.csv"', $name));

        $fp = fopen('php://output', 'w');

        if ($header)
        {
            fputs($fp, self::row_to_csv($header));
        }

        foreach ($iterator as $row)
        {
            if (!$header)
            {
                fputs($fp, self::row_to_csv(array_keys($row)));
                $header = true;
            }

            if (null !== $row_map_callback) {
                $row = call_user_func($row_map_callback, $row);
            }

            fputs($fp, self::row_to_csv($row));
        }

        fclose($fp);

        return true;
    }

    static public function toODS($name, $iterator, $header = null, $row_map_callback = null)
    {
        header('Content-type: application/vnd.oasis.opendocument.spreadsheet');
        header(sprintf('Content-Disposition: attachment; filename="%s.ods"', $name));

        $ods = new ODSWriter;
        $ods->table_name = $name;

        if ($header)
        {
            $ods->add((array) $header);
        }

        foreach ($iterator as $row)
        {
            if (!$header)
            {
                $ods->add(array_keys($row));
                $header = true;
            }

            if (null !== $row_map_callback) {
                $row = call_user_func($row_map_callback, $row);
            }

            $ods->add((array) $row);
        }

        $ods->output();

        return true;
    }

    static public function sendEmail($context, $recipient, $subject, $content, $id_membre = null, $pgp_key = null)
    {
        // Ne pas envoyer de mail à des adresses invalides
        if (!SMTP::checkEmailIsValid($recipient, false))
        {
            throw new UserException('Adresse email invalide: ' . $recipient);
        }







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







608
609
610
611
612
613
614

























































































































615
616
617
618
619
620
621
            if (!(is_numeric($params['query']) && (int)$params['query'] === 1) && $params['query'] !== true)
                $url .= $params['query'];
        }

        return $url;
    }


























































































































    static public function sendEmail($context, $recipient, $subject, $content, $id_membre = null, $pgp_key = null)
    {
        // Ne pas envoyer de mail à des adresses invalides
        if (!SMTP::checkEmailIsValid($recipient, false))
        {
            throw new UserException('Adresse email invalide: ' . $recipient);
        }
828
829
830
831
832
833
834
835




































































































































































































            {
                $raw_headers .= sprintf("%s: %s\r\n", $name, $value);
            }

            return \mail($to, $subject, $content, $raw_headers);
        }
    }
}











































































































































































































|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
            {
                $raw_headers .= sprintf("%s: %s\r\n", $name, $value);
            }

            return \mail($to, $subject, $content, $raw_headers);
        }
    }

    static public function iconUnicode(string $shape): string
    {
        switch ($shape) {
            case 'up': return '↑';
            case 'down': return '↓';
            case 'export': return '↷';
            case 'reset': return '↺';
            case 'upload': return '⇑';
            case 'download': return '⇓';
            case 'home': return '⌂';
            case 'print': return '⎙';
            case 'star': return '★';
            case 'check': return '☑';
            case 'settings': return '☸';
            case 'alert': return '⚠';
            case 'mail': return '✉';
            case 'edit': return '✎';
            case 'delete': return '✘';
            case 'help': return '❓';
            case 'plus': return '➕';
            case 'minus': return '➖';
            case 'logout': return '⤝';
            case 'eye-off': return '⤫';
            case 'menu': return '𝍢';
            case 'eye': return '👁';
            case 'user': return '👤';
            case 'users': return '👪';
            case 'calendar': return '📅';
            case 'attach': return '📎';
            case 'search': return '🔍';
            case 'lock': return '🔒';
            case 'unlock': return '🔓';
            case 'folder': return '🗀';
            case 'document': return '🗅';
            case 'bold': return 'B';
            case 'italic': return 'I';
            case 'header': return 'H';
            case 'paragraph': return '§';
            case 'list-ol': return 'ģ';
            case 'list-ul': return '•';
            case 'table': return '◫';
            case 'radio-unchecked': return '◯';
            case 'uncheck': return '☐';
            case 'radio-checked': return '⬤';
            case 'image': return '🖻';
            case 'left': return '←';
            case 'right': return '→';
            default:
                throw new \InvalidArgumentException('Unknown icon shape: ' . $shape);
        }
    }

    static public function array_transpose(array $array): array
    {
        $out = [];
        $max = 0;

        foreach ($array as $rows) {
            $max = max($max, count($rows));
        }

        foreach ($array as $column => $rows) {
            // Match number of rows of largest sub-array, in case there is a missing row in a column
            if ($max != count($rows)) {
                $rows = array_merge($rows, array_fill(0, $max - count($rows), null));
            }

            foreach ($rows as $k => $v) {
                if (!isset($out[$k])) {
                    $out[$k] = [];
                }

                $out[$k][$column] = $v;
            }
        }

        return $out;
    }

    static public function rgbHexToDec(string $hex)
    {
        return sscanf($hex, '#%02x%02x%02x');
    }

    /**
     * Converts an RGB color value to HSV. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
     * Assumes r, g, and b are contained in the set [0, 255] and
     * returns h, s, and v in the set [0, 1].
     *
     * @param   Number  r       The red color value
     * @param   Number  g       The green color value
     * @param   Number  b       The blue color value
     * @return  Array           The HSV representation
     */
    static public function rgbToHsv($r, $g = null, $b = null)
    {
        if (is_string($r) && is_null($g) && is_null($b))
        {
            list($r, $g, $b) = self::rgbHexToDec($r);
        }

        $r /= 255;
        $g /= 255;
        $b /= 255;
        $max = max($r, $g, $b);
        $min = min($r, $g, $b);
        $h = $s = $v = $max;

        $d = $max - $min;
        $s = ($max == 0) ? 0 : $d / $max;

        if($max == $min)
        {
            $h = 0; // achromatic
        }
        else
        {
            switch($max)
            {
                case $r: $h = ($g - $b) / $d + ($g < $b ? 6 : 0); break;
                case $g: $h = ($b - $r) / $d + 2; break;
                case $b: $h = ($r - $g) / $d + 4; break;
            }
            $h /= 6;
        }

        return array($h * 360, $s, $v);
    }

    static public function HTTPCache(string $hash, int $expiry): bool
    {
        $etag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : null;
        $last_modified = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : null;

        if ($etag === $hash && $last_modified <= $expiry) {
            header('HTTP/1.1 304 Not Modified', true, 304);
            exit;
        }

        header(sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $expiry)));
        header(sprintf('Etag: %s', $hash));
        header('Cache-Control: private');

        return false;
    }

    static public function getLatestVersion(): ?string
    {
        $config = Config::getInstance();
        $last = $config->get('last_version_check');

        if ($last) {
            $last = json_decode($last);
        }

        // Only check once every two weeks
        if ($last && $last->time > (time() - 3600 * 24 * 15)) {
            return $last->version;
        }

        $current_version = garradin_version();
        $last = (object) ['time' => time(), 'version' => null];
        $config->set('last_version_check', json_encode($last));
        $config->save();

        $list = (new HTTP)->GET(WEBSITE . 'juvlist');

        if (!$list) {
            return null;
        }

        $list = json_decode($list);

        if (!$list) {
            return null;
        }

        $last->version = $current_version;

        foreach ($list as $item) {
            if (preg_match('/^garradin-(.*)\.tar\.bz2$/', $item->name, $match) && !preg_match('/alpha|dev|rc|beta/', $match[1]) && version_compare($last->version, $match[1], '<')) {
                $last->version = $match[1];
            }
        }

        if ($last->version == $current_version) {
            $last->version = null;
        }

        $config->set('last_version_check', json_encode($last));
        $config->save();

        return $last->version;
    }
}

Modified src/include/lib/Garradin/Wiki.php from [640f809bb6] to [6ab9be5a8f].

465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
                'id'        =>  $id,
                'titre'     =>  $res->titre,
                'uri'       =>  $res->uri,
            ];

            if ($id == $res->parent)
            {
                throw new Exception('Parent! ' . $id . '/' . $res->parent);
            }

            $id = (int)$res->parent;
        }

        return array_reverse($flat);
    }







|







465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
                'id'        =>  $id,
                'titre'     =>  $res->titre,
                'uri'       =>  $res->uri,
            ];

            if ($id == $res->parent)
            {
                throw new \Exception('Parent! ' . $id . '/' . $res->parent);
            }

            $id = (int)$res->parent;
        }

        return array_reverse($flat);
    }

Modified src/include/lib/dependencies.list from [7b518732e5] to [b716c7d0a7].

1

2

3
4
5
6
7
8



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
KD2/data/

KD2/DB.php

KD2/DB_SQLite3.php
KD2/ErrorManager.php
KD2/FileInfo.php
KD2/Form.php
KD2/Helpers.php
KD2/Image.php



KD2/MiniSkel.php
KD2/QRCode.php
KD2/Security.php
KD2/Security_OTP.php
KD2/SimpleDiff.php
KD2/SkrivLite.php
KD2/Smartyer.php
KD2/SMTP.php
KD2/SVGPie.php
KD2/SVGPlot.php
KD2/Translate.php
KD2/UserSession.php
KD2/ZipWriter.php
KD2/ODSWriter.php
KD2/HTTP.php

>
|
>
|



|
|
>
>
>

|






<
<



<
<
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


22
23
24


KD2/data/
KD2/DB/AbstractEntity.php
KD2/DB/DB.php
KD2/DB/EntityManager.php
KD2/DB/SQLite3.php
KD2/ErrorManager.php
KD2/FileInfo.php
KD2/Form.php
KD2/HTTP.php
KD2/Graphics/Image.php
KD2/Graphics/QRCode.php
KD2/Graphics/SVG/Pie.php
KD2/Graphics/SVG/Plot.php
KD2/MiniSkel.php
KD2/Office/Calc/Writer.php
KD2/Security.php
KD2/Security_OTP.php
KD2/SimpleDiff.php
KD2/SkrivLite.php
KD2/Smartyer.php
KD2/SMTP.php


KD2/Translate.php
KD2/UserSession.php
KD2/ZipWriter.php


Modified src/include/test_required.php from [69fc54870f] to [a96dd46521].

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
        echo "Pour plus d'informations consulter http://dev.kd2.org/garradin/Probl%C3%A8mes%20fr%C3%A9quents\n";
    }

    exit;
}

test_requis(
    version_compare(phpversion(), '5.6', '>='),
    'PHP 5.6 ou supérieur requis. PHP version ' . phpversion() . ' installée.'
);

test_requis(
    defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH,
    'L\'algorithme de hashage de mot de passe Blowfish n\'est pas présent (pas installé ou pas compilé).'
);

test_requis(
    class_exists('SQLite3'),
    'Le module de base de données SQLite3 n\'est pas disponible.'
);

$v = \SQLite3::version();

test_requis(
    version_compare($v['versionString'], '3.7.4', '>='),
    'SQLite3 version 3.7.4 ou supérieur requise. Version installée : ' . $v['versionString']
);

test_requis(
    file_exists(__DIR__ . '/lib/KD2'),
    'Librairie KD2 non disponible.'
);







|
|















|
|






30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
        echo "Pour plus d'informations consulter http://dev.kd2.org/garradin/Probl%C3%A8mes%20fr%C3%A9quents\n";
    }

    exit;
}

test_requis(
    version_compare(phpversion(), '7.2', '>='),
    'PHP 7.2 ou supérieur requis. PHP version ' . phpversion() . ' installée.'
);

test_requis(
    defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH,
    'L\'algorithme de hashage de mot de passe Blowfish n\'est pas présent (pas installé ou pas compilé).'
);

test_requis(
    class_exists('SQLite3'),
    'Le module de base de données SQLite3 n\'est pas disponible.'
);

$v = \SQLite3::version();

test_requis(
    version_compare($v['versionString'], '3.16', '>='),
    'SQLite3 version 3.16 ou supérieur requise. Version installée : ' . $v['versionString']
);

test_requis(
    file_exists(__DIR__ . '/lib/KD2'),
    'Librairie KD2 non disponible.'
);

Modified src/scripts/cron.php from [8c598487d9] to [338658a83b].

1
2
3
4


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace Garradin;



require_once __DIR__ . '/include/init.php';

// Exécution des tâches automatiques

if (ENABLE_AUTOMATIC_BACKUPS && $config->get('frequence_sauvegardes') && $config->get('nombre_sauvegardes'))
{
	$s = new Sauvegarde;
	$s->auto();
}

// Exécution des rappels automatiques
$rappels = new Rappels;

if ($rappels->countAll())
{
	$rappels->sendPending();
}




>
>
|










<
<
<
<
|
<
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17




18

<?php

namespace Garradin;

use Garradin\Services\Reminders;

require_once __DIR__ . '/../include/init.php';

// Exécution des tâches automatiques

if (ENABLE_AUTOMATIC_BACKUPS && $config->get('frequence_sauvegardes') && $config->get('nombre_sauvegardes'))
{
	$s = new Sauvegarde;
	$s->auto();
}

// Exécution des rappels automatiques




Reminders::sendPending();

Added src/scripts/upgrade.php version [53bc57b249].















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace Garradin;

const UPGRADE_PROCESS = true;

require_once __DIR__ . '/../include/init.php';

$config = Config::getInstance();

try {
	if (Upgrade::preCheck()) {
		Upgrade::upgrade();
		exit(2);
	}
	else {
		exit(0);
	}
}
catch (UserException $e) {
	echo $e->getMessage() . PHP_EOL;
	exit(1);
}

Added src/templates/acc/_simple_help.tpl version [09ea62fa86].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<details>
	<summary class="help block">
		Attention&nbsp;: dans cette vue simplifiée,
		{if $type == Entities\Accounting\Account::TYPE_THIRD_PARTY}les dettes de ce tiers envers l'association apparaissent au crédit (positif) et les créances au débit (négatif)&nbsp;!
		{elseif $type}les écritures apparaissent tels que sur le relevé de banque ou le journal de caisse&nbsp;!
		{else}les comptes de banque, caisse, dépenses et tiers apparaissent de manière «&nbsp;simplifiée&nbsp;»&nbsp;!
		{/if}
		C'est l'inverse de la {if $link}<a href="{$link}">{/if}la vue comptable{if $link}</a>{/if} en comptabilité en partie double.</summary>
	<div class="help block">
		<p>L'extrait de compte fourni par la banque fonctionne « à l'envers », parce qu'il est établi du point de vue de la banque&nbsp;:</p>
		<ul>
			<li>les sommes versées sur votre compte (salaires etc.) constituent pour elle une ressource ( = crédit ; simultanément, cela augmente la dette de la banque à votre égard, ou réduit votre dette à son égard si vous êtes «&nbsp;débiteur&nbsp;»),</li>
			<li>les sommes retirées (paiement de chèques, carte bleue, etc.) constituent une utilisation (&nbsp;=&nbsp;débit).</li>
		</ul>
		<p>Du point de vue du client de la banque, dans une comptabilité en partie double, ce que la banque appelle crédit (une entrée d'argent) est un débit (c'est une utilisation de l'argent), et inversement (un débit pour la banque est une ressource de son client, donc, pour lui, un crédit). (<a href="https://fr.wikipedia.org/wiki/Comptabilit%C3%A9_en_partie_double">Source Wikipedia</a>)</p>
	</div>
</details>

Added src/templates/acc/_year_select.tpl version [9d69235d94].











>
>
>
>
>
1
2
3
4
5
<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$current_year.label} — {$current_year.start_date|date_short} au {$current_year.end_date|date_short}</h3>
	<footer>{linkbutton label="Changer d'exercice" href="!acc/years/select.php?from=%s"|args:rawurlencode($self_url) shape="settings"}</footer>
</nav>

Added src/templates/acc/accounts/deposit.tpl version [f0e1448813].





















































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{include file="admin/_head.tpl" title="Dépôt en banque : %s — %s"|args:$account.code,$account.label current="acc/accounts"}

<p class="help">
	Cocher les cases correspondant aux montants à déposer, une nouvelle écriture sera générée.
</p>

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">
	<table class="list">
		<thead>
			<tr>
				<td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>
				<td></td>
				<td>Date</td>
				<td>Réf. écriture</td>
				<td>Réf. ligne</td>
				<th>Libellé</th>
				<td class="money">Montant</td>
				<td class="money">Solde cumulé</td>
			</tr>
		</thead>
		<tbody>
			{foreach from=$journal item="line"}
			{if isset($line.sum)}
			<tr>
				<td colspan="5"></td>
				<td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|html_money:false}</td>
				<th>Solde au {$line.date|date_short}</th>
				<td colspan="2"></td>
			</tr>
			{else}
			<tr>
				<td class="check">
					{input type="checkbox" name="deposit[%d]"|args:$line.id value="1" data-debit=$line.debit|abs data-credit=$line.credit default=$line.checked}
				</td>
				<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
				<td>{$line.date|date_short}</td>
				<td>{$line.reference}</td>
				<td>{$line.line_reference}</td>
				<th>{$line.label}</th>
				<td class="money">{$line.debit|raw|html_money}</td>
				<td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|html_money:false}</td>
			</tr>
			{/if}
		{/foreach}
		</tbody>
	</table>

	<fieldset>
		<legend>Détails de l'écriture de dépôt</legend>
		<dl>
			{input type="text" name="label" label="Libellé" required=1 default="Dépôt en banque"}
			{input type="date" name="date" default=$date label="Date" required=1}
			{input type="money" name="amount" label="Montant" required=1}
			{input type="list" target="acc/charts/accounts/selector.php?chart=%d&targets=%d"|args:$account.id_chart,$target name="account_transfer" label="Compte de dépôt" required=1}
			{input type="text" name="reference" label="Numéro de pièce comptable"}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_deposit_%s"|args:$account.id}
		{button type="submit" name="save" label="Enregistrer" class="main" shape="check"}
	</p>
</form>

{literal}
<script type="text/javascript">
var total = 0;
$('tbody input[type=checkbox]').forEach((e) => {
	e.addEventListener('change', () => {
		var v = e.getAttribute('data-debit') || e.getAttribute('data-credit');
		v = parseInt(v, 10);
		total += e.checked ? v : -v;
		if (total < 0) {
			total = 0;
		}
		$('#f_amount').value = g.formatMoney(total);
	});
});

$('#f_all').addEventListener('change', (e) => {
	$('#f_amount').value = '';
	total = 0;
});
</script>
{/literal}

{include file="admin/_foot.tpl"}

Added src/templates/acc/accounts/index.tpl version [d00593bab9].





















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{include file="admin/_head.tpl" title="Comptes favoris" current="acc/accounts"}

{include file="acc/_year_select.tpl"}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/accounts/">Comptes favoris</a></li>
		<li><a href="{$admin_url}acc/reports/trial_balance.php?year={$current_year.id}">Balance générale (tous les comptes)</a></li>
		<li><a href="{$admin_url}acc/search.php?year={$current_year.id}">Recherche</a></li>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
			<li><a href="{$admin_url}acc/charts/accounts/all.php?id={$chart_id}">Plan comptable</a></li>
		{/if}
	</ul>
</nav>

{include file="acc/_simple_help.tpl" link="../reports/trial_balance.php?year=%d"|args:$current_year.id type=null}

{if !empty($grouped_accounts)}
	<table class="list">
		<thead>
			<tr>
				<td class="num">Numéro</td>
				<th>Compte</th>
				<td class="money">Solde</td>
				<td></td>
				<td></td>
			</tr>
		</thead>
		{foreach from=$grouped_accounts item="group"}
		<tbody>
			<tr>
				<td colspan="5"><h2 class="ruler">{$group.label}</h2></td>
			</tr>
			{foreach from=$group.accounts item="account"}
				<tr>
					<td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$current_year.id}">{$account.code}</a></td>
					<th><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$current_year.id}">{$account.label}</a></th>
					<td class="money">
						{if $account.sum < 0}<strong class="error">{/if}
						{$account.sum|raw|money_currency:false}
						{if $account.sum < 0}</strong>{/if}
					</td>
					<td>
						{if $account.type == Entities\Accounting\Account::TYPE_THIRD_PARTY}
						<em class="alert">
							{if $account.sum < 0}(Dette)
							{elseif $account.sum > 0}(Créance)
							{/if}
						</em>
						{/if}
					</td>
					<td class="actions">
						{linkbutton label="Journal" shape="menu" href="journal.php?id=%d&year=%d"|args:$account.id,$current_year.id}
						{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
							{if $account.type == Entities\Accounting\Account::TYPE_BANK}
								{linkbutton label="Rapprochement" shape="check" href="reconcile.php?id=%d"|args:$account.id}
							{elseif $account.type == Entities\Accounting\Account::TYPE_OUTSTANDING}
								{linkbutton label="Dépôt en banque" shape="check" href="deposit.php?id=%d"|args:$account.id}
							{/if}
						{/if}
					</td>
				</tr>
			{/foreach}
		</tbody>
		{/foreach}
	</table>
{/if}

<p class="help">
	Note : n'apparaissent ici que les comptes favoris.
	Pour voir le solde de tous les comptes, se référer à la <a href="{$admin_url}acc/reports/trial_balance.php?year={$current_year.id}">balance générale de l'exercice</a>.
</p>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/accounts/journal.tpl from [f69dd30f9f] to [eea4b60d81].

1
2
3



4




5

6



7
8
9





10
11

12
13
14
15
16
17
18
19
20
21
22



23
24

25

26



27
28
29
30





31
32
33

34
35
36
37
38
39
40
41
42


43
44
45
46

47
48
49
50
51
52
53





54
55



56
57





58



59
60
61
62





63



64
65
66
67
68
69




70

















71
72
73
74
75


76
77
{if null !== $suivi}
    {include file="admin/_head.tpl" title="Journal : %s - %s"|args:$compte.id:$compte.libelle current="compta/banques" body_id="rapport"}




    <ul class="actions">




        <li><a href="{$admin_url}compta/banques/">Comptes bancaires</a></li>

        <li{if $compte.id == Compta\Comptes::CAISSE} class="current"{/if}><a href="{$admin_url}compta/comptes/journal.php?id={$id_caisse}&amp;suivi">Journal de caisse</a></li>



        <li{if $compte.id == Compta\Comptes::CHEQUE_A_ENCAISSER} class="current"{/if}><a href="{$admin_url}compta/comptes/journal.php?id={$id_cheque_a_encaisser}&amp;suivi">Chèques à encaisser</a></li>
        <li{if $compte.id == Compta\Comptes::CARTE_A_ENCAISSER} class="current"{/if}><a href="{$admin_url}compta/comptes/journal.php?id={$id_carte_a_encaisser}&amp;suivi">Paiements par carte à encaisser</a></li>
    </ul>





{else}
    {include file="admin/_head.tpl" title="Journal : %s - %s"|args:$compte.id:$compte.libelle current="compta/gestion" body_id="rapport"}

{/if}

{if count($exercices)}
    <form action="{$self_url_no_qs}" method="get" class="shortFormRight">
        <fieldset>
            <legend><label for="f_exercice">Afficher le journal de l'exercice suivant :</label></legend>
            <p>
                <select name="exercice" id="f_exercice" onchange="this.form.submit();">
                {foreach from=$exercices item="exercice"}
                    <option value="{$exercice.id}"{if $exercice_selectionne == $exercice.id} selected="selected"{/if}>{$exercice.libelle}</option>
                {/foreach}



                </select>
                <input type="hidden" name="id" value="{$compte.id}" />

                {if null !== $suivi}<input type="hidden" name="suivi" value=""/>{/if}

                <noscript><input type="submit" value="Afficher"/></noscript>



            </p>
        </fieldset>
    </form>
{/if}






<table class="list">
    <colgroup>

        <col width="3%" />
        <col width="12%" />
        <col width="10%" />
        <col width="12%" />
        <col />
        <col width="6%" />
    </colgroup>
    <thead>
        <tr>


            <td>N°</td>
            <td>Date</td>
            <td>Montant</td>
            <td>Solde cumulé</td>

            <th>Libellé</th>
            <td></td>
        </tr>
    </thead>
    <tbody>
    {foreach from=$journal item="ligne"}
        <tr>





            <td class="num"><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id}">{$ligne.id}</a></td>
            <td>{$ligne.date|date_fr:'d/m/Y'}</td>



            <td>{if $ligne.compte_credit == $compte.id}{$credit}{else}{$debit}{/if}{$ligne.montant|escape|html_money}</td>
            <td>{$ligne.solde|escape|html_money}</td>





            <th>{$ligne.libelle}</th>



            <td class="actions">
                <a class="icn" href="{$admin_url}compta/operations/voir.php?id={$ligne.id}" title="Détails de l'écriture">❓</a>
            {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
                <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id}" title="Modifier cette écriture">✎</a>





            {/if}



            </td>
        </tr>
    {/foreach}
    </tbody>
    <tfoot>
        <tr>




            <td colspan="3"></td>

















            <th>Solde</th>
            <td colspan="2">{$solde|escape|html_money} {$config.monnaie}</td>
        </tr>
    </tfoot>
</table>



{include file="admin/_foot.tpl"}
<
|

>
>
>
|
>
>
>
>
|
>
|
>
>
>
|
<
|
>
>
>
>
>
|
<
>
|
|
|
|
|
|
<
<
<
<
|
>
>
>
|
<
>
|
>
|
>
>
>
|
<
|
|
>
>
>
>
>
|
|
<
>
|
|
<
<
|
<
|
<
|
>
>
|
|
|
|
>
|
<
<
|
|
|
|
>
>
>
>
>
|
|
>
>
>
|
|
>
>
>
>
>
|
>
>
>
|
<
<
<
>
>
>
>
>
|
>
>
>
|
|
|
|
|
|
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
<
|
|

>
>



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
24

25
26
27
28
29
30
31




32
33
34
35
36

37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53

54
55
56


57

58

59
60
61
62
63
64
65
66
67


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

132
133
134
135
136
137
138

{include file="admin/_head.tpl" title="Journal : %s - %s"|args:$account.code:$account.label current="acc/accounts" body_id="rapport"}

{if empty($year)}
	{include file="acc/_year_select.tpl"}
{else}
	<nav class="acc-year">
		<h4>Exercice sélectionné&nbsp;:</h4>
		<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
	</nav>
{/if}

{if $account.type}

	{if $simple && $account::isReversed($account.type)}
		{include file="acc/_simple_help.tpl" link="?id=%d&simple=0&year=%d"|args:$account.id,$year.id type=$account.type}
	{/if}


	{if $simple}
		{if $account.type == $account::TYPE_THIRD_PARTY}
			{if $sum < 0}
				<p class="alert block">Vous devez <strong>{$sum|abs|raw|money_currency}</strong> à ce tiers.</p>
			{elseif $sum > 0}
				<p class="alert block">Ce tiers vous doit <strong>{$sum|abs|raw|money_currency}</strong>.</p>
			{else}

				<p class="confirm block">Vous ne devez pas d'argent à ce tiers, et il ne vous en doit pas non plus.</p>
			{/if}
		{elseif $account.type == $account::TYPE_BANK}
			{if $sum < 0}
				<p class="error block">Ce compte est à découvert de <strong>{$sum|abs|raw|money_currency}</strong> à la banque.</p>
			{elseif $sum >= 0}
				<p class="confirm block">Ce compte est créditeur de <strong>{$sum|abs|raw|money_currency}</strong> à la banque.</p>




			{/if}
		{elseif $account.type == $account::TYPE_CASH}
			{if $sum < 0}
				<p class="error block">Cette caisse est débiteur de <strong>{$sum|abs|raw|money_currency}</strong>. Est-ce normal&nbsp;? Une vérification est peut-être nécessaire&nbsp;?</p>
			{elseif $sum >= 0}

				<p class="confirm block">Cette caisse est créditrice de <strong>{$sum|abs|raw|money_currency}</strong>.</p>
			{/if}
		{elseif $account.type == $account::TYPE_OUTSTANDING}
			{if $sum < 0}
				<p class="error block">Ce compte est débiteur <strong>{$sum|abs|raw|money_currency}</strong>. Est-ce normal&nbsp;? Une vérification est peut-être nécessaire&nbsp;?</p>
			{elseif $sum >= 0}
				<p class="confirm block">Ce compte d'attente est créditeur de <strong>{$sum|abs|raw|money_currency}</strong>. {if $sum > 200}Un dépôt à la banque serait peut-être une bonne idée&nbsp;?{/if}</p>
			{/if}

		{/if}
	{/if}


	<nav class="tabs">
		<aside>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
			{linkbutton href="%s&export=csv"|args:$self_url label="Export CSV" shape="export"}
			{linkbutton href="%s&export=ods"|args:$self_url label="Export tableur" shape="export"}

		{/if}
		{if $year.id == CURRENT_YEAR_ID}
			{linkbutton href="!acc/transactions/new.php?account=%d"|args:$account.id label="Saisir une écriture dans ce compte" shape="plus"}


		{/if}

		</aside>

		<ul>
			<li{if $simple} class="current"{/if}><a href="?id={$account.id}&amp;simple=1&amp;year={$year.id}">Vue simplifiée</a></li>
			<li{if !$simple} class="current"{/if}><a href="?id={$account.id}&amp;simple=0&amp;year={$year.id}">Vue comptable</a></li>
		</ul>
	</nav>
{/if}

<form method="post" action="{$admin_url}acc/transactions/actions.php">



{include file="common/dynamic_list_head.tpl" check=$can_edit}

	{foreach from=$list->iterate() item="line"}
		<tr>
			{if $can_edit}
			<td class="check">
				{input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0}
			</td>
			{/if}
			<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
			<td>{$line.date|date_short}</td>
			{if $simple}
			<td class="money">{if $line.change > 0}+{else}-{/if}{$line.change|abs|raw|html_money}</td>
			{else}
			<td class="money">{$line.debit|raw|html_money}</td>
			<td class="money">{$line.credit|raw|html_money}</td>
			{/if}
			{if isset($line->sum)}
				<td class="money">{$line.sum|raw|html_money:false}</td>
			{/if}
			<td>{$line.reference}</td>
			<th>{$line.label}</th>
			{if !$simple}<td>{$line.line_label}</td>{/if}
			<td>{$line.line_reference}</td>
			<td class="num">{if $line.id_analytical}<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.code_analytical}</a>{/if}</td>
			<td class="actions">



			{if ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)}
				{if $line.type == Entities\Accounting\Transaction::TYPE_DEBT}
					{linkbutton shape="check" label="Régler cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$line.id}
				{elseif $line.type == Entities\Accounting\Transaction::TYPE_CREDIT}
					{linkbutton shape="export" label="Régler cette créance" href="!acc/transactions/new.php?payoff_for=%d"|args:$line.id}
				{/if}
			{/if}

				{linkbutton href="!acc/transactions/details.php?id=%d"|args:$line.id label="Détails" shape="search"}
			</td>
		</tr>
	{/foreach}
	</tbody>
	<tfoot>
		<tr>
			{if $can_edit}
				<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>
			{/if}
			{if !$simple}<td></td>{/if}
			<td colspan="3">Solde</td>
			<td class="money">{$sum|raw|html_money:false}</td>
			{if !$simple}<td></td>{/if}
			<td class="actions" colspan="5">
				{if $can_edit}
					<em>Pour les écritures cochées :</em>
					<input type="hidden" name="from" value="{$self_url}" />
					<input type="hidden" name="year" value="{$year.id}" />
					{csrf_field key="projects_action"}
					<select name="action">
						<option value="">— Choisir une action à effectuer —</option>
						<option value="change_analytical">Ajouter/enlever d'un projet</option>
						<option value="delete">Supprimer les écritures</option>
					</select>
					<noscript>
						{button type="submit" value="OK" shape="right" label="Valider"}
					</noscript>
				{/if}
			</td>

		</tr>
	</tfoot>
</table>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/accounts/reconcile.tpl from [77a239871a] to [cc53da36ae].

1
2


3

4
5
6
7

8
9
10
11
12
13
14
15


16
17
18
19
20
21
22
23
24
25
26
27


28
29
30
31
32
33




34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

57

58
59
60
61
62
63
64
65
66
67
68
69
70

71

72
73





74
75
76
77

78

79
80
81


82
83
84
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99

100

101
102
103
104
{include file="admin/_head.tpl" title="Rapprochement — %s"|args:$compte.id current="compta/banques" js=1}



<ul class="actions">

    <li><a href="{$admin_url}compta/banques/">Comptes bancaires</a></li>
    <li><a href="{$admin_url}compta/comptes/journal.php?id={$id_caisse}&amp;suivi">Journal de caisse</a></li>
    <li class="current"><a href="{$self_url_no_qs}?id={$compte.id}">Rapprochement</a></li>
</ul>


<form method="get" action="{$self_url_no_qs}">
    {if !empty($prev) && !empty($next)}
    <fieldset class="shortFormRight">
        <legend>Rapprochement par mois</legend>
        <dl>
            <dd class="actions">
            <a class="icn" href="{$self_url_no_qs}?id={$compte.id}&amp;debut={$prev|date_fr:'Y-m-01'}&amp;fin={$prev|date_fr:'Y-m-t'}{if qg('sauf')}&amp;sauf=1{/if}">&larr; {$prev|date_fr:'F Y'}</a>


            | <a class="icn" href="{$self_url_no_qs}?id={$compte.id}&amp;debut={$next|date_fr:'Y-m-01'}&amp;fin={$next|date_fr:'Y-m-t'}{if qg('sauf')}&amp;sauf=1{/if}">{$next|date_fr:'F Y'} &rarr;</a>
            </dd>
        </dl>
    </fieldset>
    {/if}
    <fieldset>
        <legend>Période de rapprochement</legend>
        <p>
            Du
            <span><input type="date" name="debut" id="f_debut" value="{$debut}" /></span>
            au
            <span><input type="date" name="fin" id="f_fin" value="{$fin}" /></span>


            <label><input type="checkbox" name="sauf" value="1" {if qg('sauf')} checked="checked"{/if} /> Ne pas inclure les écritures déjà rapprochées</label>
            <input type="hidden" name="id" value="{$compte.id}" />
            <input type="submit" value="Afficher" />
        </p>
    </fieldset>
</form>





{form_errors}

<form method="post" action="{$self_url}">
    <table class="list">
        <colgroup>
            <col width="3%" />
            <col width="3%" />
            <col width="3%" />
            <col width="12%" />
            <col width="10%" />
            <col width="12%" />
            <col />
            <col width="5%" />
            <col width="3%" />
        </colgroup>
        <thead>
            <tr>
                <td class="check"><input type="checkbox" title="Tout cocher / décocher" /></td>
                <td></td>
                <td></td>
                <td>Date</td>
                <td>Montant</td>

                <td>Solde cumulé</td>

                <th>Libellé</th>
                <th>Numéro pièce</th>
                <th>Numéro chèque</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td colspan="5"></td>
                <td>{$solde_initial|escape|html_money} {$config.monnaie}</td>
                <th>Solde au {$debut|format_sqlite_date_to_french}</th>
                <td colspan="2"></td>
            </tr>
        {foreach from=$journal item="ligne"}

            <tr>

                <td class="check"><input type="checkbox" name="rapprocher[{$ligne.id}]" value="1" {if $ligne.date_rapprochement}checked="checked"{/if} /></td>
                <td class="num"><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id}">{$ligne.id}</a></td>





                <td class="actions">
                {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
                    <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id}" title="Modifier cette opération">✎</a>
                {/if}

                </td>

                <td>{$ligne.date|date_fr:'d/m/Y'}</td>
                <td>{if $ligne.compte_credit == $compte.id}-{else}+{/if}{$ligne.montant|escape|html_money}</td>
                <td>{$ligne.solde|escape|html_money}</td>


                <th>{$ligne.libelle}</th>
                <td>{$ligne.numero_piece}</td>
                <td>{$ligne.numero_cheque}</td>
            </tr>

        {/foreach}
        </tbody>
        <tfoot>
            <tr>
                <td colspan="5"></td>
                <td>{$solde_final|escape|html_money} {$config.monnaie}</td>
                <th>Solde au {$fin|format_sqlite_date_to_french}</th>
                <td colspan="2"></td>
            </tr>
        </tfoot>
    </table>
    <p class="submit">
        {csrf_field key="compta_rapprocher_%s"|args:$compte.id}
        <input type="submit" name="save" value="Enregistrer" />

        <input type="submit" name="save_next" value="Enregistrer et aller au mois suivant &rarr;" class="minor" />

    </p>
</form>

{include file="admin/_foot.tpl"}
|

>
>
|
>
|
|
<
|
>


|
|
|
|
|
|
>
>
|
|
|
|
|
|
|
|
|
|
|
|
>
>
|
|
|
|
|

>
>
>
>




|
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|
<
|
|
>
|
>
|
|
|
|
|
|
<
<
<
<
<
<
|
>
|
>
|
|
>
>
>
>
>
|
<
<
<
>
|
>
|
|
|
>
>
|
|
|
|
>
|
|
<
<
<
<
<
<
<
<
|
|
|
|
>
|
>
|



1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49











50
51
52
53

54
55
56
57
58
59
60
61
62
63
64






65
66
67
68
69
70
71
72
73
74
75
76



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91








92
93
94
95
96
97
98
99
100
101
102
{include file="admin/_head.tpl" title="Rapprochement : %s — %s"|args:$account.code,$account.label current="acc/accounts"}

{include file="acc/_year_select.tpl"}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/accounts/reconcile.php?id={$account.id}">Rapprochement manuel</a></li>
		<li><a href="{$admin_url}acc/accounts/reconcile_assist.php?id={$account.id}">Rapprochement assis</a></li>

	</ul>
</nav>

<form method="get" action="{$self_url_no_qs}">
	{if $prev || $next}
	<fieldset class="shortFormRight">
		<legend>Rapprochement par mois</legend>
		<p>
			{if $prev}
				{linkbutton shape="left" href=$prev.url label=$prev.date|date_fr:'F Y'}
			{/if}
			{if $next}
				{linkbutton shape="right" href=$next.url label=$next.date|date_fr:'F Y'}
			{/if}
		</p>
	</fieldset>
	{/if}
	<fieldset class="shortFormLeft">
		<legend>Période de rapprochement</legend>
		<p>
			Du
			{input type="date" name="start" default=$start}
			au
			{input type="date" name="end" default=$end}
		</p>
		<p>
			<label>{input type="checkbox" name="only" value=1 default=$only} Seulement les écritures non rapprochées</label>
			<input type="hidden" name="id" value="{$account.id}" />
			<input type="submit" value="Afficher" />
		</p>
	</fieldset>
</form>

<p class="block help">
	Les écritures apparaissent ici dans le sens du relevé de banque, à l'inverse des journaux comptables.
</p>

{form_errors}

<form method="post" action="{$self_url}">
	<table class="list">











		<thead>
			<tr>
				<td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>
				<td></td>

				<td>Date</td>
				<td class="money">Débit</td>
				<td class="money">Crédit</td>
				<td class="money">Solde cumulé</td>
				<td class="money">Solde rapproché</td>
				<th>Libellé</th>
				<th>Réf. écriture</th>
				<th>Réf. ligne</th>
			</tr>
		</thead>
		<tbody>






			{foreach from=$journal item="line"}
			{if isset($line->sum)}
			<tr>
				<td colspan="5"></td>
				<td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|html_money:false}</td>
				<td class="money">{if $line.reconciled_sum > 0}-{/if}{$line.reconciled_sum|abs|raw|html_money}</td>
				<th>Solde au {$line.date|date_short}</th>
				<td colspan="2"></td>
			</tr>
			{else}
			<tr>
				<td class="check">



					{input type="checkbox" name="reconcile[%d]"|args:$line.id_line value="1" default=$line.reconciled}
				</td>
				<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
				<td>{$line.date|date_short}</td>
				<td class="money">{$line.credit|raw|html_money}</td>
				<td class="money">{$line.debit|raw|html_money}</td> {* Not a bug! Credit/debit is reversed here to reflect the bank statement *}
				<td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|html_money:false}</td>
				<td class="money">{if $line.reconciled_sum > 0}-{/if}{$line.reconciled_sum|abs|raw|html_money:false}</td>
				<th>{$line.label}</th>
				<td>{$line.reference}</td>
				<td>{$line.line_reference}</td>
			</tr>
			{/if}
		{/foreach}
		</tbody>








	</table>
	<p class="submit">
		{csrf_field key="acc_reconcile_%s"|args:$account.id}
		{button type="submit" name="save" label="Enregistrer" class="main" shape="check"}
		{if $next}
			{button type="submit" name="save_next" label="Enregistrer et aller au mois suivant" class="main minor" shape="right"}
		{/if}
	</p>
</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/accounts/reconcile_assist.tpl version [4e7ccd5117].





























































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
{include file="admin/_head.tpl" title="Rapprochement : %s — %s"|args:$account.code,$account.label current="acc/accounts"}

{include file="acc/_year_select.tpl"}

<nav class="tabs">
	<ul>
		<li><a href="{$admin_url}acc/accounts/reconcile.php?id={$account.id}">Rapprochement manuel</a></li>
		<li class="current"><a href="{$admin_url}acc/accounts/reconcile_assist.php?id={$account.id}">Rapprochement assisté</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data">
	{if !$csv->loaded()}
		<fieldset>
			<legend>Relevé de compte</legend>
			<p class="help block">
				Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br />
				<a href="https://fossil.kd2.org/garradin/wiki?name=Compta/Rapprochement_assist%C3%A9" target="_blank">Aide détaillée</a>
			</p>
			<dl>
				{include file="common/_csv_help.tpl"}
				{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			</dl>
			<p class="submit">
				{csrf_field key=$csrf_key}
				{button type="submit" name="upload" label="Envoyer le fichier" class="main" shape="upload"}
			</p>
		</fieldset>
	{elseif !$csv->ready()}
		{include file="common/_csv_match_columns.tpl"}
		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="cancel" value="1" label="Annuler" shape="left"}
			{button type="submit" name="assign" label="Continuer" class="main" shape="right"}
		</p>
	{else}
		<fieldset>
			<legend>Relevé de compte</legend>
			<dl>
				<dt>
					Nombre de lignes
				</dt>
				<dd>
					{$csv->count()}
				</dd>
				<dt>
					Période couverte
				</dt>
				<dd>
					Du {$start|date_short} au {$end|date_short}
				</dd>
			</dl>
			<p class="submit">
				{csrf_field key=$csrf_key}
				{button type="submit" name="cancel" value="1" label="Annuler" shape="left"}
			</p>
		</fieldset>
	{/if}
</form>

{if !empty($lines)}
	<p class="block help">
		Les écritures apparaissent ici dans le sens du relevé de banque, à l'inverse des journaux comptables.
	</p>

	<form method="post" action="{$self_url}">
		<table class="list">
			<thead>
				<tr>
					<th colspan="6">Journal du compte (compta)</th>
					<td class="separator"></td>
					<th colspan="4" class="separator">Extrait de compte (banque)</th>
				</tr>
				<tr>
					<td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>
					<td></td>
					<td>Date</td>
					<td class="money">Mouvement</td>
					<td class="money">Solde cumulé</td>
					<th style="text-align: right">Libellé</th>
					<td class="separator"></td>
					<th class="separator">Libellé</th>
					<td class="money">Mouvement</td>
					<td class="money">Solde cumulé</td>
					<td>Date</td>
				</tr>
			</thead>
			<tbody>
				{foreach from=$lines key="line_id" item="line"}
				{if isset($line->journal->sum)}
				<tr>
					<td colspan="4"></td>
					<td class="money">{if $line.journal.sum > 0}-{/if}{$line.journal.sum|abs|raw|html_money:false}</td>
					<th style="text-align: right">Solde au {$line.journal.date|date_short}</th>
					<td class="separator"></td>
					<td class="separator"></td>
					<td colspan="3"></td>
				</tr>
				{else}
				<tr>
					{if isset($line->journal)}
						<td class="check">
							{input type="checkbox" name="reconcile[%d]"|args:$line.journal.id_line value="1" default=$line.journal.reconciled}
						</td>
						<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.journal.id}">#{$line.journal.id}</a></td>
						<td>{$line.journal.date|date_short}</td>
						<td class="money">
							{if $line.journal.credit}
								{* Not a bug! Credit/debit is reversed here to reflect the bank statement *}
								-{$line.journal.credit|raw|html_money}
							{else}
								{$line.journal.debit|raw|html_money}
							{/if}
						</td>
						<td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|html_money:false}</td>
						<th style="text-align: right">{$line.journal.label}</th>
					{else}
						<td colspan="5"></td>
						<td style="text-align: right">
							{if $line.add}
							{* FIXME later add ability to pre-fill multi-line transactions in new.php
								{linkbutton label="Créer cette écriture" target="_blank" href="%s&create=%s"|args:$self_url,$line_id shape="plus"}
							*}
							{/if}
						</td>
					{/if}
						<td class="separator">
						{if $line->journal && $line->csv}
							==
						{else}
							<b class="icn">⚠</b>
						{/if}
						</td>
					{if isset($line->csv)}
						<th class="separator">{$line.csv.label}</th>
						<td class="money">
							{$line.csv.amount|raw|html_money}
						</td>
						<td class="money">{$line.csv.running_sum|raw|html_money}</td>
						<td>{$line.csv.date|date_short}</td>
					{else}
						<td colspan="4" class="separator"></td>
					{/if}
				</tr>
				{/if}
			{/foreach}
			</tbody>
		</table>
		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="save" label="Enregistrer" class="main" shape="check"}
		</p>
	</form>
{/if}

{include file="admin/_foot.tpl"}

Added src/templates/acc/accounts/simple.tpl version [d13f106002].



















































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
{include file="admin/_head.tpl" title="Suivi : %s"|args:$types[$type] current="acc/simple"}

{if empty($year)}
	{include file="acc/_year_select.tpl"}
{else}
	<nav class="acc-year">
		<h4>Exercice sélectionné&nbsp;:</h4>
		<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
	</nav>
{/if}

<nav class="tabs">
	<aside>
	{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		{linkbutton href="?type=%d&export=csv"|args:$type label="Export CSV" shape="export"}
		{linkbutton href="?type=%d&export=ods"|args:$type label="Export tableur" shape="export"}
	{/if}
	</aside>
	<ul>
		{foreach from=$types key="key" item="label"}
		<li{if $type == $key} class="current"{/if}><a href="?type={$key}">{$label}</a></li>
		{/foreach}
	</ul>
</nav>

{if !$list->count()}
	<p class="alert block">
		Aucune écriture à afficher.
	</p>
{else}
	<form method="post" action="{$admin_url}acc/transactions/actions.php">

	{include file="common/dynamic_list_head.tpl" check=$can_edit}

		{foreach from=$list->iterate() item="line"}
			<tr>
				{if $can_edit}
				<td class="check">
					{input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0}
				</td>
				{/if}
				<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
				<td>{$line.date|date_short}</td>
				<td class="money">{$line.change|abs|raw|html_money}</td>
				<td>{$line.reference}</td>
				<th>{$line.label}</th>
				<td>{$line.line_reference}</td>
				<td class="num">{if $line.id_analytical}<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.code_analytical}</a>{/if}</td>
				<td class="actions">
					{if $line.type == Entities\Accounting\Transaction::TYPE_DEBT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)}
						{linkbutton shape="check" label="Régler cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$line.id}
					{elseif $line.type == Entities\Accounting\Transaction::TYPE_CREDIT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)}
						{linkbutton shape="export" label="Régler cette créance" href="!acc/transactions/new.php?payoff_for=%d"|args:$line.id}
					{/if}

					{linkbutton href="!acc/transactions/details.php?id=%d"|args:$line.id label="Détails" shape="search"}
				</td>
			</tr>
		{/foreach}
		</tbody>
		{if $can_edit}
			<tfoot>
			<tr>
				<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>
				<td class="actions" colspan="10">
					<em>Pour les écritures cochées :</em>
					<input type="hidden" name="from" value="{$self_url}" />
					<input type="hidden" name="year" value="{$year.id}" />
					{csrf_field key="projects_action"}
					<select name="action">
						<option value="">— Choisir une action à effectuer —</option>
						<option value="change_analytical">Ajouter/enlever d'un projet</option>
						<option value="delete">Supprimer les écritures</option>
					</select>
					<noscript>
						{button type="submit" value="OK" shape="right" label="Valider"}
					</noscript>
				</td>
			</tr>
		</tfoot>
		{/if}
	</table>

	</form>

	{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}
{/if}

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/accounts/_account_form.tpl version [67f92e389c].





































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<dl>
	{input type="select" label="Type de compte favori" name="type" source=$account required=true options=$types}
	<dd class="help">Le statut de compte favori est utilisé pour les écritures <em>«&nbsp;simplifiées&nbsp;»</em> (recettes, dépenses, dettes, créances, virements), pour la liste des comptes, et également pour proposer certaines fonctionnalités (rapprochement pour les comptes bancaires, règlement rapide de dette et créance, dépôt de chèques).</dd>
	<dd class="help">Un compte qui n'a pas de type favori ne pourra être utilisé que dans une saisie avancée, et ne sera visible que dans les rapports de l'exercice.</dd>

	{if !$simple}
		<dt><label for="f_position_0">Position au bilan ou résultat</label>{if !$edit_disabled} <b>(obligatoire)</b>{/if}</dt>
		<dd class="help">La position permet d'indiquer dans quelle partie du bilan ou du résultat doit figurer le compte.</dd>
		<dd class="help">Les comptes inscrits en actif ou passif figureront dans le bilan, alors que ceux inscrits en produit ou charge figureront au compte de résultat.</dd>
		{input type="radio" label="Ne pas utiliser ce compte au bilan ni au résultat" name="position" value=0 source=$account disabled=$edit_disabled}
		{input type="radio" label="Bilan : actif" name="position" value=Entities\Accounting\Account::ASSET source=$account help="ce que possède l'association : stocks, locaux, soldes bancaires, etc." disabled=$edit_disabled}
		{input type="radio" label="Bilan : passif" name="position" value=Entities\Accounting\Account::LIABILITY source=$account help="ce que l'association doit : dettes, provisions, réserves, etc." disabled=$edit_disabled}
		{input type="radio" label="Bilan : actif ou passif" name="position" value=Entities\Accounting\Account::ASSET_OR_LIABILITY source=$account help="le compte sera placé à l'actif si son solde est débiteur, ou au passif s'il est créditeur" disabled=$edit_disabled}
		{input type="radio" label="Résultat : charge" name="position" value=Entities\Accounting\Account::EXPENSE source=$account help="dépenses" disabled=$edit_disabled}
		{input type="radio" label="Résultat : produit" name="position" value=Entities\Accounting\Account::REVENUE source=$account help="recettes" disabled=$edit_disabled}
	{/if}
</dl>

<dl id="code_container">
	{input type="text" label="Code" maxlength="10" name="code" source=$account required=true help="Le code du compte sert à trier le compte dans le plan comptable, attention à choisir un code qui correspond au plan comptable." disabled=$edit_disabled}
</dl>

<dl>
	{input type="text" label="Libellé" name="label" source=$account required=true disabled=$edit_disabled}
	{input type="textarea" label="Description" name="description" source=$account}
</dl>

{if isset($translate_type_position, $translate_type_codes)}
<script type="text/javascript">
var types_positions = {$translate_type_position|escape:json};
var types_codes = {$translate_type_codes|escape:json};
var simple = {$simple|escape:json};

{literal}
$('#f_type').onchange = changeType;
function changeType() {
	var v = $('#f_type').value;

	if ($('#f_position_0')) {
		if (v in types_positions) {
			$('#f_position_' + types_positions[v]).checked = true;
		}
		else {
			$('#f_position_3').checked = true;
		}
	}

	var code = $('#f_code');
	if (types_codes[v]) {
		code.value = types_codes[v];
	}
	else {
		code.value = '';
	}

	if (simple && !(v in types_codes)) {
		g.toggle('#code_container', true);
	}
	else if (simple) {
		g.toggle('#code_container', false);
	}
}
changeType();
{/literal}
</script>
{/if}

Added src/templates/acc/charts/accounts/_nav.tpl version [ec091420e5].

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/charts/">Plans comptables</a></li>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		<li><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li>
		{/if}
	</ul>
	<ul class="sub">
		<li class="title">{$chart.label}</li>
		<li{if $current == 'favorites'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/?id={$chart.id}">Comptes favoris</a></li>
		<li{if $current == 'all'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/all.php?id={$chart.id}">Tous les comptes</a></li>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
			<li{if $current == 'new'} class="current"{/if}><a href="{$admin_url}acc/charts/accounts/new.php?id={$chart.id}"><strong>Ajouter un compte</strong></a></li>
		{/if}
	</ul>
</nav>

Added src/templates/acc/charts/accounts/all.tpl version [532b7dcf92].











































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{include file="admin/_head.tpl" title="Plan comptable"|args:$chart.label current="acc/charts"}

{include file="acc/charts/accounts/_nav.tpl" current="all"}

<p class="help">
	Les comptes marqués comme «&nbsp;<em>Ajouté</em>&nbsp;» ont été ajoutés au plan comptable officiel par vous-même.
</p>

<table class="accounts">
	<tbody>
	{foreach from=$accounts item="account"}
		<tr class="account-level-{$account.code|strlen}">
			<td>{$account.code}</td>
			<th>{$account.label}</th>
			<td>
				{if $account.type}
					{icon shape="star"} <?=Entities\Accounting\Account::TYPES_NAMES[$account->type]?>
				{/if}
			</td>
			<td>
				{if $account.user}<em>Ajouté</em>{/if}
			</td>
			<td class="actions">
				{if $session->canAccess('compta', Membres::DROIT_ADMIN) && !$chart.archived}
					{if $account.user || !$chart.code}
						{linkbutton shape="delete" label="Supprimer" href="!acc/charts/accounts/delete.php?id=%d"|args:$account.id}
					{/if}
					{linkbutton shape="edit" label="Modifier" href="!acc/charts/accounts/edit.php?id=%d"|args:$account.id}
				{/if}
			</td>
		</tr>
	{/foreach}
	</tbody>
</table>


{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/accounts/delete.tpl version [b79c3f0fab].





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
{include file="admin/_head.tpl" title="Supprimer un compte" current="acc/charts"}

{include file="common/delete_form.tpl"
	legend="Supprimer ce plan comptable ?"
	warning="Êtes-vous sûr de vouloir supprimer le compte « %s — %s » ?"|args:$account.code,$account.label
	alert="Attention, le compte ne pourra pas être supprimé si des écritures y sont affectées (sauf en tant que compte analytique)."
	csrf_key="acc_accounts_delete_%s"|args:$account.id
}

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/accounts/edit.tpl version [0283435372].





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Modifier un compte" current="acc/charts"}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	{if $edit_disabled}
		<p class="block alert">
			Il n'est pas possible de modifier le libellé, le code ou la position de ce compte car il {if $account.user}est utilisé par des écritures liées à des exercices clôturés{else}fait partie du plan comptable officiel{/if}.<br />
			Pour pouvoir modifier ce compte pour l'exercice courant, il est conseillé de <a href="{$admin_url}acc/charts/?from={$account.id_chart}">créer un nouveau plan comptable</a> en y recopiant l'ancien plan comptable.
		</p>
	{/if}

	<fieldset>
		<legend>Modifier un compte</legend>
		{include file="acc/charts/accounts/_account_form.tpl" simple=false}
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_accounts_edit_%s"|args:$account.id}
		{button type="submit" name="edit" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/accounts/index.tpl version [d986fd5237].









































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{include file="admin/_head.tpl" title="Comptes favoris" current="acc/charts"}

{include file="acc/charts/accounts/_nav.tpl" current="favorites"}

<table class="list">
{foreach from=$accounts_grouped item="group"}
	<tbody>
		<tr>
			<td colspan="3"><h2 class="ruler">{$group.label}</h2></td>
			<td class="actions">
				{if !$chart.archived}
					{linkbutton label="Ajouter un compte" shape="plus" href="!acc/charts/accounts/new.php?id=%d&type=%d"|args:$chart.id,$group.type}
				{/if}
			</td>
		</tr>

	{foreach from=$group.accounts item="account"}
		<tr>
			<td class="num">{$account.code}</td>
			<th>{$account.label}</th>
			<td class="desc">{$account.description}</td>
			<td class="actions">
				{if $session->canAccess('compta', Membres::DROIT_ADMIN) && !$chart.archived}
					{if $account.user || !$chart.code}
						{linkbutton shape="delete" label="Supprimer" href="!acc/charts/accounts/delete.php?id=%d"|args:$account.id}
					{/if}
					{linkbutton shape="edit" label="Modifier" href="!acc/charts/accounts/edit.php?id=%d"|args:$account.id}
				{/if}
			</td>
		</tr>
	{/foreach}
	</tbody>
{/foreach}
</table>

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/accounts/new.tpl version [4b1c300015].











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{include file="admin/_head.tpl" title="Nouveau compte" current="acc/charts"}

{include file="acc/charts/accounts/_nav.tpl" current="new"}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Créer un nouveau compte</legend>
		{include file="acc/charts/accounts/_account_form.tpl" simple=$simple edit_disabled=false}
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_accounts_new"}
		{button type="submit" name="save" label="Créer" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/accounts/selector.tpl version [6cbc945144].























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{include file="admin/_head.tpl" title="Sélectionner un compte" body_id="popup" is_popup=true}

{if empty($grouped_accounts) && empty($accounts)}
	<p class="block alert">Le plan comptable ne comporte aucun compte de ce type. Pour afficher des comptes ici, les <a href="{$www_url}admin/acc/charts/accounts/all.php?id={$chart.id}" target="_blank">modifier dans le plan comptable</a> en sélectionnant le type de compte favori voulu.</td>

{elseif isset($grouped_accounts)}

	<?php $index = 1; ?>
	{foreach from=$grouped_accounts item="group"}
		<h2 class="ruler">{$group.label}</h2>

		<table class="list">
			<tbody>
			{foreach from=$group.accounts item="account"}
				<tr data-idx="{$index}">
					<td>{$account.code}</td>
					<th>{$account.label}</th>
					<td class="desc">{$account.description}</td>
					<td class="actions">
						<button class="icn-btn" value="{$account.id}" data-label="{$account.code} — {$account.label}" data-icon="&rarr;">Sélectionner</button>
					</td>
				</tr>
				<?php $index++; ?>
			{/foreach}
			</tbody>
		</table>
	{/foreach}

{else}

	<h2 class="ruler">
		<input type="text" placeholder="Recherche rapide" id="lookup" />
		<label>{input type="checkbox" name="typed_only" value=0 default=0 default=$all} N'afficher que les comptes favoris</label>
	</h2>

	<table class="accounts">
		<tbody>
		{foreach from=$accounts item="account"}
			<tr data-idx="{$iteration}" class="account-level-{$account.code|strlen}">
				<td>{$account.code}</td>
				<th>{$account.label}</th>
				<td>
				{if $account.type}
					{icon shape="star"} <?=Entities\Accounting\Account::TYPES_NAMES[$account->type]?>
				{/if}
				</td>
				<td class="actions">
					<button class="icn-btn" value="{$account.id}" data-label="{$account.code} — {$account.label}" data-icon="&rarr;">Sélectionner</button>
				</td>
			</tr>
		{/foreach}
		</tbody>
	</table>

{/if}

<script type="text/javascript" src="{$admin_url}static/scripts/selector.js?{$version_hash}"></script>

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/delete.tpl version [664469a9cf].



















>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
{include file="admin/_head.tpl" title="Supprimer un plan comptable" current="acc/charts"}

{include file="common/delete_form.tpl"
	legend="Supprimer ce plan comptable ?"
	warning="Êtes-vous sûr de vouloir supprimer le plan comptable « %s » ?"|args:$chart.label
	csrf_key="acc_charts_delete_%s"|args:$chart.id
}

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/edit.tpl version [21a0b34d02].











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{include file="admin/_head.tpl" title="Modifier un plan comptable" current="acc/charts"}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">
	<fieldset>
		<legend>Modifier un plan comptable</legend>
		<dl>
			{input type="text" name="label" label="Libellé" required=1 source=$chart}
			{input type="select" name="country" label="Pays" required=1 options=$country_list source=$chart}
			<dt><label for="f_archived_1">Archivage</label></dt>
			{input type="checkbox" name="archived" value="1" source=$chart label="Plan comptable archivé" help="Ce plan comptable ne pourra plus être modifié"}
		</dl>
		<p class="submit">
			{csrf_field key="acc_charts_edit_%d"|args:$chart.id}
			{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
		</p>
	</fieldset>
</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/import.tpl version [8e50447ace].













































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{include file="admin/_head.tpl" title="Importer un nouveau plan comptable" current="acc/charts"}

<nav class="tabs">
	<ul>
		<li><a href="{$admin_url}acc/charts/">Plans comptables</a></li>
		<li class="current"><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="1">
	<fieldset>
		<legend>Importer un plan comptable</legend>
		<dl>
			{input type="text" name="label" label="Libellé" required=1}
			{input type="select" name="country" label="Pays" required=1 options=$country_list default=$config.pays}
			{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			<dd class="help">
				Règles à suivre pour créer le fichier CSV&nbsp;:
				<ul>
					<li>Il est recommandé d'utiliser LibreOffice pour créer le fichier CSV</li>
					<li>Le fichier doit être en UTF-8</li>
					<li>Le séparateur doit être le point-virgule ou la virgule</li>
					<li>Cocher l'option <em>"Mettre en guillemets toutes les cellules du texte"</em></li>
					<li>Le fichier doit comporter les colonnes suivantes : <em>{$columns}</em></li>
					<li>Pour obtenir un exemple du format attendu, faire un export d'un plan comptable existant</li>
				</ul>
			</dd>
		</dl>
	</fieldset>
	<p class="submit">
		{csrf_field key="acc_charts_import"}
		{button type="submit" name="import" label="Importer" shape="upload" class="main"}
	</p>
</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/charts/index.tpl version [edc5b4d9b1].

















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="Gestion des plans comptables" current="acc/charts"}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/charts/">Plans comptables</a></li>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
			<li><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable</a></li>
		{/if}
	</ul>
</nav>

{if $_GET.msg == 'OPEN'}
<p class="block alert">
	Il n'existe aucun exercice ouvert.
	{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures.
	{/if}
</p>
{/if}

{if count($list)}
	<table class="list">
		<thead>
			<td>Pays</td>
			<th>Libellé</th>
			<td>Type</td>
			<td>Archivé</td>
			<td></td>
		</thead>
		<tbody>
			{foreach from=$list item="item"}
				<tr{if $item.archived} class="disabled"{/if}>
					<td>{$item.country|get_country_name}</td>
					<th><a href="{$admin_url}acc/charts/accounts/?id={$item.id}">{$item.label}</a></th>
					<td>{if $item.code}Officiel{else}Personnel{/if}</td>
					<td>{if $item.archived}<em>Archivé</em>{/if}</td>
					<td class="actions">
						{linkbutton shape="star" label="Comptes favoris" href="!acc/charts/accounts/?id=%d"|args:$item.id}
						{linkbutton shape="menu" label="Tous les comptes" href="!acc/charts/accounts/all.php?id=%d"|args:$item.id}
						{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
							{linkbutton shape="edit" label="Modifier" href="!acc/charts/edit.php?id=%d"|args:$item.id}
							{linkbutton shape="export" label="Export CSV" href="!acc/charts/export.php?id=%d"|args:$item.id}
							{linkbutton shape="export" label="Export tableur" href="!acc/charts/export.php?id=%d&ods"|args:$item.id}
							{if !$item.code && !$item.archived}
								{linkbutton shape="delete" label="Supprimer" href="!acc/charts/delete.php?id=%d"|args:$item.id}
							{/if}
						{/if}
					</td>
				</tr>
			{/foreach}
		</tbody>
	</table>
{/if}

{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
	<form method="post" action="{$self_url_no_qs}">
		<fieldset>
			<legend>Créer un nouveau plan comptable</legend>
			<dl>
				{input type="select_groups" name="copy" options=$charts_groupped label="Recopier depuis" required=1 default=$from}
				{input type="text" name="label" label="Libellé" required=1}
				{input type="select" name="country" label="Pays" required=1 options=$country_list default=$config.pays}
			</dl>
			<p class="submit">
				{csrf_field key="acc_charts_new"}
				{button type="submit" name="new" label="Créer" shape="right" class="main"}
			</p>
		</fieldset>
	</form>
{/if}

{include file="admin/_foot.tpl"}

Added src/templates/acc/index.tpl version [275f4e0a55].

































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{include file="admin/_head.tpl" title="Comptabilité" current="acc"}

{foreach from=$years item="year"}
<section class="year-infos">
	<h2 class="ruler">{$year.label} —
		Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2>

	<nav class="tabs">
		<aside>
			{linkbutton shape="search" href="!acc/search.php?year=%d"|args:$year.id label="Recherche"}
			{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
				{linkbutton shape="upload" href="!acc/years/import.php?year=%d"|args:$year.id label="Import & export"}
			{/if}
		</aside>
		<ul>
			<li><a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a></li>
			<li><a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a></li>
			<li><a href="{$admin_url}acc/reports/journal.php?year={$year.id}">Journal général</a></li>
			<li><a href="{$admin_url}acc/reports/ledger.php?year={$year.id}">Grand livre</a></li>
			<li><a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a></li>
			<li><a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a></li>
		</ul>
	</nav>

	{if $year.nb_transactions > 3}
		<section class="graphs">
			{foreach from=$graphs key="url" item="label"}
			<figure>
				<img src="{$url|args:'year='|cat:$year.id}" alt="" />
				<figcaption>{$label}</figcaption>
			</figure>
			{/foreach}
		</section>
	{else}
		<p class="help block">Il n'y a pas encore suffisamment d'écritures dans cet exercice pour pouvoir afficher les statistiques.</p>
		<p>{linkbutton label="Saisir une nouvelle écriture" shape="plus" href="transactions/new.php"}</p>
	{/if}
</section>


{foreachelse}
	<p class="block alert">
		Il n'y a aucun exercice ouvert en cours.<br />
		{linkbutton label="Ouvrir un nouvel exercice" shape="plus" href="!acc/years/new.php"}
	</p>
{/foreach}

{include file="admin/_foot.tpl"}

Modified src/templates/acc/reports/_header.tpl from [1061b17f5a] to [c9b9d26764].

1















2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
<div class="exercice">















    <h2>{$config.nom_asso}</h2>
    {if isset($projet)}
        <h3>Projet&nbsp;: {$projet.libelle}</h3>
    {else}

        <p>Exercice comptable {if $exercice.cloture}clôturé{else}en cours{/if} du
            {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}, généré le {$cloture|date_fr:'d/m/Y'}</p>
    {/if}

	<p class="noprint">
		<button onclick="window.print(); return false;">
			<b href="#need_js" class="action icn print">⎙</b>
			Imprimer
		</button>
	</p>
</div>

|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
>
|
|
|

|
|
<
<
<


<
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27



28
29

<div class="year-header">

	<nav class="tabs noprint">
		<ul>
			{if isset($analytical)}
			<li><strong><a href="{$admin_url}acc/reports/projects.php">Projets</a></strong></li>
			{/if}
			<li{if $current == "graphs"} class="current"{/if}><a href="{$admin_url}acc/reports/graphs.php?{$criterias_query}">Graphiques</a></li>
			<li{if $current == "trial_balance"} class="current"{/if}><a href="{$admin_url}acc/reports/trial_balance.php?{$criterias_query}">Balance générale</a></li>
			<li{if $current == "journal"} class="current"{/if}><a href="{$admin_url}acc/reports/journal.php?{$criterias_query}">Journal général</a></li>
			<li{if $current == "ledger"} class="current"{/if}><a href="{$admin_url}acc/reports/ledger.php?{$criterias_query}">Grand livre</a></li>
			<li{if $current == "statement"} class="current"{/if}><a href="{$admin_url}acc/reports/statement.php?{$criterias_query}">Compte de résultat</a></li>
			<li{if $current == "balance_sheet"} class="current"{/if}><a href="{$admin_url}acc/reports/balance_sheet.php?{$criterias_query}">Bilan</a></li>
		</ul>
	</nav>

	<h2>{$config.nom_asso}</h2>
	{if isset($analytical)}
		<h3>Projet&nbsp;: {$analytical.label}</h3>
	{/if}
	{if isset($year)}
		<p>Exercice comptable {if $year.closed}clôturé{else}en cours{/if} du
			{$year.start_date|date_short} au {$year.end_date|date_short}, généré le {$close_date|date_short}</p>
	{/if}

	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>



	</p>
</div>

Added src/templates/acc/reports/_journal.tpl version [37e07b0e8b].

































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<table class="list multi">
	<thead>
		<tr>
			<td>Réf.</td>
			<td>Date</td>
			<th>Libellé</th>
			<td>Comptes</td>
			<td class="money">Débit</td>
			<td class="money">Crédit</td>
			<td>Libellé ligne</td>
			<td>Réf. ligne</td>
		</tr>
	</thead>
	{foreach from=$journal item="transaction"}
	<tbody>
		<tr>
			<td rowspan="{$transaction.lines|count}" class="num"><a href="{$admin_url}acc/transactions/details.php?id={$transaction.id}">{if $transaction.reference}{$transaction.reference}{else}#{$transaction.id}{/if}</a></td>
			<td rowspan="{$transaction.lines|count}">{$transaction.date|date_short}</td>
			<th rowspan="{$transaction.lines|count}">{$transaction.label}</th>
		{foreach from=$transaction.lines item="line"}
			<td>{$line.account_code} - {$line.account_label}</td>
			<td class="money">{$line.debit|raw|html_money}</td>
			<td class="money">{$line.credit|raw|html_money}</td>
			<td>{$line.label}</td>
			<td>{$line.reference}</td>
		</tr>
		<tr>
		{/foreach}
		</tr>
	</tbody>
	{/foreach}
</table>

Modified src/templates/acc/reports/_statement_table.tpl from [97ca40a29c] to [59dd236cd8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<table>
    <colgroup>
        <col width="50%" />
        <col width="50%" />
    </colgroup>
    <tbody>
        <tr>
            <td>
                <table>
                    {if $header}
                    <caption><h3>Charges</h3></caption>
                    {/if}
                    <tbody>
                    {foreach from=$comptes.charges.comptes key="parent_code" item="parent"}
                        <tr class="parent">
                            <th>{$parent_code|get_nom_compte}</th>
                            <td>{$parent.solde|escape|html_money}</td>
                        </tr>
                        {foreach from=$parent.comptes item="solde" key="compte"}
                        <tr class="compte">
                            <th><a href="{$admin_url}compta/comptes/journal.php?id={$compte}">{$compte|get_nom_compte}</a></th>
                            <td>{$solde|escape|html_money}</td>
                        </tr>
                        {/foreach}
                    {/foreach}
                    </tbody>
                </table>
            </td>
            <td>
                <table>
                    {if $header}
                    <caption><h3>Produits</h3></caption>
                    {/if}
                    <tbody>
                    {foreach from=$comptes.produits.comptes key="parent_code" item="parent"}
                        <tr class="parent">
                            <th>{$parent_code|get_nom_compte}</th>
                            <td>{$parent.solde|escape|html_money}</td>
                        </tr>
                        {foreach from=$parent.comptes item="solde" key="compte"}
                        <tr class="compte">
                            <th><a href="{$admin_url}compta/comptes/journal.php?id={$compte}">{$compte|get_nom_compte}</a></th>
                            <td>{$solde|escape|html_money}</td>
                        </tr>
                        {/foreach}
                    {/foreach}
                    </tbody>
                </table>
            </td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
            <td>
                <table>
                    <tfoot>

                        <tr>
                            <th>Total charges</th>
                            <td>{$comptes.charges.total|escape|html_money}</td>
                        </tr>
                    </tfoot>
                </table>
            </td>
           <td>
                <table>
                    <tfoot>
                        <tr>
                            <th>Total produits</th>
                            <td>{$comptes.produits.total|escape|html_money}</td>
                        </tr>
                    </tfoot>
                </table>
            </td>
        </tr>
        {if $result}
        <tr>
            <td>
            {if ($comptes.resultat >= 0)}
                <table>
                    <tfoot>
                        <tr>
                            <th>Résultat (excédent)</th>
                            <td>{$comptes.resultat|escape|html_money}</td>
                        </tr>
                    </tfoot>
                </table>


            {/if}
            </td>
            <td>
            {if ($comptes.resultat < 0)}
                <table>
                    <tfoot>
                        <tr>
                            <th>Résultat (déficit)</th>
                            <td>{$comptes.resultat|escape|html_money}</td>
                        </tr>
                    </tfoot>
                </table>
            {/if}
            </td>
        </tr>
        {/if}
    </tfoot>
</table>
|
<
<
<
<
<
<
<
<
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
>
|
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
>
>
|
|
<
<
<
<
<
|
|
|
<
<
|
|
<
<
<

1








2
3
4








































5



6
7






8






















9
10
11
12





13
14
15


16
17



18
<table class="list">








	{if !empty($caption)}
		<caption><h3>{$caption}</h3></caption>
	{/if}








































	<tbody>



	{foreach from=$accounts item="account"}
		<tr class="compte">






			<td class="num">






















				{if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$year.id}">{$account.code}</a>
				{else}{$account.code}
				{/if}
			</td>





			<th>{$account.label}</th>
			<td class="money">{$account.sum|raw|html_money}</td>
		</tr>


	{/foreach}
	</tbody>



</table>

Modified src/templates/acc/reports/balance_sheet.tpl from [f3d93246ee] to [0249d23d75].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{include file="admin/_head.tpl" title="Bilan" current="compta/exercices" body_id="rapport"}

{include file="admin/compta/rapports/_header.tpl"}

<table>
    <colgroup>
        <col width="50%" />
        <col width="50%" />
    </colgroup>
    <tbody>
        <tr>
            <td>
                <table>
                    <caption><h3>Actif</h3></caption>
                    <tbody>
                    {foreach from=$bilan.actif.comptes key="parent_code" item="parent"}
                        <tr class="parent">
                            <th>{$parent_code|get_nom_compte}</th>
                            <td>{$parent.solde|escape|html_money}</td>
                        </tr>
                        {foreach from=$parent.comptes item="solde" key="compte"}
                        <tr class="compte">
                            <th><a href="{$admin_url}compta/comptes/journal.php?id={$compte}">{$compte|get_nom_compte}</a></th>
                            <td>{$solde|escape|html_money}</td>
                        </tr>
                        {/foreach}
                    {/foreach}
                    </tbody>
                </table>
            </td>
            <td>
                <table>
                    <caption><h3>Passif</h3></caption>
                    <tbody>
                    {foreach from=$bilan.passif.comptes key="parent_code" item="parent"}
                        <tr class="parent">
                            <th>{$parent_code|get_nom_compte}</th>
                            <td>{$parent.solde|escape|html_money}</td>
                        </tr>
                        {foreach from=$parent.comptes item="solde" key="compte"}
                        <tr class="compte">
                            <th><a href="{$admin_url}compta/comptes/journal.php?id={$compte}">{$compte|get_nom_compte}</a></th>
                            <td>{$solde|escape|html_money}</td>
                        </tr>
                        {/foreach}
                    {/foreach}
                    </tbody>
                </table>
            </td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
            <td>
                <table>
                    <tfoot>
                        <tr>
                            <th>Total actif</th>
                            <td>{$bilan.actif.total|escape|html_money}</td>
                        </tr>
                    </tfoot>
                </table>
            </td>
           <td>
                <table>
                    <tfoot>
                        <tr>
                            <th>Total passif</th>
                            <td>{$bilan.passif.total|escape|html_money}</td>
                        </tr>
                    </tfoot>
                </table>
            </td>
        </tr>
    </tfoot>
</table>

<p class="help">Toutes les opérations sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}
|

|

|
|
|
|
|
|
|
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|


|


1
2
3
4
5
6
7
8
9
10
11
12

13















14
15

16















17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{include file="admin/_head.tpl" title="Bilan" current="acc/years"}

{include file="acc/reports/_header.tpl" current="balance_sheet"}

<table class="statement">
	<colgroup>
		<col width="50%" />
		<col width="50%" />
	</colgroup>
	<tbody>
		<tr>
			<td>

				{include file="acc/reports/_statement_table.tpl" accounts=$asset caption="Actif"}















			</td>
			<td>

				{include file="acc/reports/_statement_table.tpl" accounts=$liability caption="Passif"}















			</td>
		</tr>
	</tbody>
	<tfoot>
		<tr>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total actif</th>
							<td class="money">{$asset_sum|raw|html_money:false}</td>
						</tr>
					</tfoot>
				</table>
			</td>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total passif</th>
							<td class="money">{$liability_sum|raw|html_money:false}</td>
						</tr>
					</tfoot>
				</table>
			</td>
		</tr>
	</tfoot>
</table>

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}

Added src/templates/acc/reports/graphs.tpl version [526c29e562].

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Graphiques" current="acc"}

{include file="acc/reports/_header.tpl" current="graphs"}

<section class="year-infos">
	<section class="graphs">
		{foreach from=$graphs key="url" item="label"}
		<figure>
			<img src="{$url|args:$criterias_query}" alt="" />
			<figcaption>{$label}</figcaption>
		</figure>
		{/foreach}
	</section>
</section>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/reports/journal.tpl from [7a19ce99c8] to [5b585d401b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{include file="admin/_head.tpl" title="Journal général" current="compta/exercices" body_id="rapport"}

{include file="admin/compta/rapports/_header.tpl"}

<table class="list multi">
    <thead>
        <tr>
            <td>Date</td>
            <th>Intitulé</th>
            <td>Comptes</td>
            <td>Débit</td>
            <td>Crédit</td>
        </tr>
    </thead>
    <tbody>
    {foreach from=$journal item="ligne"}
        <tr>
            <td rowspan="2">{$ligne.date|date_fr:'d/m/Y'}</td>
            <th rowspan="2">{$ligne.libelle}</th>
            <td>{$ligne.compte_debit} - {$ligne.compte_debit|get_nom_compte}</td>
            <td>{$ligne.montant|escape|html_money}</td>
            <td></td>
        </tr>
        <tr>
            <td>{$ligne.compte_credit} - {$ligne.compte_credit|get_nom_compte}</td>
            <td></td>
            <td>{$ligne.montant|escape|html_money}</td>
        </tr>
    {/foreach}
    </tbody>
</table>

<p class="help">Toutes les opérations sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}
|

|

<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

|


1
2
3
4











5















6
7
8
9
{include file="admin/_head.tpl" title="Journal général" current="acc/years"}

{include file="acc/reports/_header.tpl" current="journal"}












{include file="acc/reports/_journal.tpl"}
















<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/reports/ledger.tpl from [9f320d7052] to [58b1dc5251].

1
2
3
4
5


6
7
8
9



10
11
12
13
14
15

16
17

18
19
20


21
22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74
75
76
77
78



79
80
81
82
83
84
85
86
87
88
89
{include file="admin/_head.tpl" title="Grand livre" current="compta/exercices" body_id="rapport"}

{include file="admin/compta/rapports/_header.tpl"}

{foreach from=$livre.classes key="classe" item="comptes"}


<h3>{$classe|get_nom_compte}</h3>

{foreach from=$comptes item="compte" key="code"}
    {foreach from=$compte.comptes item="souscompte" key="souscode"}



    <table class="list">
        <caption><h4>{$souscode} — {$souscode|get_nom_compte}</h4></caption>
        <colgroup>
            <col width="15%" />
            <col width="55%" />
            <col width="10%" />

            <col width="10%" />
            <col width="10%" />

        </colgroup>
        <thead>
            <tr>


                <td>Date</td>
                <th>Intitulé</th>
                <td class="money">Débit</td>
                <td class="money">Crédit</td>
                <td class="money">Solde</td>
            </tr>
        </thead>
        <tbody>
        {foreach from=$souscompte.journal item="ligne"}
            <tr>


                <td>{$ligne.date|date_fr:'d/m/Y'}</td>
                <th>{$ligne.libelle}</th>
                <td class="money">{if $ligne.compte_debit == $souscode}{$ligne.montant|escape|html_money}{/if}</td>
                <td class="money">{if $ligne.compte_credit == $souscode}{$ligne.montant|escape|html_money}{/if}</td>
                <td class="money">{$ligne.solde|escape|html_money}</td>
            </tr>
        {/foreach}
        </tbody>
        <tfoot>
            <tr>
                <td></td>
                <th>Solde final</th>
                <td class="money">{if $souscompte.debit > 0}{$souscompte.debit|escape|html_money}{/if}</td>
                <td class="money">{if $souscompte.credit > 0}{$souscompte.credit|escape|html_money}{/if}</td>
                <td class="money">{$souscompte.solde|escape|html_money}</td>
            </tr>
        </tfoot>
    </table>
    {/foreach}



    <table class="list">
        <colgroup>
            <col width="15%" />
            <col width="65%" />
            <col width="10%" />
            <col width="10%" />
        </colgroup>
        <tfoot>
            <tr>
                <td>Total</td>
                <th>{$code|get_nom_compte}</th>
                <td>{if $compte.total > 0}{$compte.total|abs|escape|html_money}{/if}</td>
                <td>{if $compte.total < 0}{$compte.total|abs|escape|html_money}{/if}</td>
            </tr>
        </tfoot>
    </table>

    {/foreach}
{/foreach}

<table class="list">
    <colgroup>
        <col width="15%" />
        <col width="65%" />
        <col width="10%" />
        <col width="10%" />
    </colgroup>
    <tfoot>
        <tr>



            <td><strong>Total</strong></td>
            <th></th>
            <td>{$livre.debit|escape|html_money}</td>
            <td>{$livre.credit|escape|html_money}</td>
        </tr>
    </tfoot>
</table>

<p class="help">Toutes les opérations sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}
|

|

|
>
>
|

|
|
>
>
>
|
<
|
|
|
|
>
|
|
>
|
|
|
>
>
|
|
|
|
|
|
|
|
|
|
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>

>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
|


|
<
|
|
|
<
<
<
|
>
>
>
|
|
|
|
|
|
<

|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

84
85
86



87
88
89
90
91
92
93
94
95
96

97
98
99
100
{include file="admin/_head.tpl" title="Grand livre" current="acc/years"}

{include file="acc/reports/_header.tpl" current="ledger"}

<div class="year-header noprint">
	<button type="button" data-icon="↓" class="icn-btn" id="open_details">Déplier tous les comptes</button>
	<button type="button" data-icon="↑" class="icn-btn" id="close_details">Replier tous les comptes</button>
</div>

{foreach from=$ledger item="account"}

<details open="open">
	<summary><h2 class="ruler"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$account.id_year}">{$account.code} — {$account.label}</a></h2></summary>

	<table class="list">

		<colgroup>
			<col width="5%" />
			<col width="5%" />
			<col width="10%" />
			<col width="50%" />
			<col width="10%" />
			<col width="10%" />
			<col width="10%" />
		</colgroup>
		<thead>
			<tr>
				<td>Réf.</td>
				<td>Réf. ligne</td>
				<td>Date</td>
				<th>Intitulé</th>
				<td class="money">Débit</td>
				<td class="money">Crédit</td>
				<td class="money">Solde</td>
			</tr>
		</thead>
		<tbody>
		{foreach from=$account.lines item="line"}
			<tr>
				<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">{if $line.reference}{$line.reference}{else}#{$line.id}{/if}</a></td>
				<td class="num">{$line.line_reference}</td>
				<td>{$line.date|date_short}</td>
				<th>{$line.label}{if $line.line_label} <em>({$line.line_label})</em>{/if}</th>
				<td class="money">{$line.debit|raw|html_money}</td>
				<td class="money">{$line.credit|raw|html_money}</td>
				<td class="money">{$line.running_sum|raw|html_money:false}</td>
			</tr>
		{/foreach}
		</tbody>
		<tfoot>
			<tr>
				<td colspan="3"></td>
				<th>Solde final</th>
				<td class="money">{$account.debit|raw|html_money}</td>
				<td class="money">{$account.credit|raw|html_money}</td>
				<td class="money">{$account.sum|raw|html_money:false}</td>
			</tr>
		</tfoot>
	</table>

</details>

{if isset($account->all_debit)}
	<table class="list">
		<colgroup>
			<col width="70%" />
			<col width="10%" />
			<col width="10%" />
			<col width="10%" />
		</colgroup>
		<tfoot>
			<tr>
				<td><strong>Totaux</strong></td>
				<td class="money">{$account.all_debit|raw|html_money:false}</td>
				<td class="money">{$account.all_credit|raw|html_money:false}</td>
				<td></td>
			</tr>
		</tfoot>
	</table>
{/if}

{/foreach}

{literal}

<script type="text/javascript">
document.querySelector('#open_details').onclick = () => {
	document.querySelectorAll('details').forEach((e) => {



		e.setAttribute('open', 'open');
	});
};
document.querySelector('#close_details').onclick = () => {
	document.querySelectorAll('details').forEach((e) => {
		e.removeAttribute('open');
	});
};
</script>
{/literal}


<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}

Added src/templates/acc/reports/projects.tpl version [a54424c23b].

































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{include file="admin/_head.tpl" title="Projets" current="acc/years"}

<nav class="tabs">
	{if CURRENT_YEAR_ID}
	<aside>
		{linkbutton label="Créer un nouveau compte de projet" href="!acc/charts/accounts/new.php?id=%d&type=%d"|args:$current_year.id_chart,$analytical_type shape="plus"}
	</aside>
	{/if}

	<ul>
		<li><a href="{$admin_url}acc/years/">Exercices</a></li>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		<li><a href="{$admin_url}acc/years/new.php">Nouvel exercice</a></li>
		{/if}
		<li class="current"><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li>
	</ul>

	<aside>
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>
	</aside>
	<ul class="sub">
		<li{if !$by_year} class="current"{/if}><a href="{$self_url_no_qs}">Par projet</a></li>
		<li{if $by_year} class="current"{/if}><a href="{$self_url_no_qs}?by_year=1">Par exercice</a></li>
	</ul>
</nav>

{if !empty($list)}


	<table class="list">
		<thead>
			<tr>
				<td>Année</td>
				<td></td>
				<td class="money">Débits</td>
				<td class="money">Crédits</td>
				<td class="money">Solde</td>
			</tr>
		</thead>
		{foreach from=$list item="parent"}
			<tbody>
				<tr>
					<th colspan="5">
						<h2 class="ruler">{$parent.label}</h2>
					</th>
				</tr>
			{foreach from=$parent.items item="item"}
				<tr>
					<th>{$item.label}</th>
					<td>
					<span class="noprint">
						<a href="{$admin_url}acc/reports/graphs.php?analytical={$item.id_account}&year={$item.id_year}">Graphiques</a>
						| <a href="{$admin_url}acc/reports/trial_balance.php?analytical={$item.id_account}&year={$item.id_year}">Balance générale</a>
						| <a href="{$admin_url}acc/reports/journal.php?analytical={$item.id_account}&year={$item.id_year}">Journal général</a>
						| <a href="{$admin_url}acc/reports/ledger.php?analytical={$item.id_account}&year={$item.id_year}">Grand livre</a>
						| <a href="{$admin_url}acc/reports/statement.php?analytical={$item.id_account}&year={$item.id_year}">Compte de résultat</a>
						| <a href="{$admin_url}acc/reports/balance_sheet.php?analytical={$item.id_account}&year={$item.id_year}">Bilan</a>
					</span>
					</td>
					<td class="money">{$item.debit|raw|html_money}</td>
					<td class="money">{$item.credit|raw|html_money}</td>
					<td class="money">{$item.sum|raw|html_money:false}</td>
				</tr>
			{/foreach}
			</tbody>
		{/foreach}
	</table>

{else}
	<p class="block alert">
		Il n'y a pas de projet visible en cours.
		{if $current_year && !$analytical_accounts_count}
			{linkbutton label="Créer un nouveau compte de projet" href="!acc/charts/accounts/new.php?id=%d&type=%d"|args:$current_year.id_chart,$analytical_type shape="plus"}
		{else}
			Le solde des projets apparaîtra quand des écritures seront affectées à ces projets.
		{/if}
	</p>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/acc/reports/statement.tpl from [4206fc3206] to [8f97001d6f].

1
2
3
4








5
6











7
























8






9








10





11
12
13
14
{include file="admin/_head.tpl" title="Compte de résultat" current="compta/exercices" body_id="rapport"}

{include file="admin/compta/rapports/_header.tpl"}









{include file="admin/compta/rapports/_table_resultat.tpl" comptes=$compte_resultat header=true result=true}












{if !empty($compte_nature.charges.comptes)}
























    <h2 class="ruler">Contributions en nature</h2>






    {include file="admin/compta/rapports/_table_resultat.tpl" comptes=$compte_nature header=false result=false}








{/if}






<p class="help">Toutes les opérations sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}
|

|

>
>
>
>
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
|
>
>
>
>
>

|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{include file="admin/_head.tpl" title="Compte de résultat" current="acc/years"}

{include file="acc/reports/_header.tpl" current="statement"}

<table class="statement">
	<colgroup>
		<col width="50%" />
		<col width="50%" />
	</colgroup>
	<tbody>
		<tr>
			<td>
				{include file="acc/reports/_statement_table.tpl" accounts=$expense caption="Charges"}
			</td>
			<td>
				{include file="acc/reports/_statement_table.tpl" accounts=$revenue caption="Produits"}
			</td>
		</tr>
	</tbody>
	<tfoot>
		<tr>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total charges</th>
							<td class="money">{$expense_sum|raw|html_money:false}</td>
						</tr>
					</tfoot>
				</table>
			</td>
			<td>
				<table>
					<tfoot>
						<tr>
							<th>Total produits</th>
							<td class="money">{$revenue_sum|raw|html_money:false}</td>
						</tr>
					</tfoot>
				</table>
			</td>
		</tr>
		{if $result}
		<tr>
			<td>
			{if ($result < 0)}
				<table>
					<tfoot>
						<tr>
							<th>Résultat (perte)</th>
							<td class="money">{$result|raw|html_money:false}</td>
						</tr>
					</tfoot>
				</table>
			{/if}
			</td>
			<td>
			{if ($result >= 0)}
				<table>
					<tfoot>
						<tr>
							<th>Résultat (excédent)</th>
							<td class="money">{$result|raw|html_money:false}</td>
						</tr>
					</tfoot>
				</table>
			{/if}
			</td>
		</tr>
		{/if}
	</tfoot>
</table>

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}

Added src/templates/acc/reports/trial_balance.tpl version [f08d97e86e].









































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{include file="admin/_head.tpl" title="Balance générale" current="acc/years"}

{include file="acc/reports/_header.tpl" current="trial_balance"}

<table class="list">
	<thead>
		<tr>
			<td>Numéro</td>
			<th>Compte</th>
			<td class="money">Total des débits</td>
			<td class="money">Total des crédits</td>
			<td class="money">Solde débiteur</td>
			<td class="money">Solde créditeur</td>
		</tr>
	</thead>
	<tbody>
	{foreach from=$balance item="account"}
		<tr>
			<td class="num">
				{if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&amp;year={$year.id}">{$account.code}</a>
				{else}{$account.code}
				{/if}
			</td>
			<th>{$account.label}</th>
			<td class="money">{$account.debit|raw|html_money}</td>
			<td class="money">{$account.credit|raw|html_money}</td>
			<td class="money">{if $account.sum < 0}{$account.sum|abs|escape|html_money}{/if}</td>
			<td class="money">{if $account.sum > 0}{$account.sum|abs|escape|html_money}{/if}</td>
		</tr>
	{/foreach}
	</tbody>
</table>

<p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p>

{include file="admin/_foot.tpl"}

Added src/templates/acc/search.tpl version [b1c54ffaff].





















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{include file="admin/_head.tpl" title="Recherche" current="acc" custom_js=['query_builder.min.js']}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$self_url}">Recherche</a></li>
		<li><a href="saved_searches.php">Recherches enregistrées</a></li>
	</ul>
</nav>

{include file="common/search/advanced.tpl" action_url=$self_url}

{if !empty($result)}
	{*if $session->canAccess('compta', Membres::DROIT_ECRITURE)}
		<form method="post" action="{$admin_url}membres/action.php" class="memberList">
	{/if*}

	<p class="help">{$result|count} écritures trouvées pour cette recherche.</p>
	<table class="list search">
		<thead>
			<tr>
				{*if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" /></td>{/if*}
				{foreach from=$result_header item="label"}
					<td>{$label}</td>
				{/foreach}
				<td></td>
			</tr>
		</thead>
		<tbody>
			{foreach from=$result item="row"}
				<tr>
					{*if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" name="selected[]" value="{$row.id}" /></td>{/if*}
					{foreach from=$row key="key" item="value"}
						{if $key == 'id'}
						<td class="num">
							<a href="{$admin_url}acc/transactions/details.php?id={$value}">{$value}</a>
						</td>
						{else}
						<td>
							{if $key == 'credit' || $key == 'debit'}
								{$value|raw|html_money:false}
							{elseif null == $value}
								<em>(nul)</em>
							{else}
								{$value}
							{/if}
						</td>
						{/if}
					{/foreach}
					<td class="actions">
						{linkbutton shape="search" label="Détails" href="!acc/transactions/details.php?id=%d"|args:$row.id}
					</td>
				</tr>
			{/foreach}
		</tbody>
	{*if $session->canAccess('membres', Membres::DROIT_ADMIN)}
		{include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1}
	{/if*}
	</table>

	{*if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
		</form>
	{/if*}

{elseif $result !== null}

	<p class="block alert">
		Aucun résultat trouvé.
	</p>

	</form>
{/if}


{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/_lines_form.tpl version [25878c61c0].































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
assert(is_array($lines));
assert(is_array($analytical_accounts));
assert(!isset($lines_accounts) || is_array($lines_accounts));
?>

<table class="list transaction-lines">
	<thead>
		<tr>
			<td>Compte</td>
			<td>Débit</td>
			<td>Crédit</td>
			<td>Réf. ligne</td>
			<td>Libellé ligne</td>
			{if count($analytical_accounts) > 1}
				<td>Projet</td>
			{/if}
			<td></td>
		</tr>
	</thead>
	<tbody>
	{foreach from=$lines key="k" item="line"}
		<tr>
			<td>
				{input type="list" target="acc/charts/accounts/selector.php?chart=%d"|args:$chart_id name="lines[account][]" default=$line.account}
			</td>
			<td class="money">{input type="money" name="lines[debit][]" default=$line.debit size=5}</td>
			<td class="money">{input type="money" name="lines[credit][]" default=$line.credit size=5}</td>
			<td>{input type="text" name="lines[reference][]" default=$line.reference size=10}</td>
			<td>{input type="text" name="lines[label][]" default=$line.label}</td>
			{if count($analytical_accounts) > 1}
				<td>{input default=$line.id_analytical type="select" name="lines[id_analytical][]" options=$analytical_accounts}</td>
			{/if}
			<td>{button label="Enlever" title="Enlever la ligne" shape="minus" min="2" name="remove_line"}</td>
		</tr>
	{/foreach}
	</tbody>
	<tfoot>
		<tr>
			<th>Total</th>
			<td class="money">{input type="money" name="debit_total" readonly="readonly" tabindex="-1" }</td>
			<td class="money">{input type="money" name="credit_total" readonly="readonly" tabindex="-1" }</td>
			<td colspan="{if count($analytical_accounts) > 1}3{else}2{/if}" id="lines_message"></td>
			<td>{button label="Ajouter" title="Ajouter une ligne" shape="plus"}</td>
		</tr>
	</tfoot>
</table>

Added src/templates/acc/transactions/actions_analytical.tpl version [326812a369].









































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{include file="admin/_head.tpl" title="Ajouter/supprimer des écritures à un projet" current="acc/accounts"}

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Affecter {$count} écritures sélectionnées à un projet</legend>
		<dl>
			<dd>
				{input type="select" name="id_analytical" options=$analytical_accounts label="Projet à utiliser" help="Pour retirer les écritures de leur projet actuellement affecté, sélectionner simplement « Aucun projet »."}
			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_actions"}
		<input type="hidden" name="action" value="add" />
		{button type="submit" name="change_analytical" label="Modifier les écritures" shape="right" class="main"}

		{if isset($extra)}
			{foreach from=$extra key="key" item="value"}
				{if is_array($value)}
					{foreach from=$value key="subkey" item="subvalue"}
						<input type="hidden" name="{$key}[{$subkey}]" value="{$subvalue}" />
					{/foreach}
				{else}
					<input type="hidden" name="{$key}" value="{$value}" />
				{/if}
			{/foreach}
		{/if}
	</p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/actions_delete.tpl version [94d2ed495a].























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
{include file="admin/_head.tpl" title="Supprimer %d écritures"|args:$count current="acc"}

{include file="common/delete_form.tpl"
	legend="Supprimer ces écritures ?"
	warning="Êtes-vous sûr de vouloir supprimer %d écritures ?"|args:$count
	confirm="Cocher cette case pour confirmer la suppression"
	csrf_key=$csrf_key
	extra=$extra
}

{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/creator.tpl version [75918e5101].











>
>
>
>
>
1
2
3
4
5
{include file="admin/_head.tpl" title="Écritures crées par %s"|args:$transaction_creator.identite current="acc/accounts"}

{include file="acc/reports/_journal.tpl"}

{include file="admin/_foot.tpl"}

Modified src/templates/acc/transactions/delete.tpl from [f5372a2a85] to [a0b4d20f5c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{include file="admin/_head.tpl" title="Supprimer une opération" current="compta/gestion"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer cette opération ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer l'opération{$operation.id}
            «&nbsp;{$operation.libelle}&nbsp;» du {$operation.date|date_fr:'d/m/Y'} ?
        </h3>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_supprimer_%d"|args:$operation.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|

<
|
<
<
<
|
<
|
<
<
<
|
<
<
<
<
|
<


1
2

3



4

5



6




7

8
9
{include file="admin/_head.tpl" title="Supprimer l'écriture n°%d"|args:$transaction.id current="acc"}


{include file="common/delete_form.tpl"



	legend="Supprimer cette écriture ?"

	warning="Êtes-vous sûr de vouloir supprimer l'écriture%d « %s » ?"|args:$transaction.id,$transaction.label



	csrf_key="acc_delete_%s"|args:$transaction.id




}


{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/details.tpl version [f76172b6ec].















































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
{include file="admin/_head.tpl" title="Écriture n°%d"|args:$transaction.id current="acc"}

{if $session->canAccess('compta', Membres::DROIT_ADMIN) && !$transaction->validated && !$tr_year->closed}
<nav class="tabs">
	<ul>
		<li><a href="edit.php?id={$transaction.id}">Modifier cette écriture</a></li>
		<li><a href="delete.php?id={$transaction.id}">Supprimer cette écriture</a></li>
	</ul>
</nav>
{/if}

{if $session->canAccess('compta', Membres::DROIT_ECRITURE) && $transaction.status & $transaction::STATUS_WAITING}
<div class="block alert">
	{if $transaction.type == $transaction::TYPE_DEBT}
		<h3>Dette en attente</h3>
		{linkbutton shape="check" label="Enregistrer le règlement de cette dette" href="!acc/transactions/new.php?payoff_for=%d"|args:$transaction.id}
	{else}
		<h3>Créance en attente</h3>
		{linkbutton shape="export" label="Enregistrer le règlement de cette créance" href="!acc/transactions/new.php?payoff_for=%d"|args:$transaction.id}
	{/if}
</div>
{/if}

<dl class="describe">
	{if $transaction.id_related}
	<dt>Écriture liée à</dt>
	<dd><a href="{$admin_url}acc/transactions/details.php?id={$transaction.id_related}">#{$transaction.id_related}</a>
		{if $transaction.type == $transaction::TYPE_DEBT || $transaction.type == $transaction::TYPE_CREDIT}(en règlement de){/if}
	</dd>
	{/if}
	<dt>Libellé</dt>
	<dd><h2>{$transaction.label}</h2></dd>
	<dt>Date</dt>
	<dd>{$transaction.date|date_fr:'l j F Y (d/m/Y)'}</dd>
	<dt>Numéro pièce comptable</dt>
	<dd>{if trim($transaction.reference)}{$transaction.reference}{else}-{/if}</dd>

	<dt>Exercice</dt>
	<dd>
		<a href="{$admin_url}acc/reports/ledger.php?year={$transaction.id_year}">{$tr_year.label}</a>
		| Du {$tr_year.start_date|date_short} au {$tr_year.end_date|date_short}
		| <strong>{if $tr_year.closed}Clôturé{else}En cours{/if}</strong>
	</dd>

	<dt>Écriture créée par</dt>
	<dd>
		{if $transaction.id_creator}
			{if $session->canAccess('compta', Membres::DROIT_ACCES)}
				<a href="{$admin_url}membres/fiche.php?id={$transaction.id_creator}">{$creator_name}</a>
			{else}
				{$creator_name}
			{/if}
		{else}
			<em>membre supprimé</em>
		{/if}
	</dd>

	<dt>Écriture liée à</dt>
	{if empty($related_users)}
		<dd><em>Aucun membre n'est lié à cette écriture.</em></dd>
	{else}
		{foreach from=$related_users item="u"}
			<dd>
				<a href="{$admin_url}membres/fiche.php?id={$u.id}">{$u.identity}</a>
				{if $u.id_service_user}— en règlement d'une <a href="{$admin_url}services/user.php?id={$u.id}&amp;only={$u.id_service_user}">activité</a>{/if}
			</dd>
		{/foreach}
	{/if}

	<dt>Remarques</dt>
	<dd>{if trim($transaction.notes)}{$transaction.notes|escape|nl2br}{else}-{/if}</dd>

	<dt>Fichiers joints</dt>
	{foreach from=$files item="file"}
	<dd>
		<aside class="file">
			<a target="_blank" href="{$file.url}">{$file.nom}</a>
			<small>({$file.type}, {$file.taille|format_bytes})</small>
			{linkbutton shape="download" href=$file.url target="_blank" label="Télécharger"}
			{linkbutton shape="delete" href="!acc/transactions/delete_file.php?id=%d&from=%d"|args:$file.id,$transaction.id label="Supprimer"}
		</aside>
	</dd>
	{foreachelse}
	<dd>-</dd>
	{/foreach}
</dl>

<table class="list">
	<thead>
		<tr>
			<td class="num">N° compte</td>
			<th>Compte</th>
			<td class="money">Débit</td>
			<td class="money">Crédit</td>
			<td>Libellé</td>
			<td>Référence</td>
			<td>Projet</td>
		</tr>
	</thead>
	<tbody>
		{foreach from=$transaction->getLinesWithAccounts() item="line"}
		<tr>
			<td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$line.id_account}&amp;year={$transaction.id_year}">{$line.account_code}</a></td>
			<td>{$line.account_name}</td>
			<td class="money">{if $line.debit}{$line.debit|escape|html_money}{/if}</td>
			<td class="money">{if $line.credit}{$line.credit|escape|html_money}{/if}</td>
			<td>{$line.label}</td>
			<td>{$line.reference}</td>
			<td>
				{if $line.id_analytical}
					<a href="{$admin_url}acc/reports/statement.php?analytical={$line.id_analytical}">{$line.analytical_name}</a>
				{/if}
			</td>
		</tr>
		{/foreach}
	</tbody>
</table>

{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/edit.tpl version [e42938496f].











































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{include file="admin/_head.tpl" title="Modification d'une écriture" current="acc/simple"}

<form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="#f_date">
	{form_errors}

	<fieldset>
		<legend>Type d'écriture</legend>
		<dl>
		{foreach from=$types_details item="type"}
			<dd class="radio-btn">
				{input type="radio" name="type" value=$type.id source=$transaction label=null}
				<label for="f_type_{$type.id}">
					<div>
						<h3>{$type.label}</h3>
						{if !empty($type.help)}
							<p>{$type.help}</p>
						{/if}
					</div>
				</label>
			</dd>
		{/foreach}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" label="Date" required=1 source=$transaction}
			{input type="text" name="label" label="Libellé" required=1 source=$transaction}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de note de frais, etc."}
		</dl>
		<dl data-types="all-but-advanced">
			{input type="money" name="amount" label="Montant" required=1 default=$amount}
		</dl>
	</fieldset>

	{foreach from=$types_details item="type"}
		<fieldset data-types="t{$type.id}">
			<legend>{$type.label}</legend>
			{if $type.id == $transaction::TYPE_ADVANCED}
				{* Saisie avancée *}
				{include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart}
			{else}
				<dl>
				{foreach from=$type.accounts key="key" item="account"}
					<?php $selected = $types_accounts[$key] ?? null; ?>
					{input type="list" target="acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1 default=$selected}
				{/foreach}
				</dl>
			{/if}
		</fieldset>
	{/foreach}

	<fieldset>
		<legend>Détails facultatifs</legend>
		<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." source=$transaction}
		</dl>
		<dl>
			{input type="list" multiple=true name="users" label="Membres associés" target="membres/selector.php" default=$linked_users}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30 source=$transaction}

			{input type="file" name="file" label="Ajouter un fichier joint"}
		</dl>
		<dl data-types="all-but-advanced">
			{if count($analytical_accounts) > 1}
				{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts}
			{/if}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_edit_%d"|args:$transaction.id}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{literal}
<script type="text/javascript" defer="defer" async="async">
g.script('scripts/accounting.js', () => { initTransactionForm(); });
</script>
{/literal}

{include file="admin/_foot.tpl"}

Modified src/templates/acc/transactions/new.tpl from [14b3e3ace9] to [8d26140458].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35
36
37
38
39
40
41


42

43


44

45

46
47
48
49
50

51
52

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

110
111
112
113

114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

141
142

143
144


145
146
147
148
149
150
151


152
153

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212







213
{include file="admin/_head.tpl" title="Saisie d'une opération" current="compta/saisie" js=1}

<form method="post" action="{$self_url}">
    <ul class="actions">
        <li><input type="radio" name="type" value="recette" {form_field name=type checked=recette default=recette} id="f_type_recette" /><label for="f_type_recette">Recette</label></li>
        <li><input type="radio" name="type" value="depense" {form_field name=type checked=depense} id="f_type_depense" /><label for="f_type_depense">Dépense</label></li>
        <li><input type="radio" name="type" value="virement" {form_field name=type checked=virement} id="f_type_virement" /><label for="f_type_virement">Virement interne</label></li>
        <li><input type="radio" name="type" value="avance" {form_field name=type checked=avance} id="f_type_avance" /><label for="f_type_avance">Saisie avancée</label></li>
    </ul>


    {form_errors}

    {if $ok}
        <p class="confirm">
            L'opération numéro <a href="{$admin_url}compta/operations/voir.php?id={$ok}">{$ok}</a> a été ajoutée.
            (<a href="{$admin_url}compta/operations/voir.php?id={$ok}">Voir l'opération</a>)
        </p>
    {/if}

    <fieldset>
        <legend>Informations sur l'opération</legend>
        <dl>
            <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="date" name="date" id="f_date" value="{form_field name=date default=$date}" size="10" required="required" /></dd>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
            <dt><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>

            <dd><input type="number" size="5" name="montant" id="f_montant" value="{form_field name=montant default=0.00}" min="0.00" step="0.01" required="required" /> {$config.monnaie}</dd>
            <dt><label for="f_numero_piece">Numéro de pièce comptable</label></dt>
            <dd><input type="text" name="numero_piece" id="f_numero_piece" value="{form_field name=numero_piece}" /></dd>
            <dt><label for="f_remarques">Remarques</label></dt>
            <dd><textarea name="remarques" id="f_remarques" rows="4" cols="30">{form_field name=remarques}</textarea></dd>
            {if count($projets) > 0}
            <dt><label for="f_projet">Projet</label></dt>
            <dd>
                <select name="projet" id="f_projet">
                    <option value="0">-- Aucun</option>
                    {foreach from=$projets key="id" item="libelle"}
                    <option value="{$id}"{form_field name="projet" selected=$id}>{$libelle}</option>
                    {/foreach}
                </select>


            </dd>

            {/if}


        </dl>

        <dl class="type_recette type_depense">

            <dt><label for="f_moyen_paiement">Moyen de paiement</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="moyen_paiement" id="f_moyen_paiement" required="required">
                {foreach from=$moyens_paiement item="moyen"}
                    <option value="{$moyen.code}"{if $moyen.code == $moyen_paiement} selected="selected"{/if}>{$moyen.nom}</option>

                {/foreach}
                </select>

            </dd>
            <dd class="f_a_encaisser">
                <input type="checkbox" name="a_encaisser" value="1" id="f_a_encaisser" {form_field name=a_encaisser checked="1"} />
                <label for="f_a_encaisser">En attente d'encaissement</label>
            </dd>
            <dt class="f_cheque"><label for="f_numero_cheque">Numéro de chèque</label></dt>
            <dd class="f_cheque"><input type="text" name="numero_cheque" id="f_numero_cheque" value="{form_field name=numero_cheque}" /></dd>
            <dt class="f_banque"><label for="f_banque">Compte bancaire</label></dt>
            <dd class="f_banque">
                <select name="banque" id="f_banque">
                {foreach from=$comptes_bancaires item="compte"}
                    <option value="{$compte.id}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle} - {$compte.banque}</option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>

    <fieldset class="type_avance">
        <legend>Saisie avancée</legend>
        <dl>
            <dt><label for="f_compte_debit">Compte débité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                {select_compte comptes=$comptes name="compte_debit"}
            </dd>
            <dt><label for="f_compte_credit">Compte crédité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                {select_compte comptes=$comptes name="compte_credit"}
            </dd>
        </dl>
    </fieldset>

    <fieldset class="type_virement">
        <legend>Virement</legend>
        <dl>
            <dt><label for="f_compte2">De</label></dt>
            <dd>
                <select name="compte2" id="f_compte2">
                    <option value="{$id_caisse}">Caisse</option>
                {foreach from=$comptes_bancaires item="compte"}
                    <option value="{$compte.id}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle} - {$compte.banque}</option>
                {/foreach}
                    <option value="{$compte_cheque_e_encaisser}">Chèques à encaisser</option>
                    <option value="{$compte_carte_e_encaisser}">Paiement CB à encaisser</option>
                </select>
            </dd>
            <dt><label for="f_compte1">Vers</label></dt>
            <dd>
                <select name="compte1" id="f_compte1">
                    <option value="{$id_caisse}">Caisse</option>
                {foreach from=$comptes_bancaires item="compte"}
                    <option value="{$compte.id}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle} - {$compte.banque}</option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>


    <fieldset class="type_dette">
        <legend>Dette</legend>
        <dl>

            <dt><label for="f_compte_usager">Type de dette</label></dt>
            <dd>
                <input type="radio" name="compte" id="f_compte_usager" value="4110" {form_field name=compte checked=4110 default=4110} />
                <label for="f_compte_usager">Dette envers un membre ou usager</label>
            </dd>
            <dd>
                <input type="radio" name="compte" id="f_compte_fournisseur" value="4010" {form_field name=compte checked=4010} />
                <label for="f_compte_fournisseur">Dette envers un fournisseur</label>
            </dd>
        </dl>
    </fieldset>

    <fieldset class="type_recette">
        <legend>Catégorie</legend>
        <dl class="catList">
        {foreach from=$categories_recettes item="cat"}
            <dt>
                <input type="radio" name="categorie_recette" value="{$cat.id}" id="f_cat_{$cat.id}" {form_field name="categorie" checked=$cat.id} />
                <label for="f_cat_{$cat.id}">{$cat.intitule}</label>
            </dt>
            {if !empty($cat.description)}
                <dd class="desc">{$cat.description}</dd>
            {/if}
        {/foreach}
        </dl>
    </fieldset>


    <fieldset class="type_depense type_dette">
        <legend>Catégorie</legend>

        <dl class="catList">
        {foreach from=$categories_depenses item="cat"}


            <dt>
                <input type="radio" name="categorie_depense" value="{$cat.id}" id="f_cat_{$cat.id}" {form_field name="categorie" checked=$cat.id} />
                <label for="f_cat_{$cat.id}">{$cat.intitule}</label>
            </dt>
            {if !empty($cat.description)}
                <dd class="desc">{$cat.description}</dd>
            {/if}


        {/foreach}
        </dl>

    </fieldset>


    <script type="text/javascript">
    {literal}
    (function () {

        function changeMoyenPaiement()
        {
            var elm = $('#f_moyen_paiement');
            g.toggle('.f_cheque', elm.value == 'CH');
            g.toggle('.f_banque', elm.value != 'ES');

            g.toggle('.f_a_encaisser', elm.value == 'CB' || elm.value == 'CH');

            cocherAEncaisser();
        }

        function changeTypeSaisie(type)
        {
            g.toggle(['.type_dette', '.type_recette', '.type_depense', '.type_avance', '.type_virement'], false);
            g.toggle('.type_' + type, true);
        }

        function cocherAEncaisser()



        {
            var elm = $('#f_a_encaisser');
            g.toggle('.f_banque', !elm.checked && $('#f_moyen_paiement').value != 'ES');
        }

        changeMoyenPaiement();
        changeTypeSaisie(document.forms[0].type.value);
        cocherAEncaisser();

        $('#f_moyen_paiement').onchange = changeMoyenPaiement;

        $('#f_a_encaisser').onchange = cocherAEncaisser;

        var inputs = $('input[name="type"]');

        for (var i = 0; i < inputs.length; i++)
        {
            inputs[i].onchange = function (e) {
                changeTypeSaisie(this.value);
            };
        }

        $('#f_date').focus();
    } ());
    {/literal}
    </script>

    <p class="submit">
        {csrf_field key="compta_saisie"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>








{include file="admin/_foot.tpl"}
|

<
|
<
<
<
<
<

>
|

|
|
|
|
|
|
<
<
<
<
<
<
<
<
|
>
|
<
|
|
|
|
<
|
<
<
<
<
<
|
>
>
|
>
|
>
>
|
>
|
>
|
|
<
|
|
>
|
|
>
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
|
|
>

|
|
|
>
|
<
|
<
|
|
|
<
<
|
|
<
<
<
<
<
<
|
<
<
<
<
<
|
<
<

>
|
|
>
|
<
>
>
|
<
<
<
<
<
<
>
>
|
|
>
|
|
|
<
<
<

|
<
<
<
<
|
<
|
<
<
|
<
<
<
<
<
|
<
>
>
>
|
<
<
<
|
<
<
<
|
<
|
<
|
<
|
<
|
<
<
<
<
|
<
<
<
|

|
|
|
|



>
>
>
>
>
>
>

1
2

3





4
5
6
7
8
9
10
11
12
13








14
15
16

17
18
19
20

21





22
23
24
25
26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42



















































43


44
45
46
47
48
49
50
51
52

53

54
55
56


57
58






59





60


61
62
63
64
65
66

67
68
69






70
71
72
73
74
75
76
77



78
79




80

81


82





83

84
85
86
87



88



89

90

91

92

93




94



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"}


{include file="acc/_year_select.tpl"}






<form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="1">
	{form_errors}

	{if $ok}
		<p class="block confirm">
			L'écriture numéro <a href="details.php?id={$ok}">{$ok}</a> a été ajoutée.
			(<a href="details.php?id={$ok}">Voir l'écriture</a>)
		</p>
	{/if}









	{if $payoff_for}
		<input type="hidden" name="type" value="{$transaction.type}" />

		<input type="hidden" name="payoff_for" value="{$payoff_for.id}" />
		<input type="hidden" name="{$payoff_for.form_account_name}[{$payoff_for.id_account}]" value="-" />
		<fieldset>
			<legend>{if $payoff_for.type == $transaction::TYPE_DEBT}Règlement de dette{else}Règlement de créance{/if}</legend>

			<dl>





				<dt>Écriture d'origine</dt>
				<dd><a class="num" href="{$admin_url}acc/transactions/details.php?id={$payoff_for.id}">#{$payoff_for.id}</a></dd>
				{input type="list" target="acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$payoff_targets,$chart_id name=$payoff_for.form_target_name label="Compte de règlement" required=1}
			</dl>
		</fieldset>
	{else}
		<fieldset>
			<legend>Type d'écriture</legend>
			<dl>
			{foreach from=$types_details item="type"}
				<dd class="radio-btn">
					{input type="radio" name="type" value=$type.id source=$transaction label=null}
					<label for="f_type_{$type.id}">
						<div>

							<h3>{$type.label}</h3>
							{if !empty($type.help)}
								<p>{$type.help}</p>
							{/if}
						</div>
					</label>
				</dd>



















































			{/foreach}


			</dl>
		</fieldset>
	{/if}

	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" default=$date label="Date" required=1 source=$transaction}
			{input type="text" name="label" label="Libellé" required=1 source=$transaction}

			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de note de frais, etc."}

		</dl>
		<dl data-types="all-but-advanced">
			{input type="money" name="amount" label="Montant" required=1 default=$amount}


		</dl>
	</fieldset>












	{if !$payoff_for}



		{foreach from=$types_details item="type"}
			<fieldset data-types="t{$type.id}">
				<legend>{$type.label}</legend>
				{if $type.id == $transaction::TYPE_ADVANCED}
					{* Saisie avancée *}

					{include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart}
				{else}
					<dl>






					{foreach from=$type.accounts key="key" item="account"}
						{input type="list" target="acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1}
					{/foreach}
					</dl>
				{/if}
			</fieldset>
		{/foreach}
	{/if}




	<fieldset>




		<legend>Détails facultatifs</legend>

		<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">


			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." source=$transaction}





		</dl>

		<dl>
			{input type="list" multiple=true name="users" label="Membres associés" target="membres/selector.php"}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}




			{input type="file" name="file" label="Fichier joint"}



		</dl>

		<dl data-types="all-but-advanced">

			{if count($analytical_accounts) > 1}

				{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts}

			{/if}




		</dl>



	</fieldset>

	<p class="submit">
		{csrf_field key="acc_transaction_new"}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript" defer="defer" async="async">
let is_new = {if $payoff_for}false{else}true{/if};
{literal}
g.script('scripts/accounting.js', () => { initTransactionForm(is_new && !$('.block').length); });
</script>
{/literal}

{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/service_user.tpl version [a8761edf6b].

































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{include file="admin/_head.tpl" title="Écritures liées à une inscription" current="acc/accounts"}

<p>
	{linkbutton href="!membres/fiche.php?id=%d"|args:$user_id label="Retour à la fiche membre" shape="user"}
</p>

{include file="acc/reports/_journal.tpl"}

<h2 class="ruler">Solde des comptes</h2>

<table class="list">
	<thead>
		<tr>
			<td>Numéro</td>
			<th>Compte</th>
			<td class="money">Solde débiteur</td>
			<td class="money">Solde créditeur</td>
		</tr>
	</thead>
	<tbody>
	{foreach from=$balance item="account"}
		<tr>
			<td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td>
			<th>{$account.label}</th>
			<td class="money">{if $account.sum < 0}{$account.sum|raw|html_money}{/if}</td>
			<td class="money">{if $account.sum > 0}{$account.sum|raw|html_money}{/if}</td>
		</tr>
	{/foreach}
	</tbody>
</table>

{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/user.tpl version [fca819aadf].































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{include file="admin/_head.tpl" title="Écritures liées à %s"|args:$transaction_user.identite current="acc/accounts"}

<p>
	{linkbutton href="!membres/fiche.php?id=%d"|args:$transaction_user.id label="Retour à la fiche membre" shape="user"}
</p>

{include file="acc/reports/_journal.tpl"}

<h2 class="ruler">Solde des comptes</h2>

<form method="get" action="{$self_url_no_qs}">
	<fieldset>
		<legend>Exercice</legend>
		<dl>
			{input type="select" name="year" options=$years onchange="this.form.submit();" default=$year}
		</dl>
		<input type="hidden" name="id" value="{$transaction_user.id}" />
		<noscript>
			<input type="submit" value="OK" />
		</noscript>
	</fieldset>
</form>

<p class="block help">Cette liste représente le solde des comptes uniquement pour les écritures liées à ce membre.</p>

<table class="list">
	<thead>
		<tr>
			<td>Numéro</td>
			<th>Compte</th>
			<td class="money">Solde débiteur</td>
			<td class="money">Solde créditeur</td>
		</tr>
	</thead>
	<tbody>
	{foreach from=$balance item="account"}
		<tr>
			<td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td>
			<th>{$account.label}</th>
			<td class="money">{if $account.sum < 0}{$account.sum|raw|html_money}{/if}</td>
			<td class="money">{if $account.sum > 0}{$account.sum|raw|html_money}{/if}</td>
		</tr>
	{/foreach}
	</tbody>
</table>

{include file="admin/_foot.tpl"}

Added src/templates/acc/years/balance.tpl version [6a308c5420].









































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
{include file="admin/_head.tpl" title="Balance d'ouverture" current="acc/years"}

{form_errors}

{if $year->countTransactions()}
<p class="block alert">
	<strong>Attention&nbsp;!</strong>
	Cet exercice a déjà des écritures, peut-être avez-vous déjà renseigné la balance d'ouverture&nbsp;?
</p>
{/if}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Exercice&nbsp;: «&nbsp;{$year.label}&nbsp;» du {$year.start_date|date_short} au {$year.end_date|date_short}</legend>

		{if null === $previous_year}
		<dl>
			<dt><label for="f_from_year">Reprendre les soldes de fermeture d'un exercice clôturé</label></dt>
			<dd>
				<select id="f_from_year" name="from_year">
					<option value="">-- Aucun</option>
					{foreach from=$years item="year"}
					<option value="{$year.id}">{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</option>
					{/foreach}
				</select>
			</dd>
		</dl>
		{else}
		<p class="help">
			Renseigner ici les soldes d'ouverture (débiteur ou créditeur) des comptes.
		</p>
		<table class="list transaction-lines">
			<thead>
				<tr>
					{if $chart_change}
						<td>Ancien compte</td>
						<th>Nouveau compte</th>
					{else}
						<th>Compte</th>
					{/if}
					<td>Débit</td>
					<td>Crédit</td>
					<td></td>
				</tr>
			</thead>
			<tbody>
			{foreach from=$lines key="k" item="line"}
				<tr>
					{if $chart_change}
						<td>{$line.code} — {$line.label}</td>
					{/if}
					<th>
						{input type="list" target="acc/charts/accounts/selector.php?chart=%d"|args:$year.id_chart name="lines[account][]" default=$line.account_selected}
					</th>
					<td>{input type="money" name="lines[debit][]" default=$line.debit size=5}</td>
					<td>{input type="money" name="lines[credit][]" default=$line.credit size=5}</td>
					<td>{button label="Enlever la ligne" shape="minus" min="1" name="remove_line"}</td>
				</tr>
			{/foreach}
			</tbody>
			<tfoot>
				<tr>
					<th>Total</th>
					{if $chart_change}
						<td></td>
					{/if}
					<td>{input type="money" name="debit_total" readonly="readonly" tabindex="-1" }</td>
					<td>{input type="money" name="credit_total" readonly="readonly" tabindex="-1" }</td>
					<td>{button label="Ajouter une ligne" shape="plus"}</td>
				</tr>
			</tfoot>
		</table>
		{/if}
	</fieldset>

	<p class="submit">
		{if null === $previous_year}
			{button type="submit" name="next" label="Continuer" shape="right" class="main"}
			- ou -
			{linkbutton shape="reset" href="!acc/years/" label="Passer cet étape"} <i class="help">(Il sera toujours possible de reprendre la balance d'ouverture plus tard.)</i>
		{else}
			{csrf_field key="acc_years_balance_%s"|args:$year.id}
			{if $previous_year}
			<input type="hidden" name="from_year" value="{$previous_year.id}" />
			{/if}
			{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}

			{literal}
			<script type="text/javascript" defer="defer" async="async">
			g.script('scripts/accounting.js', () => { initTransactionForm(); });
			</script>
			{/literal}
		{/if}
	</p>

</form>


{include file="admin/_foot.tpl"}

Modified src/templates/acc/years/close.tpl from [6f4abfc5df] to [a98ff863c3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
38
{include file="admin/_head.tpl" title="Clôturer un exercice" current="compta/exercices" js=1}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Clôturer un exercice</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir clôturer l'exercice «&nbsp;{$exercice.libelle}&nbsp;» ?
        </h3>
        <p class="warning">
            Un exercice clôturé ne peut plus être rouvert ou modifié&nbsp;!<br />
            Il ne sera plus possible de modifier ou supprimer les écritures de l'exercice clôturé.
        </p>
        <dl>
            <dt>Début de l'exercice</dt>
            <dd>{$exercice.debut|date_fr:'d/m/Y'}</dd>
            <dt><label for="f_fin">Fin de l'exercice</label></dt>
            <dd class="help">Si des opérations existent après cette date, elles seront automatiquement
                attribuées à un nouvel exercice.</dd>
            <dd><input type="date" name="fin" id="f_fin" value="{form_field name=fin default=$exercice.fin|date_fr:'Y-m-d'}" size="10" /></dd>
            <dt>
                <input type="checkbox" name="reports" {form_field name=reports default="1" checked=true} id="f_reports" /> <label for="f_reports">Exécuter automatiquement les reports à nouveau</label>
            </dt>
            <dd class="help">Les soldes créditeurs et débiteurs de chaque compte seront reportés 
                automatiquement dans le nouvel exercice. Si vous ne cochez pas la case, vous devrez faire les reports à nouveau vous-même.</dd>
        </h3>
    </fieldset>



    <p class="submit">
        {csrf_field key="compta_cloturer_exercice_%s"|args:$exercice.id}
        <input type="submit" name="close" value="Clôturer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|





|
|
|
|
|
|
|
|
|
|
|
|
|
<
<
<
<
<
|
<
<
|
|

>
>
|
|
|
|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19





20


21
22
23
24
25
26
27
28
29
30
31
32
33
{include file="admin/_head.tpl" title="Clôturer un exercice" current="acc/years"}

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Clôturer un exercice</legend>
		<h3 class="warning">
			Êtes-vous sûr de vouloir clôturer l'exercice «&nbsp;{$year.label}&nbsp;» ?
		</h3>
		<p class="block alert">
			Un exercice clôturé ne peut plus être rouvert ou modifié&nbsp;!<br />
			Il ne sera plus possible de modifier ou supprimer les écritures de l'exercice clôturé.
		</p>
		<dl>
			<dt>Début de l'exercice</dt>
			<dd>{$year.start_date|date_short}</dd>
			<dt>Fin de l'exercice</dt>





			<dd>{$year.end_date|date_short}</dd>


		</h3>
	</fieldset>

	<p class="help">Les soldes créditeurs ou débiteurs de chaque compte pourront être reportés automatiquement lors de l'ouverture de l'exercice suivant.</p>

	<p class="submit">
		{csrf_field key="acc_years_close_%d"|args:$year.id}
		{button type="submit" name="close" label="Clôturer" shape="lock" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/years/delete.tpl from [604797e2eb] to [d401db76d9].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Supprimer un exercice" current="compta/exercices"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer un exercice</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer l'exercice «&nbsp;{$exercice.libelle}&nbsp;»
            du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'} ?
        </h3>
        <p class="help">
            Attention, l'exercice ne pourra pas être supprimé si des opérations y sont
            toujours affectées.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_supprimer_exercice_%s"|args:$exercice.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|

<
|
<
|
<
<
<
|
<
<
<
|
<
<
<
|
<
|
<
<
|
<


1
2

3

4



5



6



7

8


9

10
11
{include file="admin/_head.tpl" title="Supprimer un exercice" current="acc/years"}


{include file="common/delete_form.tpl"

	legend="Supprimer cet exercice ?"



	warning="Êtes-vous sûr de vouloir supprimer l'exercice « %s » et ses %d écritures ?"|args:$year.label,$nb_transactions



	alert="Attention, il ne sera pas possible de récupérer les écritures supprimées."



	confirm="Cocher cette case pour confirmer la suppression de cet exercice et des %d écritures liées."|args:$nb_transactions

	csrf_key="acc_years_delete_%s"|args:$year.id


}


{include file="admin/_foot.tpl"}

Modified src/templates/acc/years/edit.tpl from [ea0aa3d194] to [6825ad64b8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Modifier un exercice" current="compta/exercices" js=1}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Modifier un exercice</legend>
        <dl>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$exercice}" required="required" /></dd>
            <dt><label for="f_debut">Début de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="date" name="debut" id="f_debut" value="{form_field name=debut default=$exercice.debut|date_fr:'Y-m-d'}" size="10" required="required" /></dd>
            <dt><label for="f_fin">Fin de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="date" name="fin" id="f_fin" value="{form_field name=fin default=$exercice.fin|date_fr:'Y-m-d'}" size="10" required="required" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_modif_exercice_%s"|args:$exercice.id}
        <input type="submit" name="edit" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|



|

|
|
|
<
|
|
<
|
<
|
|

|
|
|
|




1
2
3
4
5
6
7
8
9

10
11

12

13
14
15
16
17
18
19
20
21
22
23
{include file="admin/_head.tpl" title="Modifier un exercice" current="acc/years"}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Modifier un exercice</legend>
		<dl>

			{input type="text" label="Libellé" name="label" source=$year required=true}
			{input type="date" label="Début de l'exercice" name="start_date" source=$year required=true}

			{input type="date" label="Fin de l'exercice" name="end_date" source=$year required=true}

		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_years_edit_%s"|args:$year.id}
		{button type="submit" name="edit" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/years/import.tpl version [83c69987e2].

















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{include file="admin/_head.tpl" title="Importer des écritures" current="acc/years"}

<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/years/import.php?id={$year.id}">Import</a></li>
		<li><a href="{$admin_url}acc/years/import.php?id={$year.id}&amp;export=csv">Export journal général - CSV</a></li>
		<li><a href="{$admin_url}acc/years/import.php?id={$year.id}&amp;export=ods">Export journal général - tableur</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

{if $csv->loaded()}

		{include file="common/_csv_match_columns.tpl"}

		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="cancel" value="1" label="Annuler" shape="left"}
			{button type="submit" name="assign" label="Continuer" class="main" shape="right"}
		</p>

{else}

	<fieldset>
		<legend>Import d'écritures</legend>
		<dl>
			<dt><label for="f_type_garradin">Format de fichier</label></dt>
			{input type="radio" name="type" value="garradin" label="Journal général au format CSV Garradin" default="garradin"}
			{input type="radio" name="type" value="csv" label="Journal au format CSV libre"}
			{include file="common/_csv_help.tpl"}
			{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			<dd class="help block">
				- Les lignes comportant un numéro d'écriture mettront à jour les écritures existantes correspondant à ces numéros (sauf si celles-ci ont été validées), alors que les lignes sans numéro créeront de nouvelles écritures.<br />
				- Si le fichier comporte des écritures dont la date est en dehors de l'exercice courant, elles seront ignorées.
			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="load" label="Importer" shape="upload" class="main"}
	</p>

{/if}

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/acc/years/index.tpl from [ec3f086414] to [1a1526a67f].

1
2
3

4

5


6

7
8
9


10

11
12
13
14

15
16
17
18
19
20








21



22
23



24
25
26

27
28

29
30
31
32


33


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{include file="admin/_head.tpl" title="Exercices" current="compta/exercices"}

<ul class="actions">

    <li class="current"><a href="{$admin_url}compta/exercices/">Exercices</a></li>

    <li><a href="{$admin_url}compta/projets/">Projets (compta analytique)</a></li>


</ul>


{if !$current_exercice}
<ul class="actions">


    <li><strong><a href="{$admin_url}compta/exercices/ajouter.php">Commencer un nouvel exercice</a></strong></li>

</ul>
{/if}

{if !empty($liste)}

    <dl class="catList">
    {foreach from=$liste item="exercice"}
        <dt>{$exercice.libelle}</dt>
        <dd class="desc">
            {if $exercice.cloture}Clôturé{else}En cours{/if}
            | Du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}








        </dd>



        <dd class="compte">
            <strong>{$exercice.nb_operations}</strong> opérations enregistrées.



        </dd>
        <dd class="desc">
            <a href="{$admin_url}compta/rapports/journal.php?exercice={$exercice.id}">Journal général</a>

            | <a href="{$admin_url}compta/rapports/grand_livre.php?exercice={$exercice.id}">Grand livre</a>
            | <a href="{$admin_url}compta/rapports/compte_resultat.php?exercice={$exercice.id}">Compte de résultat</a>

            | <a href="{$admin_url}compta/rapports/bilan.php?exercice={$exercice.id}">Bilan</a>
        </dd>
        {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
        <dd class="actions">


            {if !$exercice.cloture}


            <a class="icn" href="{$admin_url}compta/exercices/modifier.php?id={$exercice.id}" title="Modifier">✎</a>
            <a class="icn" href="{$admin_url}compta/exercices/supprimer.php?id={$exercice.id}" title="Supprimer">✘</a>
            <a class="icn" href="{$admin_url}compta/exercices/cloturer.php?id={$exercice.id}" title="Clôturer cet exercice">🔒</a>
            {elseif $exercice.cloture && $exercice.nb_operations == 0}
            <a class="icn" href="{$admin_url}compta/exercices/supprimer.php?id={$exercice.id}" title="Supprimer">✘</a>
            {/if}
        </dd>
        {/if}
    {/foreach}
    </dl>
{else}
    <p class="alert">
        Il n'y a pas d'exercice en cours.
    </p>
{/if}

{include file="admin/_foot.tpl"}
|

|
>
|
>
|
>
>
|
>

|
|
>
>
|
>
|


|
>
|
<
<
|
|
<
>
>
>
>
>
>
>
>
|
>
>
>
|
<
>
>
>
|
|
|
>
|
|
>
|
|
|
|
>
>
|
>
>
|
<
|
<
|
|
|
|
|
|

|
|
|



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


25
26

27
28
29
30
31
32
33
34
35
36
37
38
39

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

60

61
62
63
64
65
66
67
68
69
70
71
72
73
{include file="admin/_head.tpl" title="Exercices" current="acc/years"}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$self_url}">Exercices</a></li>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		<li><a href="{$admin_url}acc/years/new.php">Nouvel exercice</a></li>
		{/if}
		<li><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li>
	</ul>
</nav>

{if $_GET.msg == 'OPEN'}
<p class="block error">
	Il n'existe aucun exercice ouvert.
	{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures.
	{/if}
</p>
{/if}

{if !empty($list)}
	{if count($list) > 1}
	<section class="year-infos">


		<section class="graphs">
			<figure>

				<img src="{$admin_url}acc/reports/graph_plot_all.php?type=assets" alt="" />
			</figure>
			<figure>
				<img src="{$admin_url}acc/reports/graph_plot_all.php?type=result" alt="" />
			</figure>
		</section>
	</section>
	{/if}

	<dl class="list">
	{foreach from=$list item="year"}
		<dt>{$year.label}</dt>
		<dd class="desc">

			{if $year.closed}Clôturé{else}En cours{/if}
			| Du {$year.start_date|date_short} au {$year.end_date|date_short}
			| <a href="../charts/accounts/?id={$year.id_chart}">Plan comptable</a>
		</dd>
		<dd class="desc">
			<a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a>
			| <a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a>
			| <a href="{$admin_url}acc/reports/journal.php?year={$year.id}">Journal général</a>
			| <a href="{$admin_url}acc/reports/ledger.php?year={$year.id}">Grand livre</a>
			| <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a>
			| <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a>
		</dd>
		{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
		<dd class="actions">
			{linkbutton label="Export CSV" shape="export" href="import.php?id=%d&export=csv"|args:$year.id}
			{linkbutton label="Export tableur" shape="export" href="import.php?id=%d&export=ods"|args:$year.id}
			{if !$year.closed}
				{linkbutton label="Import" shape="upload" href="import.php?id=%d"|args:$year.id}
				{linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id}
				{linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id}

				{linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id}

				{linkbutton label="Supprimer" shape="delete" href="delete.php?id=%d"|args:$year.id}
			{/if}
		</dd>
		{/if}
	{/foreach}
	</dl>
{else}
	<p class="block alert">
		Il n'y a pas d'exercice en cours.
	</p>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/acc/years/new.tpl from [c84bd45486] to [847d4ab122].

1
2







3
4
5
6
7
8
9
10




11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Commencer un exercice" current="compta/exercices" js=1}








{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Commencer un nouvel exercice</legend>
        <dl>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>




            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
            <dt><label for="f_debut">Début de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="date" name="debut" id="f_debut" value="{form_field name=debut}" size="10" required="required" /></dd>
            <dt><label for="f_fin">Fin de l'exercice</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="date" name="fin" id="f_fin" value="{form_field name=fin}" size="10" required="required" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_ajout_exercice"}
        <input type="submit" name="add" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|

>
>
>
>
>
>
>


|

|
|
|
<
>
>
>
>
|
|
|
<
<
|
|

|
|
|
|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23


24
25
26
27
28
29
30
31
32
33
34
{include file="admin/_head.tpl" title="Commencer un exercice" current="acc/years"}

<nav class="tabs">
	<ul>
		<li><a href="./">Exercices</a></li>
		<li class="current"><a href="new.php">Nouvel exercice</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Commencer un nouvel exercice</legend>
		<dl>

			{input type="select_groups" options=$charts name="id_chart" label="Plan comptable" required=true}
			<dd class="help">Attention, il ne sera pas possible de modifier ou supprimer un compte du plan comptable si le compte est utilisé dans un exercice clôturé.<br />
				Si vous souhaitez modifier le plan comptable pour ce nouvel exercice, il est recommandé de créer un nouveau plan comptable, recopié à partir de l'ancien plan comptable. Ainsi tous les comptes seront modifiables et supprimables.</dd>
			<dd class="help">{linkbutton shape="settings" label="Gestion des plans comptables" href="!acc/charts/"}</dd>
			{input type="text" name="label" label="Libellé" required=true}
			{input type="date" label="Début de l'exercice" name="start_date" required=true default=$start_date}
			{input type="date" label="Fin de l'exercice" name="end_date" required=true default=$end_date}


		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_years_new"}
		{button type="submit" name="new" label="Créer ce nouvel exercice" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/acc/years/select.tpl version [c4985ce24e].





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Changer d'exercice" current="acc/years"}

<form method="post" action="{$self_url}" data-focus="1">
	<fieldset>
		<legend>Changer l'exercice de travail</legend>
		<dl>
			<dd>
				<select name="year">
					{foreach from=$list item="year"}
					<option value="{$year.id}">{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</option>
					{/foreach}
				</select>
			</dd>
			<dd class="help">Ici ne peuvent être sélectionnés que les exercices ouverts, car il n'est pas possible de modifier un exercice clos.
				Pour consulter les rapports pour les exercices clos, voir <a href="{$www_url}admin/acc/years/">la liste des exercices</a>.</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_select_year"}
		<input type="hidden" name="from" value="{$from}" />
		{button type="submit" name="change" label="Changer" shape="right" class="main"}
	</p>
</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/_head.tpl from [ab1e909a41] to [d27c715ba4].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
<head>
    <meta charset="utf-8" />
    <title>{$title}</title>
    <link rel="icon" type="image/png" href="{$admin_url}static/icon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" />
    {if isset($js) || isset($custom_js)}
        <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script>
    {/if}
    {if isset($custom_js)}
        {foreach from=$custom_js item="js"}
            <script type="text/javascript" src="{$admin_url}static/scripts/{$js}?{$version_hash}"></script>
        {/foreach}
    {/if}
    {if isset($custom_css)}
        {foreach from=$custom_css item="css"}








<
|
<







1
2
3
4
5
6
7
8

9

10
11
12
13
14
15
16
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
<head>
    <meta charset="utf-8" />
    <title>{$title}</title>
    <link rel="icon" type="image/png" href="{$admin_url}static/icon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" />

    <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script>

    {if isset($custom_js)}
        {foreach from=$custom_js item="js"}
            <script type="text/javascript" src="{$admin_url}static/scripts/{$js}?{$version_hash}"></script>
        {/foreach}
    {/if}
    {if isset($custom_css)}
        {foreach from=$custom_css item="css"}
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

105
106
107
108
109
110
111
112
113
114
            {/if}
        </li>
        {if $session->canAccess('membres', Membres::DROIT_ACCES)}
            <li class="member list{if $current == 'membres'} current{elseif $current_parent == 'membres'} current_parent{/if}"><a href="{$admin_url}membres/"><b class="icn">👪</b><i> Membres</i></a>
            {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
            <ul>
                <li class="member new{if $current == 'membres/ajouter'} current{/if}"><a href="{$admin_url}membres/ajouter.php">Ajouter</a></li>
                <li class="member cotisations{if $current == 'membres/cotisations'} current{/if}"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
                <li class="member message{if $current == 'membres/message'} current{/if}"><a href="{$admin_url}membres/message_collectif.php">Message collectif</a></li>
            </ul>
            {/if}
            </li>
        {/if}
        {if $session->canAccess('compta', Membres::DROIT_ACCES)}
            <li class="compta{if $current == 'compta'} current{elseif $current_parent == 'compta'} current_parent{/if}"><a href="{$admin_url}compta/"><b>€</b><i> Comptabilité</i></a>
            <ul>
            {if $session->canAccess('compta', Membres::DROIT_ECRITURE)}
                <li class="compta new{if $current == 'compta/saisie'} current{/if}"><a href="{$admin_url}compta/operations/saisir.php">Saisie</a></li>
            {/if}
                <li class="compta list{if $current == 'compta/gestion'} current{/if}"><a href="{$admin_url}compta/operations/">Suivi des opérations</a></li>

                <li class="compta banks{if $current == 'compta/banques'} current{/if}"><a href="{$admin_url}compta/banques/">Banques &amp; caisse</a></li>
            {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
                <li class="compta admin config{if $current == 'compta/categories'} current{/if}"><a href="{$admin_url}compta/categories/">Catégories &amp; comptes</a></li>
            {/if}
                <li class="compta admin reports{if $current == 'compta/exercices'} current{/if}"><a href="{$admin_url}compta/exercices/">Exercices &amp; projets</a></li>
            </ul>
            </li>
        {/if}
        {if $session->canAccess('wiki', Membres::DROIT_ACCES)}
            <li class="wiki{if $current == 'wiki'} current{elseif $current_parent == 'wiki'} current_parent{/if}"><a href="{$admin_url}wiki/"><b class="icn">✎</b><i> Wiki</i></a>
            <ul>
                <li class="wiki list{if $current == 'wiki/recent'} current{/if}"><a href="{$admin_url}wiki/recent.php">Dernières modifications</a>
                <li class="wiki search{if $current == 'wiki/chercher'} current{/if}"><a href="{$admin_url}wiki/chercher.php">Recherche</a>
                {*<li class="wiki follow{if $current == 'wiki/suivi'} current{/if}"><a href="{$admin_url}wiki/suivi.php">Mes pages suivies</a>*}
                {*<li class="wiki follow{if $current == 'wiki/contribution'} current{/if}"><a href="{$admin_url}wiki/contributions.php">Mes contributions</a>*}
            </ul>
            </li>
        {/if}
        {if $session->canAccess('config', Membres::DROIT_ADMIN)}
            <li class="main config{if $current == 'config'} current{elseif $current_parent == 'config'} current_parent{/if}"><a href="{$admin_url}config/"><b class="icn">☸</b><i> Configuration</i></a>
        {/if}

        <li class="my config{if $current == 'mes_infos'} current{elseif $current_parent == 'mes_infos'} current_parent{/if}"><a href="{$admin_url}mes_infos.php"><b class="icn">👤</b><i> Mes infos personnelles</i></a>
            <ul>
                <li class="my cotisations{if $current == 'mes_cotisations'} current{/if}"><a href="{$admin_url}mes_cotisations.php">Mes cotisations</a></li>
            </ul>
        </li>
        {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN}
        <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b><i> Déconnexion</i></a></li>
        {/if}
    {/if}
    </ul>







|






|


|

|
>
|
|
|

<








<
<






>
|

|







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

87
88
89
90
91
92
93
94


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
            {/if}
        </li>
        {if $session->canAccess('membres', Membres::DROIT_ACCES)}
            <li class="member list{if $current == 'membres'} current{elseif $current_parent == 'membres'} current_parent{/if}"><a href="{$admin_url}membres/"><b class="icn">👪</b><i> Membres</i></a>
            {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
            <ul>
                <li class="member new{if $current == 'membres/ajouter'} current{/if}"><a href="{$admin_url}membres/ajouter.php">Ajouter</a></li>
                <li class="{if $current == 'membres/services'} current{/if}"><a href="{$admin_url}services/">Activités &amp; cotisations</a></li>
                <li class="member message{if $current == 'membres/message'} current{/if}"><a href="{$admin_url}membres/message_collectif.php">Message collectif</a></li>
            </ul>
            {/if}
            </li>
        {/if}
        {if $session->canAccess('compta', Membres::DROIT_ACCES)}
            <li class="{if $current == 'acc'} current{elseif $current_parent == 'acc'} current_parent{/if}"><a href="{$admin_url}acc/"><b>€</b><i> Comptabilité</i></a>
            <ul>
            {if $session->canAccess('compta', Membres::DROIT_ECRITURE)}
                <li class="{if $current == 'acc/new'} current{/if}"><a href="{$admin_url}acc/transactions/new.php">Saisie</a></li>
            {/if}
                <li class="{if $current == 'acc/accounts'} current{/if}"><a href="{$admin_url}acc/accounts/">Comptes</a></li>
                <li class="{if $current == 'acc/simple'} current{/if}"><a href="{$admin_url}acc/accounts/simple.php">Suivi des écritures</a></li>
                <li class="{if $current == 'acc/years'} current{/if}"><a href="{$admin_url}acc/years/">Exercices &amp; rapports</a></li>
            {if $session->canAccess('compta', Membres::DROIT_ECRITURE)}
                <li class="{if $current == 'acc/charts'} current{/if}"><a href="{$admin_url}acc/charts/">Plans comptables</a></li>
            {/if}

            </ul>
            </li>
        {/if}
        {if $session->canAccess('wiki', Membres::DROIT_ACCES)}
            <li class="wiki{if $current == 'wiki'} current{elseif $current_parent == 'wiki'} current_parent{/if}"><a href="{$admin_url}wiki/"><b class="icn">✎</b><i> Wiki</i></a>
            <ul>
                <li class="wiki list{if $current == 'wiki/recent'} current{/if}"><a href="{$admin_url}wiki/recent.php">Dernières modifications</a>
                <li class="wiki search{if $current == 'wiki/chercher'} current{/if}"><a href="{$admin_url}wiki/chercher.php">Recherche</a>


            </ul>
            </li>
        {/if}
        {if $session->canAccess('config', Membres::DROIT_ADMIN)}
            <li class="main config{if $current == 'config'} current{elseif $current_parent == 'config'} current_parent{/if}"><a href="{$admin_url}config/"><b class="icn">☸</b><i> Configuration</i></a>
        {/if}
        <li class="{if $current == 'mes_infos'} current{elseif $current_parent == 'mes_infos'} current_parent{/if}">
            <a href="{$admin_url}mes_infos.php"><b class="icn">👤</b><i> Mes infos personnelles</i></a>
            <ul>
                <li{if $current == 'my_services'}  class="current"{/if}><a href="{$admin_url}my_services.php">Mes activités &amp; cotisations</a></li>
            </ul>
        </li>
        {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN}
        <li class="logout"><a href="{$admin_url}logout.php"><b class="icn">⤝</b><i> Déconnexion</i></a></li>
        {/if}
    {/if}
    </ul>

Deleted src/templates/admin/compta/banques/index.tpl version [dfa7e103a3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{include file="admin/_head.tpl" title="Comptes bancaires" current="compta/banques"}

<ul class="actions">
    <li class="current"><a href="{$admin_url}compta/banques/">Comptes bancaires</a></li>
    <li><a href="{$admin_url}compta/comptes/journal.php?id={$id_caisse}&amp;suivi">Journal de caisse</a></li>
    <li><a href="{$admin_url}compta/comptes/journal.php?id={$id_cheque_a_encaisser}&amp;suivi">Chèques à encaisser</a></li>
    <li><a href="{$admin_url}compta/comptes/journal.php?id={$id_carte_a_encaisser}&amp;suivi">Paiements par carte à encaisser</a></li>
</ul>

{if !empty($liste)}
    <table class="list">
        <thead>
            <tr>
                <td>Banque</td>
                <th>Libellé</th>
                <td>Solde</td>
                <td>IBAN</td>
                <td>BIC</td>
                <td></td>
            </tr>
        </thead>
        <tbody>
        {foreach from=$liste item="compte"}
            <tr>
                <td>{$compte.banque}</td>
                <th>{$compte.libelle}</th>
                <td><strong>{$compte.solde|escape|html_money} {$config.monnaie}</strong></td>
                <td>{$compte.iban|escape|format_iban}</td>
                <td>{$compte.bic}</td>
                <td class="actions">
                    <a class="icn" href="{$admin_url}compta/comptes/journal.php?id={$compte.id}&amp;suivi" title="Journal">𝍢</a>
                    {if $session->canAccess('compta', Membres::DROIT_ECRITURE)}
                        <a class="icn" href="{$admin_url}compta/banques/rapprocher.php?id={$compte.id}" title="Rapprocher">☑</a>
                    {/if}
                    {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
                        <a class="icn" href="{$admin_url}compta/banques/modifier.php?id={$compte.id}" title="Modifier">✎</a>
                        <a class="icn" href="{$admin_url}compta/banques/supprimer.php?id={$compte.id}" title="Supprimer">✘</a>
                    {/if}
                </td>
            </tr>
        {/foreach}
        </tbody>
    </table>
    </dl>
{/if}

{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
    {form_errors}

    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Ajouter un compte bancaire</legend>
            <dl>
                <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
                <dt><label for="f_banque">Nom de la banque</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="text" name="banque" id="f_banque" value="{form_field name=banque}" required="required" /></dd>
                <dt><label for="f_solde">Solde initial</label></dt>
                <dd><input type="number" size="5" name="solde" id="f_solde" value="{form_field name=solde default=0.00}" step="0.01" /> {$config.monnaie}</dd>
                <dt><label for="f_iban">Numéro IBAN</label></dt>
                <dd><input type="text" size="30" name="iban" id="f_iban" value="{form_field name=iban}" /></dd>
                <dt><label for="f_bic">Code BIC/SWIFT de la banque</label></dt>
                <dd><input type="text" size="10" name="bic" id="f_bic" value="{form_field name=bic}" /></dd>
            </dl>
        </fieldset>

        <p class="submit">
            {csrf_field key="compta_ajout_banque"}
            <input type="submit" name="add" value="Enregistrer &rarr;" />
        </p>

    </form>
{/if}
{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































Deleted src/templates/admin/compta/banques/modifier.tpl version [f69ac09d72].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{include file="admin/_head.tpl" title="Modifier un compte" current="compta/banques"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Modifier un compte bancaire</legend>
        <dl>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$compte}" required="required" /></dd>
            <dt><label for="f_banque">Nom de la banque</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="banque" id="f_banque" value="{form_field name=banque data=$compte}" required="required" /></dd>
            <dt><label for="f_iban">Numéro IBAN</label></dt>
            <dd><input type="text" size="30" name="iban" id="f_iban" value="{form_field name=iban data=$compte}" /></dd>
            <dt><label for="f_bic">Code BIC/SWIFT de la banque</label></dt>
            <dd><input type="text" size="10" name="bic" id="f_bic" value="{form_field name=bic data=$compte}" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_edit_banque_%s"|args:$compte.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































Deleted src/templates/admin/compta/banques/supprimer.tpl version [f2f4e5378b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{include file="admin/_head.tpl" title="Supprimer un compte" current="compta/banques"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer le compte ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer le compte «&nbsp;{$compte.id} - {$compte.libelle}&nbsp;» ?
        </h3>
        <p class="help">
            Attention, le compte ne pourra pas être supprimé si des opérations y sont
            affectées.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_delete_banque_%s"|args:$compte.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































Deleted src/templates/admin/compta/categories/_nav.tpl version [b71d1d8635].

1
2
3
4
5
6
<ul class="actions">
    <li{if $current == "recettes"} class="current"{/if}><a href="{$admin_url}compta/categories/?recettes">Recettes</a></li>
    <li{if $current == "depenses"} class="current"{/if}><a href="{$admin_url}compta/categories/?depenses">Dépenses</a></li>
    <li{if $current == "ajouter"} class="current"{/if}><strong><a href="{$admin_url}compta/categories/ajouter.php">Ajouter une catégorie</a></strong></li>
    <li{if $current == "plan"} class="current"{/if}><em><a href="{$admin_url}compta/comptes/">Plan comptable</a></em></li>
</ul>
<
<
<
<
<
<












Deleted src/templates/admin/compta/categories/ajouter.tpl version [f30b2371c1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{include file="admin/_head.tpl" title="Ajouter une catégorie" current="compta/categories"}

{include file="admin/compta/categories/_nav.tpl" current="ajouter"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Ajouter une catégorie</legend>
        <dl>
            <dt><label for="f_type">Type</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="type" id="f_type" required="required">
                    <option value="{$categories::RECETTES}"{if $type == $categories::RECETTES} selected="selected"{/if}>Recette</option>
                    <option value="{$categories::DEPENSES}"{if $type == $categories::DEPENSES} selected="selected"{/if}>Dépense</option>
                </select>
            </dd>
            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule}" required="required" /></dd>
            <dt><label for="f_description">Description</label></dt>
            <dd><textarea name="description" id="f_description" rows="4" cols="30">{form_field name=description}</textarea></dd>
            <dt><label for="f_compte">Compte affecté</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                {select_compte comptes=$comptes name="compte"}
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_ajout_cat"}
        <input type="submit" name="add" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































Deleted src/templates/admin/compta/categories/index.tpl version [7a1c1dcb5e].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Catégories" current="compta/categories"}

{include file="admin/compta/categories/_nav.tpl" current=$current_nav}

    {if !empty($liste)}
        <dl class="catList">
        {foreach from=$liste item="cat"}
            <dt>{$cat.intitule}</dt>
            {if !empty($cat.description)}
                <dd class="desc">{$cat.description}</dd>
            {/if}
            <dd class="compte"><strong>{$cat.compte}</strong> - {$cat.compte_libelle}</dd>
            <dd class="actions">
                <a class="icn" href="{$admin_url}compta/operations/?cat={$cat.id}" title="Lister les opérations de cette catégorie">𝍢</a>
                <a class="icn" href="{$admin_url}compta/categories/modifier.php?id={$cat.id}" title="Modifier">✎</a>
                <a class="icn" href="{$admin_url}compta/categories/supprimer.php?id={$cat.id}" title="Supprimer">✘</a>
            </dd>
        {/foreach}
        </dl>
    {else}
        <p class="alert">
            Aucune catégorie trouvée.
        </p>
    {/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































Deleted src/templates/admin/compta/categories/modifier.tpl version [75046d7ae0].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Modifier une catégorie" current="compta/categories"}

{include file="admin/compta/categories/_nav.tpl" current=null}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Modifier une catégorie</legend>
        <dl>
            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule data=$cat}" required="required" /></dd>
            <dt><label for="f_description">Description</label></dt>
            <dd><textarea name="description" id="f_description" rows="4" cols="70">{form_field name=description data=$cat}</textarea></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_edit_cat_%s"|args:$cat.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































Deleted src/templates/admin/compta/categories/supprimer.tpl version [30d30ea90b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{include file="admin/_head.tpl" title="Supprimer une catégorie" current="compta/categories"}

{include file="admin/compta/categories/_nav.tpl" current=null}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer la catégorie comptable ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer la catégorie «&nbsp;{$cat.intitule}&nbsp;» ?
        </h3>
        <p class="help">
            Attention, la catégorie ne pourra pas être supprimée si des opérations y sont
            affectées.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_compta_cat_"|cat:$cat.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































Deleted src/templates/admin/compta/comptes/ajouter.tpl version [f0cf451809].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{include file="admin/_head.tpl" title="Ajouter un compte" current="compta/categories"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Ajouter un compte</legend>
        <dl>
            <dt><label for="f_parent">Compte parent</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                {select_compte comptes=$comptes name="parent" create=true}
            </dd>
            <dt><label for="f_numero">Numéro de compte</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" size="10" name="numero" id="f_numero" value="{form_field name=numero}" required="required" /></dd>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle}" required="required" /></dd>
            <dt><label for="f_position_1">Position</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            {foreach from=$positions item="pos" key="id"}
            <dd>
                <input type="radio" name="position" id="f_position_{$id}" value="{$id}" {if $position == $id}checked="checked"{/if} />
                <label for="f_position_{$id}">{$pos}</label>
            </dd>
            {/foreach}
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_ajout_compte"}
        <input type="submit" name="add" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































































Deleted src/templates/admin/compta/comptes/classe.tpl version [45939485c2].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{include file="admin/_head.tpl" title=$classe_compte.libelle current="compta/categories"}

<ul class="actions">
    <li><a href="{$admin_url}compta/comptes/">Liste des classes</a></li>
    <li><a href="{$admin_url}compta/comptes/ajouter.php?classe={$classe}">Ajouter un compte dans cette classe</a></li>
</ul>

<p class="help">
    Les comptes avec la mention <em>*</em> font partie du plan comptable standard
    et ne peuvent être modifiés ou supprimés.
</p>

{if !empty($liste)}
    <table class="list accountList">
    {foreach from=$liste item="compte"}
        <tr class="niveau_{$compte.id|strlen}">
            <th>{$compte.id}</th>
            <td class="libelle">{$compte.libelle}</td>
            <td>
                {if !empty($compte.desactive)}
                    <em>Désactivé</em>
                {else}
                    {$compte.position|get_position}
                {/if}
            </td>
            <td class="actions">
                {if empty($compte.desactive)}
                    {if !$compte.plan_comptable}
                        <a class="icn" href="{$admin_url}compta/comptes/modifier.php?id={$compte.id}" title="Modifier">✎</a>
                        <a class="icn" href="{$admin_url}compta/comptes/supprimer.php?id={$compte.id}" title="Supprimer">✘</a>
                    {else}
                        <em>*</em>
                    {/if}
                {/if}
            </td>
        </tr>
    {/foreach}
    </table>

{else}
    <p class="alert">
        Aucun compte trouvé.
    </p>
{/if}


{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































Deleted src/templates/admin/compta/comptes/import.tpl version [99c4dfd495].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{include file="admin/_head.tpl" title="Plan comptable" current="compta/categories"}

<ul class="actions">
	<li><a href="{$admin_url}compta/comptes/">Plan comptable</a></li>
	<li class="current"><a href="?import">Import / remise à zéro</a></li>
	<li><a href="?export=plan">Exporter le plan en format JSON</a></li>
</ul>

{form_errors}

{if $confirm}
<p class="confirm">
	{if $confirm == 'import'}L'import s'est correctement déroulé.
	{elseif $confirm == 'reset'}Le plan comptable a bien été remis à zéro.{/if}
</p>
{/if}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

	<fieldset>
		<legend>Importer un plan comptable</legend>
		<p class="help">
			Toute modification actuelle du plan comptable sera perdue.<br />
			Les comptes associés à des écritures ou des comptes bancaires ne seront pas supprimés.
		</p>
		<dl>
			<dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><input type="file" name="upload" id="f_file" required="required" /></dd>
			<dt><label for="f_type">Format de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd>
				<input type="radio" name="format" id="f_format_json" value="json" {*form_field name="format" checked="json"*} checked="checked" />
				<label for="f_format_json">Plan comptable au format JSON de plan comptable Garradin</label>
			</dd>
		</dl>

		<p class="submit">
			{csrf_field key="plan_import"}
			<input type="submit" name="import" value="Importer &rarr;" />
		</p>
	</fieldset>

</form>

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Remise à zéro du plan comptable</legend>
		<p class="help">
			Permet de rétablir le plan comptable par défaut de Garradin.<br />
			Vos modifications personnelles seront perdues, assurez-vous d'en avoir une copie avant en cas de problèmes (bouton «&nbsp;Exporter le plan&nbsp;»).
		</p>

		<p class="submit">
			{csrf_field key="plan_reset"}
			<input type="submit" name="reset" value="Rétablir le plan comptable &rarr;" />
		</p>

	</fieldset>

</form>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































Deleted src/templates/admin/compta/comptes/index.tpl version [4936874101].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Plan comptable" current="compta/categories"}

<ul class="actions">
	<li class="current"><a href="{$admin_url}compta/comptes/">Plan comptable</a></li>
	<li><a href="?import">Import / remise à zéro</a></li>
	<li><a href="?export=plan">Exporter le plan en format JSON</a></li>
</ul>

<ul class="accountList">
{foreach from=$classes item="_classe"}
	<li><h4><a href="{$admin_url}compta/comptes/?classe={$_classe.id}">{$_classe.libelle}</a></h4></li>
{/foreach}
</ul>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























Deleted src/templates/admin/compta/comptes/modifier.tpl version [f906ad5d55].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{include file="admin/_head.tpl" title="Modifier un compte" current="compta/categories"}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Modifier un compte</legend>
        <dl>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$compte}" required="required" /></dd>
            <dt><label for="f_position_1">Position</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            {foreach from=$positions item="pos" key="id"}
            <dd>
                <input type="radio" name="position" id="f_position_{$id}" value="{$id}" {if $position == $id}checked="checked"{/if} />
                <label for="f_position_{$id}">{$pos}</label>
            </dd>
            {/foreach}
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="compta_edit_compte_%s"|args:$compte.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


























































Deleted src/templates/admin/compta/comptes/supprimer.tpl version [89dec94900].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{include file="admin/_head.tpl" title="Supprimer un compte" current="compta/categories"}

{form_errors}

{if !$can_delete && !$can_disable}
    <p class="alert">
        Ce compte ne peut être supprimé ou désactivé.
        Pour pouvoir supprimer ou désactiver un compte aucune catégorie ou écriture comptable ne doit y faire référence.
        Pour pouvoir désactiver un compte aucune écriture comptable ne doit y faire référence dans l'exercice en cours.
    </p>
{elseif $can_disable && !$can_delete}

    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Désactiver le compte ?</legend>
            <h3 class="warning">
                Êtes-vous sûr de vouloir désactiver le compte «&nbsp;{$compte.id} - {$compte.libelle}&nbsp;»&nbsp;?
            </h3>
            <p class="help">
                Une fois désactivé il ne sera plus possible de l'utiliser, mais il pourra par contre être réactivé.
            </p>
        </fieldset>

        <p class="submit">
            {csrf_field key="compta_disable_compte_%s"|args:$compte.id}
            <input type="submit" name="disable" value="Désactiver &rarr;" />
        </p>

    </form>
{else}
    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Supprimer le compte ?</legend>
            <h3 class="warning">
                Êtes-vous sûr de vouloir supprimer le compte «&nbsp;{$compte.id} - {$compte.libelle}&nbsp;»&nbsp;?
            </h3>
        </fieldset>

        <p class="submit">
            {csrf_field key="compta_delete_compte_%s"|args:$compte.id}
            <input type="submit" name="delete" value="Supprimer &rarr;" />
        </p>

    </form>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































Deleted src/templates/admin/compta/import.tpl version [5bf6707f89].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{include file="admin/_head.tpl" title="Import / Export" current="compta"}

{form_errors}

{if $ok}
    <p class="confirm">
        L'import s'est bien déroulé.
    </p>
{/if}

<ul class="actions">
    <li class="current"><a href="{$admin_url}compta/import.php">Importer</a></li>
    <li><a href="{$admin_url}compta/import.php?export=csv">Exporter en CSV</a></li>
    <li><a href="{$admin_url}compta/import.php?export=ods">Exporter en classeur Office</a></li>
</ul>

<form method="post" action="{$self_url}" enctype="multipart/form-data">

    <fieldset>
        <legend>Importer depuis un fichier</legend>
        <dl>
            <dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="file" name="upload" id="f_file" required="required" /></dd>
            <dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
                <label for="f_type">Export CSV de Garradin</label>
            </dd>
            <dd class="help">
                Export du journal comptable au format CSV provenant de Garradin.
                Les lignes comportant un numéro d'opération mettront à jour les opérations existantes,
                les lignes sans numéro créeront de nouvelles opérations.
            </dd>
        </dl>
    </fieldset>

    <p class="alert">
        Si le fichier comporte des opérations dont la date est en dehors de l'exercice courant,
        elles seront ignorées.
    </p>

    <p class="submit">
        {csrf_field key="compta_import"}
        <input type="submit" name="import" value="Importer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































Deleted src/templates/admin/compta/index.tpl version [9d6a3b1dd3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{include file="admin/_head.tpl" title="Comptabilité" current="compta"}

{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
<ul class="actions">
    <li><a href="{$admin_url}compta/import.php">Import / export</a></li>
    <li><a href="{$admin_url}compta/operations/recherche_sql.php">Recherche par requête SQL</a></li>
</ul>
{/if}

<div class="infos">
    <p>
        <img src="{$admin_url}compta/graph.php?g=recettes_depenses" />
        <img src="{$admin_url}compta/graph.php?g=banques_caisses" />
    </p>
    <p>
        <img src="{$admin_url}compta/pie.php?g=recettes" />
        <img src="{$admin_url}compta/pie.php?g=depenses" />
    </p>
</div>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































Deleted src/templates/admin/compta/operations/cotisation.tpl version [a490130f82].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{include file="admin/_head.tpl" title="Écritures liées à une cotisation" current="compta/gestion"}

<ul class="actions">
    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}"><b>{$membre.identite}</b></a></li>
    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>

{if empty($journal)}
    <p class="alert">Aucune écriture comptable n'est associée à cette cotisation.</p>
{else}
<table class="list">
    <colgroup>
        <col width="3%" />
        <col width="3%" />
        <col width="12%" />
        <col width="10%" />
        <col />
    </colgroup>
    <thead>
        <tr>
            <td></td>
            <td></td>
            <td>Date</td>
            <td>Montant</td>
            <th>Libellé</th>
            <td>Compte débité</td>
            <td>Compte crédité</td>
        </tr>
    </thead>
    <tbody>
    {foreach from=$journal item="ligne"}
        <tr>
            <td><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id}">{$ligne.id}</a></td>
            <td class="actions">
            {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
                <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id}" title="Modifier cette opération">✎</a>
            {/if}
            </td>
            <td>{$ligne.date|format_sqlite_date_to_french}</td>
            <td>{$ligne.montant|escape|html_money}</td>
            <th>{$ligne.libelle}</th>
            <td>{$ligne.compte_debit} — {$ligne.compte_debit|get_nom_compte}</td>
            <td>{$ligne.compte_credit} — {$ligne.compte_credit|get_nom_compte}</td>
        </tr>
    {/foreach}
    </tbody>
</table>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































Deleted src/templates/admin/compta/operations/index.tpl version [5baa65eca3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{include file="admin/_head.tpl" title="Suivi des opérations" current="compta/gestion"}

<ul class="actions">
    <li class="recettes{if $type == Compta\Categories::RECETTES} current{/if}"><a href="{$admin_url}compta/operations/?recettes">Recettes</a></li>
    <li class="depenses{if $type == Compta\Categories::DEPENSES} current{/if}"><a href="{$admin_url}compta/operations/?depenses">Dépenses</a></li>
    <li class="autres{if $type == Compta\Categories::AUTRES} current{/if}"><a href="{$admin_url}compta/operations/?autres">Autres</a></li>
    {*<li><a href="{$admin_url}compta/operations/recherche.php">Recherche d'opération</a></li>*}
    {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}compta/operations/recherche_sql.php">Recherche par requête SQL</a></li>
    {/if}
</ul>

{if $type != Compta\Categories::AUTRES}
<form method="get" action="{$self_url}">
    <fieldset>
        <legend>Filtrer par catégorie</legend>
        <select name="cat" onchange="if (!this.value) location.href = '?{if $type == Compta\Categories::RECETTES}recettes{else}depenses{/if}'; else this.form.submit();">
            <option value="">-- Toutes</option>
        {foreach from=$liste_cats item="cat"}
            <option value="{$cat.id}"{if $cat.id == $categorie.id} selected="selected"{/if}>{$cat.intitule}</option>
        {/foreach}
        </select>
        <input type="submit" value="OK" />
    </fieldset>
</form>
{/if}

<table class="list">
    <colgroup>
        <col width="3%" />
        <col width="12%" />
        <col width="10%" />
        <col />
        {if !$categorie && $type}
        <col width="20%" />
        {/if}
    </colgroup>
    <tbody>
    {foreach from=$journal item="ligne"}
        <tr>
            <td class="num"><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id}">{$ligne.id}</a></td>
            <td>{$ligne.date|date_fr:'d/m/Y'}</td>
            <td>{$ligne.montant|escape|html_money} {$config.monnaie}</td>
            <th>{$ligne.libelle}</th>
            {if !$categorie && $type}
            <td>{$ligne.categorie}</td>
            {/if}
        </tr>
    {foreachelse}
        <tr>
            <td colspan="2"></td>
            <td colspan="2">
                Aucune opération.
            </td>
            {if !$categorie && $type}<td></td>{/if}
        </tr>
    {/foreach}
    </tbody>
    <tfoot>
        <tr>
            <td></td>
            <th>Total</th>
            <td>{$total|escape|html_money} {$config.monnaie}</td>
            <td></td>
            {if !$categorie && $type}<td></td>{/if}
        </tr>
    </tfoot>
</table>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































































Deleted src/templates/admin/compta/operations/membre.tpl version [8524d38029].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{include file="admin/_head.tpl" title="Écritures réalisées par le membre" current="compta/gestion"}

<ul class="actions">
    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}"><b>{$membre.identite}</b></a></li>
    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>

<form method="get" action="{$self_url}">
    <fieldset>
        <legend>Exercice à visualiser</legend>
        <p>
            <input type="hidden" name="id" value="{$membre.id}" />
            <select name="exercice" id="f_exercice" onchange="this.form.submit();">
                {foreach from=$exercices item="e"}
                <option value="{$e.id}" {form_field name="exercice" selected=$e.id default=$exercice}>{$e.libelle} —
                {if $e.cloture}Clôturé{else}En cours{/if}
                — Du {$e.debut|date_fr:'d/m/Y'} au {$e.fin|date_fr:'d/m/Y'}
                </option>
                {/foreach}
            </select>
        </p>
        <noscript>
            <p>
                <input type="submit" value="Visualiser &rarr;" />
            </p>
        </noscript>
    </fieldset>
</form>

{if empty($journal)}
    <p class="alert">Aucune écriture comptable n'est associée à ce membre pour l'exercice demandé.</p>
{else}
<table class="list">
    <colgroup>
        <col width="3%" />
        <col width="3%" />
        <col width="12%" />
        <col width="10%" />
        <col />
    </colgroup>
    <thead>
        <tr>
            <td></td>
            <td></td>
            <td>Date</td>
            <td>Montant</td>
            <th>Libellé</th>
            <td>Compte débité</td>
            <td>Compte crédité</td>
        </tr>
    </thead>
    <tbody>
    {foreach from=$journal item="ligne"}
        <tr>
            <td><a href="{$admin_url}compta/operations/voir.php?id={$ligne.id}">{$ligne.id}</a></td>
            <td class="actions">
            {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
                <a class="icn" href="{$admin_url}compta/operations/modifier.php?id={$ligne.id}" title="Modifier cette opération">✎</a>
            {/if}
            </td>
            <td>{$ligne.date|format_sqlite_date_to_french}</td>
            <td>{$ligne.montant|escape|html_money}</td>
            <th>{$ligne.libelle}</th>
            <td>{$ligne.compte_debit} — {$ligne.compte_debit|get_nom_compte}</td>
            <td>{$ligne.compte_credit} — {$ligne.compte_credit|get_nom_compte}</td>
        </tr>
    {/foreach}
    </tbody>
</table>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































































Deleted src/templates/admin/compta/operations/modifier.tpl version [da5bdb03fc].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
{include file="admin/_head.tpl" title="Modification de l'opération n°%d"|args:$operation.id current="compta/saisie" js=1}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Informations sur l'opération</legend>
        <dl>
            <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="date" name="date" id="f_date" value="{form_field name=date default=$operation.date|date_fr:'Y-m-d'}" size="10" required="required" /></dd>
            <dt><label for="f_libelle">Libellé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="libelle" id="f_libelle" value="{form_field name=libelle data=$operation}" required="required" /></dd>
            <dt><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="number" size="5" name="montant" id="f_montant" value="{form_field name=montant data=$operation}" min="0.00" step="0.01" required="required" /> {$config.monnaie}</dd>

{if is_null($type)}
            <dt><label for="f_compte_debit">Compte débité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                {select_compte comptes=$comptes name="compte_debit" data=$operation}
            </dd>
            <dt><label for="f_compte_credit">Compte crédité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                {select_compte comptes=$comptes name="compte_credit" data=$operation}
            </dd>
{else}
            <dt><label for="f_moyen_paiement">Moyen de paiement</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="moyen_paiement" id="f_moyen_paiement">
                {foreach from=$moyens_paiement item="moyen"}
                    <option value="{$moyen.code}"{if $moyen.code == $operation.moyen_paiement} selected="selected"{/if}>{$moyen.nom}</option>
                {/foreach}
                </select>
            </dd>
            <dt class="f_cheque"><label for="f_numero_cheque">Numéro de chèque</label></dt>
            <dd class="f_cheque"><input type="text" name="numero_cheque" id="f_numero_cheque" value="{form_field name=numero_cheque data=$operation}" /></dd>
            <dt class="f_banque"><label for="f_banque">Compte bancaire</label></dt>
            <dd class="f_banque">
                <select name="banque" id="f_banque">
                {foreach from=$comptes_bancaires item="compte"}
                    <option value="{$compte.id}"{if ($type == Compta\Categories::DEPENSES && $compte.id == $operation.compte_credit) || $compte.id == $operation.compte_debit} selected="selected"{/if}>{$compte.libelle} - {$compte.banque}</option>
                {/foreach}
                {if ($type == Compta\Categories::RECETTES)}
                    <option value="{$id_cheque_a_encaisser}"{form_field name="banque" selected=$id_cheque_a_encaisser default=$operation.compte_debit}>Chèques à encaisser</option>
                    <option value="{$id_carte_a_encaisser}"{form_field name="banque" selected=$id_carte_a_encaisser default=$operation.compte_debit}>Paiement CB à encaisser</option>
                {/if}
                </select>
            </dd>
{/if}

            <dt><label for="f_numero_piece">Numéro de pièce comptable</label></dt>
            <dd><input type="text" name="numero_piece" id="f_numero_piece" value="{form_field name=numero_piece data=$operation}" /></dd>
            <dt><label for="f_remarques">Remarques</label></dt>
            <dd><textarea name="remarques" id="f_remarques" rows="4" cols="30">{form_field name=remarques data=$operation}</textarea></dd>

            {if count($projets) > 0}
            <dt><label for="f_projet">Projet</label></dt>
            <dd>
                <select name="id_projet" id="f_projet">
                    <option value="0">-- Aucun</option>
                    {foreach from=$projets key="id" item="libelle"}
                        <option value="{$id}"{form_field name="id_projet" selected=$id data=$operation}>{$libelle}</option>
                    {/foreach}
                </select>
            </dd>
            {/if}

        </dl>
    </fieldset>

{if !is_null($type)}
    <fieldset>
        <legend>Catégorie</legend>
        <dl class="catList">
        {foreach from=$categories item="cat"}
            <dt>
                <input type="radio" name="id_categorie" value="{$cat.id}" id="f_cat_{$cat.id}" {form_field name="id_categorie" checked=$cat.id data=$operation} />
                <label for="f_cat_{$cat.id}">{$cat.intitule}</label>
            </dt>
            {if !empty($cat.description)}
                <dd class="desc">{$cat.description}</dd>
            {/if}
        {/foreach}
        </dl>
    </fieldset>

    <script type="text/javascript">
    {literal}
    (function () {

        window.changeMoyenPaiement = function()
        {
            var elm = $('#f_moyen_paiement');
            g.toggle('.f_cheque', elm.value == 'CH');
            g.toggle('.f_banque', elm.value != 'ES');
        };

        changeMoyenPaiement();

        $('#f_moyen_paiement').onchange = changeMoyenPaiement;
    } ());
    {/literal}
    </script>
{/if}

    <p class="submit">
        {csrf_field key="compta_modifier_%d"|args:$operation.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































































































































Deleted src/templates/admin/compta/operations/recherche_sql.tpl version [c20d4df351].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{include file="admin/_head.tpl" title="Recherche par requête SQL" current="compta"}

<form method="get" action="{$admin_url}compta/operations/recherche_sql.php">
    <fieldset>
        <legend>Schéma des tables SQL</legend>
        <pre class="sql_schema">{$schema.journal}</pre>
        <dl>
            <dt><label for="f_query">Requête SQL</label></dt>
            <dd class="help">Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée.</dd>
            <dd><textarea name="query" id="f_query" cols="50" rows="7" required="required">{$query}</textarea></dd>
        </dl>
        <p class="submit">
            <input type="submit" value="Exécuter &rarr;" />
        </p>
    </fieldset>
</form>

{if !empty($error)}
<p class="error">
    <strong>Erreur dans la requête SQL :</strong><br />
    {$error}
</p>
{/if}

{if !empty($result)}
<p class="alert">{$result|count} résultats renvoyés.</p>
<table class="list search">
    <thead>
        {foreach from=$result[0] key="col" item="ignore"}
            <td>{$col}</td>
        {/foreach}
        {if array_key_exists('id', $result[0])}
        <td></td>
        {/if}
    </thead>
    <tbody>
        {foreach from=$result item="row"}
            <tr>
                {foreach from=$row item="col"}
                    <td>{$col}</td>
                {/foreach}
                {if array_key_exists('id', $result[0])}
                <td class="actions">
                    {if !empty($row.id)}
                    <a class="icn" href="voir.php?id={$row.id}" title="Fiche opération">❓</a>
                    <a class="icn" href="modifier.php?id={$row.id}" title="Modifier cette opération">✎</a>
                    {/if}
                </td>
                {/if}
            </tr>
        {/foreach}
    </tbody>
</table>

{else}
<p class="alert">
    Aucun résultat trouvé.
</p>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


























































































































Deleted src/templates/admin/compta/operations/voir.tpl version [48d792da66].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
{include file="admin/_head.tpl" title="Opération n°%d"|args:$operation.id current="compta/gestion"}

{if $session->canAccess('compta', Membres::DROIT_ADMIN)}
<ul class="actions">
    <li class="edit"><a href="{$admin_url}compta/operations/modifier.php?id={$operation.id}">Modifier cette opération</a></li>
    <li class="delete"><a href="{$admin_url}compta/operations/supprimer.php?id={$operation.id}">Supprimer cette opération</a></li>
</ul>
{/if}

<dl class="describe">
    <dt>Date</dt>
    <dd>{$operation.date|date_fr:'l j F Y (d/m/Y)'}</dd>
    <dt>Libellé</dt>
    <dd>{$operation.libelle}</dd>
    <dt>Montant</dt>
    <dd>{$operation.montant|escape|html_money}&nbsp;{$config.monnaie}</dd>
    <dt>Numéro pièce comptable</dt>
    <dd>{if trim($operation.numero_piece)}{$operation.numero_piece}{else}<em>Non renseigné</em>{/if}</dd>

    {if $operation.id_categorie}

        <dt>Moyen de paiement</dt>
        <dd>{if trim($operation.moyen_paiement)}{$moyen_paiement}{else}<em>Non renseigné</em>{/if}</dd>

        {if $operation.moyen_paiement == 'CH'}
            <dt>Numéro de chèque</dt>
            <dd>{if trim($operation.numero_cheque)}{$operation.numero_cheque}{else}<em>Non renseigné</em>{/if}</dd>
        {/if}

        {if $operation.moyen_paiement && $operation.moyen_paiement != 'ES'}
            <dt>Compte bancaire</dt>
            <dd>{$compte}</dd>
        {/if}

        <dt>Catégorie</dt>
        <dd>
            <a href="{$admin_url}compta/operations/?{if $categorie.type == Compta\Categories::DEPENSES}depenses{else}recettes{/if}">{if $categorie.type == Compta\Categories::DEPENSES}Dépense{else}Recette{/if}</a>&nbsp;:
            <a href="{$admin_url}compta/operations/?cat={$operation.id_categorie}">{$categorie.intitule}</a>
        </dd>
    {/if}

    <dt>Exercice</dt>
    <dd>
        <a href="{$admin_url}compta/exercices/">{$exercice.libelle}</a>
        | Du {$exercice.debut|date_fr:'d/m/Y'} au {$exercice.fin|date_fr:'d/m/Y'}
        | <strong>{if $exercice.cloture}Clôturé{else}En cours{/if}</strong>
    </dd>

    {if $operation.id_projet}
        <dt>Projet</dt>
        <dd>
            <a href="{$admin_url}compta/projets/">{$projet.libelle}</a>
        </dd>
    {/if}

    <dt>Opération créée par</dt>
    <dd>
        {if $operation.id_auteur}
            {if $session->canAccess('compta', Membres::DROIT_ACCES)}
                <a href="{$admin_url}membres/fiche.php?id={$operation.id_auteur}">{$nom_auteur}</a>
            {else}
                {$nom_auteur}
            {/if}
        {else}
            <em>membre supprimé</em>
        {/if}
    </dd>

    <dt>Opération liée à</dt>
    <dd>
        {if empty($related_members)}
            Aucun membre n'est lié à cette opération.
        {else}
            {foreach from=$related_members item="membre"}
                <a href="{$admin_url}membres/{if $membre.id_cotisation}cotisations{else}fiche{/if}.php?id={$membre.id_membre}">{if $membre.id_cotisation}Cotisation pour {/if}{$membre.identite}</a>
            {/foreach}
        {/if}
    </dd>

    <dt>Remarques</dt>
    <dd>{if trim($operation.remarques)}{$operation.remarques}{else}Non renseigné{/if}</dd>
</dl>

<table class="list multi">
    <thead>
        <tr>
            <th colspan="2">Comptes</th>
            <td>Débit</td>
            <td>Crédit</td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><a href="{$admin_url}compta/comptes/journal.php?id={$operation.compte_debit}">{$operation.compte_debit}</a></td>
            <td>{$nom_compte_debit}</td>
            <td>{$operation.montant|escape|html_money}&nbsp;{$config.monnaie}</td>
            <td></td>
        </tr>
        <tr>
            <td><a href="{$admin_url}compta/comptes/journal.php?id={$operation.compte_credit}">{$operation.compte_credit}</a></td>
            <td>{$nom_compte_credit}</td>
            <td></td>
            <td>{$operation.montant|escape|html_money}&nbsp;{$config.monnaie}</td>
        </tr>
    </tbody>
</table>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































































































































Deleted src/templates/admin/compta/projets/index.tpl version [c946bff41c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{include file="admin/_head.tpl" title="Projets" current="compta/exercices"}

<ul class="actions">
    <li><a href="{$admin_url}compta/exercices/">Exercices</a></li>
    <li class="current"><a href="{$admin_url}compta/projets/">Projets (compta analytique)</a></li>
</ul>

{form_errors}

{if $action == 'modifier'}

    <form method="post" action="{$self_url}">
        <fieldset>
            <legend>Modifier un projet</legend>
            <dl>
                <dt><label for="f_libelle">Libellé</label></dt>
                <dd><input type="text" name="libelle" id="f_labelle" value="{form_field name=libelle data=$projet}" /></dd>
            </dl>
            <p class="submit">
                {csrf_field key="modifier_projet_%d"|args:$projet.id}
                <input type="submit" name="modifier" value="Modifier &rarr;" />
            </p>
        </fieldset>
    </form>
{elseif $action == 'supprimer'}

    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Supprimer le projet ?</legend>
            <h3 class="warning">
                Êtes-vous sûr de vouloir supprimer le projet «&nbsp;{$projet.libelle}&nbsp;» ?
            </h3>
            <p class="help">
                Les opérations liées à ce projet ne seront pas supprimées, mais n'auront
                plus de projet lié.
            </p>
        </fieldset>

        <p class="submit">
            {csrf_field key="supprimer_projet_%d"|args:$projet.id}
            <input type="submit" name="supprimer" value="Supprimer &rarr;" />
        </p>

    </form>

{else}
    {if !empty($liste)}
        <dl class="catList">
        {foreach from=$liste item="projet"}
            <dt>{$projet.libelle}</dt>
            <dd class="compte">{$projet.nb_operations} opérations</dd>
            <dd class="desc">
                <a href="{$admin_url}compta/rapports/journal.php?projet={$projet.id}">Journal général</a>
                | <a href="{$admin_url}compta/rapports/grand_livre.php?projet={$projet.id}">Grand livre</a>
                | <a href="{$admin_url}compta/rapports/compte_resultat.php?projet={$projet.id}">Compte de résultat</a>
                | <a href="{$admin_url}compta/rapports/bilan.php?projet={$projet.id}">Bilan</a>
            </dd>
            {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
            <dd class="actions">
                <a class="icn" href="{$admin_url}compta/projets/?modifier={$projet.id}" title="Modifier">✎</a>
                <a class="icn" href="{$admin_url}compta/projets/?supprimer={$projet.id}" title="Supprimer">✘</a>
            </dd>
            {/if}
        {/foreach}
        </dl>
    {/if}

    {if $session->canAccess('compta', Membres::DROIT_ADMIN)}
    <form method="post" action="{$self_url}">
        <fieldset>
            <legend>Ajouter un nouveau projet</legend>
            <dl>
                <dt><label for="f_libelle">Libellé</label></dt>
                <dd><input type="text" name="libelle" id="f_labelle" value="{form_field name=libelle}" /></dd>
            </dl>
            <p class="submit">
                {csrf_field key="ajout_projet"}
                <input type="submit" name="ajouter" value="Ajouter &rarr;" />
            </p>
        </fieldset>
    </form>
    {/if}
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































































































Modified src/templates/admin/config/_menu.tpl from [cd582bc2fa] to [23c20c4779].

1

2
3
4
5
6
7

8

9

<ul class="actions">

	<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li>
	<li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li>
	<li{if $current == 'fiches_membres'} class="current"{/if}><a href="{$admin_url}config/membres.php">Fiche des membres</a></li>
	<li{if $current == 'site'} class="current"{/if}><a href="{$admin_url}config/site.php">Site public</a></li>
	<li{if $current == 'donnees'} class="current"{/if}><a href="{$admin_url}config/donnees/">Sauvegarde et restauration</a></li>
	<li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li>

	<li{if $current == 'logs'} class="current"{/if}><a href="{$admin_url}config/logs.php">Journaux</a></li>

</ul>

|
>
|
|
|
|
|
|
>
|
>
|
>
1
2
3
4
5
6
7
8
9
10
11
12
13
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li>
		<li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li>
		<li{if $current == 'fiches_membres'} class="current"{/if}><a href="{$admin_url}config/membres.php">Fiche des membres</a></li>
		<li{if $current == 'site'} class="current"{/if}><a href="{$admin_url}config/site.php">Site public</a></li>
		<li{if $current == 'donnees'} class="current"{/if}><a href="{$admin_url}config/donnees/">Sauvegarde et restauration</a></li>
		<li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li>
		{if ENABLE_TECH_DETAILS}
		<li{if $current == 'logs'} class="current"{/if}><a href="{$admin_url}config/logs.php?type=errors">Journaux</a></li>
		{/if}
	</ul>
</nav>

Modified src/templates/admin/config/categories/index.tpl from [7ab8e26a24] to [b1e9086f4b].

14
15
16
17
18
19
20
21
22
23
24
25


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
            <tr>
                <th>{$cat.nom}</th>
                <td class="num">{$cat.nombre}</td>
                <td class="droits">
                    {format_droits droits=$cat}
                </td>
                <td class="actions">
                    <a class="icn" href="{$admin_url}membres/?cat={$cat.id}" title="Liste des membres">👪</a>
                    <a class="icn" href="{$admin_url}config/categories/modifier.php?id={$cat.id}" title="Modifier">✎</a>
                    {if $cat.id != $user.id_categorie}
                    <a class="icn" href="{$admin_url}config/categories/supprimer.php?id={$cat.id}" title="Supprimer">✘</a>
                    {/if}


                </td>
            </tr>
        {/foreach}
    </tbody>
</table>

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Ajouter une catégorie</legend>
        <dl>
            <dt><label for="f_nom">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom" id="f_nom" value="{form_field name=nom}" required="required" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="new_cat"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>


{include file="admin/_foot.tpl"}







<
<

|

>
>


















|






14
15
16
17
18
19
20


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
            <tr>
                <th>{$cat.nom}</th>
                <td class="num">{$cat.nombre}</td>
                <td class="droits">
                    {format_droits droits=$cat}
                </td>
                <td class="actions">


                    {if $cat.id != $user.id_categorie}
                        {linkbutton shape="delete" label="Supprimer" href="supprimer.php?id=%d"|args:$cat.id}
                    {/if}
                    {linkbutton shape="edit" label="Modifier" href="modifier.php?id=%d"|args:$cat.id}
                    {linkbutton shape="users" label="Liste des membres" href="!membres/?cat=%d"|args:$cat.id}
                </td>
            </tr>
        {/foreach}
    </tbody>
</table>

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Ajouter une catégorie</legend>
        <dl>
            <dt><label for="f_nom">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom" id="f_nom" value="{form_field name=nom}" required="required" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="new_cat"}
        {button type="submit" name="save" label="Ajouter" shape="right" class="main"}
    </p>

</form>


{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/categories/modifier.tpl from [5f082eb8b5] to [fb8b7e4413].

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
            <dd class="help">
                Si coché cette catégorie ne sera visible qu'aux administrateurs et ne recevra pas
                de messages collectifs ou de rappels.
            </dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Cotisation obligatoire</legend>
        <dl>
            <dt><label for="f_id_cotisation_obligatoire">Cotisation obligatoire</label></dt>
            <dd>
                <select name="id_cotisation_obligatoire" id="f_id_cotisation_obligatoire">
                    <option value="">-- Non</option>
                    {foreach from=$cotisations item="cotisation"}
                    <option value="{$cotisation.id}" {form_field name="id_cotisation_obligatoire" selected=$cotisation.id data=$cat}>
                        {$cotisation.intitule} 
                        — {$cotisation.montant|escape|html_money} {$config.monnaie}
                        — {if $cotisation.duree}pour {$cotisation.duree} jours
                        {elseif $cotisation.debut}
                            du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french}
                        {else}
                            ponctuelle
                        {/if}
                    </option>
                    {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Droits</legend>
        <dl class="droits">
            <dt><label for="f_droit_connexion_aucun">Les membres de cette catégorie peuvent-ils se connecter ?</label></dt>
            {if $readonly}
                <dd class="help">
                    Il n'est pas possible de désactiver ce droit pour votre propre catégorie.







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







18
19
20
21
22
23
24
























25
26
27
28
29
30
31
            <dd class="help">
                Si coché cette catégorie ne sera visible qu'aux administrateurs et ne recevra pas
                de messages collectifs ou de rappels.
            </dd>
        </dl>
    </fieldset>

























    <fieldset>
        <legend>Droits</legend>
        <dl class="droits">
            <dt><label for="f_droit_connexion_aucun">Les membres de cette catégorie peuvent-ils se connecter ?</label></dt>
            {if $readonly}
                <dd class="help">
                    Il n'est pas possible de désactiver ce droit pour votre propre catégorie.
150
151
152
153
154
155
156
157
158
159
160
161
162
                <label for="f_droit_config_admin"><b class="admin">&#x2611;</b> Oui</label>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_cat_"|cat:$cat.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}







|





126
127
128
129
130
131
132
133
134
135
136
137
138
                <label for="f_droit_config_admin"><b class="admin">&#x2611;</b> Oui</label>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_cat_"|cat:$cat.id}
        {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/categories/supprimer.tpl from [9ebed2a419] to [f56242fa31].

20
21
22
23
24
25
26
27
28
29
30
31
32
            aux seuls membres de ce groupe, elles redeviendront lisibles et modifiables
            par tous les membres ayant accès au wiki !
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_cat_"|cat:$cat.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}







|





20
21
22
23
24
25
26
27
28
29
30
31
32
            aux seuls membres de ce groupe, elles redeviendront lisibles et modifiables
            par tous les membres ayant accès au wiki !
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_cat_"|cat:$cat.id}
        {button type="submit" name="delete" label="Supprimer" shape="delete" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/donnees/_menu.tpl from [27f416469b] to [96b956198e].


1
2
3
4
5
6
7
8
9


<ul class="actions sub">
	<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/donnees/">Sauvegarder et restaurer</a></li>
	<li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}config/donnees/import.php">Import et export</a></li>
	<li{if $current == 'local'} class="current"{/if}><a href="{$admin_url}config/donnees/local.php">Gestion des sauvegardes</a></li>
	{if ENABLE_AUTOMATIC_BACKUPS}
	<li{if $current == 'automatique'} class="current"{/if}><a href="{$admin_url}config/donnees/automatique.php">Configuration de la sauvegarde automatique</a></li>
	{/if}
	<li{if $current == 'reset'} class="current"{/if}><a href="{$admin_url}config/donnees/reset.php">Remise à zéro</a></li>
</ul>

>
|
|
|
|
|
|
|
|
|
>
1
2
3
4
5
6
7
8
9
10
11
<nav class="tabs">
	<ul class="sub">
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/donnees/">Sauvegarder et restaurer</a></li>
		<li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}config/donnees/import.php">Import et export</a></li>
		<li{if $current == 'local'} class="current"{/if}><a href="{$admin_url}config/donnees/local.php">Gestion des sauvegardes</a></li>
		{if ENABLE_AUTOMATIC_BACKUPS}
		<li{if $current == 'automatique'} class="current"{/if}><a href="{$admin_url}config/donnees/automatique.php">Configuration de la sauvegarde automatique</a></li>
		{/if}
		<li{if $current == 'reset'} class="current"{/if}><a href="{$admin_url}config/donnees/reset.php">Remise à zéro</a></li>
	</ul>
</nav>

Modified src/templates/admin/config/donnees/automatique.tpl from [1804eff257] to [f0d729ff37].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{include file="admin/_head.tpl" title="Sauvegarde et restauration" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="automatique"}

{form_errors}

{if $ok == 'config'}
	<p class="confirm">La configuration a bien été enregistrée.</p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
	<legend>Configuration de la sauvegarde automatique</legend>
	<p class="help">









|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{include file="admin/_head.tpl" title="Sauvegarde et restauration" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="automatique"}

{form_errors}

{if $ok == 'config'}
	<p class="block confirm">La configuration a bien été enregistrée.</p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
	<legend>Configuration de la sauvegarde automatique</legend>
	<p class="help">

Modified src/templates/admin/config/donnees/import.tpl from [79252f8a0f] to [19ab1e4db0].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="admin/_head.tpl" title="Import & export" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="import"}

<fieldset>
<dl>
	<dt>Membres</dt>
	<dd><a href="{$admin_url}membres/import.php">Import de la liste des membres</a></dd>
	<dd><a href="{$admin_url}membres/import.php?export=ods">Export de la liste des membres au format tableur Calc / Excel</a></dd>
	<dd><a href="{$admin_url}membres/import.php?export=csv">Export de la liste des membres au format CSV</a></dd>
	<dt>Comptabilité</dt>
	<dd><a href="{$admin_url}compta/import.php">Import des données comptables</a></dd>
	<dd><a href="{$admin_url}compta/import.php?export=ods">Export des données comptables au format tableur Calc / Excel</a></dd>
	<dd><a href="{$admin_url}compta/import.php?export=csv">Export des données comptables au format CSV</a></dd>
</dl>
</fieldset>

{include file="admin/_foot.tpl"}













|
|
|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="admin/_head.tpl" title="Import & export" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="import"}

<fieldset>
<dl>
	<dt>Membres</dt>
	<dd><a href="{$admin_url}membres/import.php">Import de la liste des membres</a></dd>
	<dd><a href="{$admin_url}membres/import.php?export=ods">Export de la liste des membres au format tableur Calc / Excel</a></dd>
	<dd><a href="{$admin_url}membres/import.php?export=csv">Export de la liste des membres au format CSV</a></dd>
	<dt>Comptabilité</dt>
	<dd><a href="{$admin_url}acc/years/import.php">Import des données comptables</a></dd>
	<dd><a href="{$admin_url}acc/years/import.php?export=ods">Export des données comptables au format tableur Calc / Excel</a></dd>
	<dd><a href="{$admin_url}acc/years/import.php?export=csv">Export des données comptables au format CSV</a></dd>
</dl>
</fieldset>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/donnees/index.tpl from [87fb524e37] to [45e209af0c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60


61
62
63
64
65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="Sauvegarde et restauration" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="index"}

{form_errors}

{if $code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT}
    <p class="alert">Pour passer outre, renvoyez le fichier en cochant la case «&nbsp;Ignorer les erreurs&nbsp;».
    Attention, si vous avez effectué des modifications dans la base de données, cela peut créer des bugs&nbsp;!</p>
{/if}

{if $ok}
    <p class="confirm">
        {if $ok == 'restore'}La restauration a bien été effectuée. Si vous désirez revenir en arrière, vous pouvez utiliser la sauvegarde automatique nommée <em>{$now_date}.avant_restauration.sqlite</em>, sinon vous pouvez l'effacer.
            {if $ok_code & Sauvegarde::NOT_AN_ADMIN}
            </p>
            <p class="alert">
                <strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Garradin a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter.
                Merci de corriger les droits des catégories maintenant.
            {/if}
        {elseif $ok == 'remove'}La sauvegarde a été supprimée.
        {/if}
    </p>
{/if}


<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Téléchargement d'une sauvegarde</legend>
	<p class="help">
		Info : la base de données fait actuellement {$db_size|format_bytes} (dont {$files_size|format_bytes} pour les documents et images).
	</p>
    <p>
        {csrf_field key="backup_download"}
        <input type="submit" name="download" value="Télécharger une copie de la base de données sur mon ordinateur &rarr;" />
    </p>
</fieldset>

</form>

<form method="post" action="{$self_url_no_qs}" enctype="multipart/form-data">

<fieldset>
    <legend><label for="f_file">Restaurer depuis un fichier de sauvegarde</label></legend>
    <p class="alert">
        Attention, l'intégralité des données courantes seront effacées et remplacées par celles
        contenues dans le fichier fourni.
    </p>
    <p class="help">
        Une sauvegarde des données courantes sera effectuée avant le remplacement,
        en cas de besoin d'annuler cette restauration.
    </p>
    <p>
        {csrf_field key="backup_restore"}
        <input type="hidden" name="MAX_FILE_SIZE" value="{$max_file_size}" />
        <input type="file" name="file" id="f_file" required="required" />
        (maximum {$max_file_size|format_bytes})


        <input type="submit" name="restore_file" value="Restaurer depuis le fichier sélectionné &rarr;" />
    </p>
    {if $code && ($code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT)}
    <p>
        <label><input type="checkbox" name="force_import" value="1" /> Ignorer les erreurs, je sais ce que je fait</label>
    </p>
    {/if}
</fieldset>

</form>

{include file="admin/_foot.tpl"}









|




|



|
















|

|









|












>
>
|



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{include file="admin/_head.tpl" title="Sauvegarde et restauration" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="index"}

{form_errors}

{if $code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT}
    <p class="block alert">Pour passer outre, renvoyez le fichier en cochant la case «&nbsp;Ignorer les erreurs&nbsp;».
    Attention, si vous avez effectué des modifications dans la base de données, cela peut créer des bugs&nbsp;!</p>
{/if}

{if $ok}
    <p class="block confirm">
        {if $ok == 'restore'}La restauration a bien été effectuée. Si vous désirez revenir en arrière, vous pouvez utiliser la sauvegarde automatique nommée <em>{$now_date}.avant_restauration.sqlite</em>, sinon vous pouvez l'effacer.
            {if $ok_code & Sauvegarde::NOT_AN_ADMIN}
            </p>
            <p class="block alert">
                <strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Garradin a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter.
                Merci de corriger les droits des catégories maintenant.
            {/if}
        {elseif $ok == 'remove'}La sauvegarde a été supprimée.
        {/if}
    </p>
{/if}


<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Téléchargement d'une sauvegarde</legend>
	<p class="help">
		Info : la base de données fait actuellement {$db_size|format_bytes} (dont {$files_size|format_bytes} pour les documents et images).
	</p>
    <p class="submit">
        {csrf_field key="backup_download"}
        {button type="submit" name="download" label="Télécharger une copie de la base de données sur mon ordinateur" shape="download" class="main"}
    </p>
</fieldset>

</form>

<form method="post" action="{$self_url_no_qs}" enctype="multipart/form-data">

<fieldset>
    <legend><label for="f_file">Restaurer depuis un fichier de sauvegarde</label></legend>
    <p class="block alert">
        Attention, l'intégralité des données courantes seront effacées et remplacées par celles
        contenues dans le fichier fourni.
    </p>
    <p class="help">
        Une sauvegarde des données courantes sera effectuée avant le remplacement,
        en cas de besoin d'annuler cette restauration.
    </p>
    <p>
        {csrf_field key="backup_restore"}
        <input type="hidden" name="MAX_FILE_SIZE" value="{$max_file_size}" />
        <input type="file" name="file" id="f_file" required="required" />
        (maximum {$max_file_size|format_bytes})
    </p>
    <p class="submit">
        {button type="submit" name="restore_file" label="Restaurer depuis le fichier sélectionné" shape="upload" class="main"}
    </p>
    {if $code && ($code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT)}
    <p>
        {input type="checkbox" name="force_import" value="1" label="Ignorer les erreurs, je sais ce que je fait"}
    </p>
    {/if}
</fieldset>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/donnees/local.tpl from [4984d48811] to [b1089b8af9].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{include file="admin/_head.tpl" title="Gestion des sauvegardes" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="local"}

{form_errors}

{if $ok}
    <p class="confirm">
        {if $ok == 'create'}Une nouvelle sauvegarde a été créée.
        {elseif $ok == 'restore'}La restauration a bien été effectuée. Si vous désirez revenir en arrière, vous pouvez utiliser la sauvegarde automatique nommée <em>date-du-jour.avant_restauration.sqlite</em>, sinon vous pouvez l'effacer.
            {if $ok_code & Sauvegarde::NOT_AN_ADMIN}
            </p>
            <p class="alert">
                <strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Garradin a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter.
                Merci de corriger les droits des catégories maintenant.
            {/if}
        {elseif $ok == 'remove'}La sauvegarde a été supprimée.
        {/if}
    </p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Copies de sauvegarde disponibles</legend>
    {if empty($liste)}
        <p class="help">Aucune copie de sauvegarde disponible.</p>
    {else}
        <dl>
            <dt><label for="f_select">Sélectionner une sauvegarde</label></dt>
            <dd>
                <select name="file" id="f_select">
                {foreach from=$liste key="f" item="d"}
                    <option value="{$f}">{$f} — {$d|date_fr:'d/m/Y à H:i'}</option>
                {/foreach}
                </select>
            </dd>
            <dd class="help">
                Attention, en cas de restauration, l'intégralité des données courantes seront effacées et remplacées par celles contenues dans la sauvegarde sélectionnée. Cependant, afin de prévenir toute erreur
                une sauvegarde des données sera réalisée avant la restauration.
            </dd>
        </dl>
        <p>
            {csrf_field key="backup_manage"}
            <input type="submit" name="restore" value="Restaurer cette sauvegarde" />
            <input type="submit" name="remove" value="Supprimer cette sauvegarde" />
        </p>
    {/if}
</fieldset>

</form>

<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Sauvegarde manuelle</legend>
    <p>
        {csrf_field key="backup_create"}
        <input type="submit" name="create" value="Créer une nouvelle sauvegarde des données &rarr;" />
    </p>
</fieldset>

</form>

{include file="admin/_foot.tpl"}









|




|




















|










|
|










|

|






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{include file="admin/_head.tpl" title="Gestion des sauvegardes" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="local"}

{form_errors}

{if $ok}
    <p class="block confirm">
        {if $ok == 'create'}Une nouvelle sauvegarde a été créée.
        {elseif $ok == 'restore'}La restauration a bien été effectuée. Si vous désirez revenir en arrière, vous pouvez utiliser la sauvegarde automatique nommée <em>date-du-jour.avant_restauration.sqlite</em>, sinon vous pouvez l'effacer.
            {if $ok_code & Sauvegarde::NOT_AN_ADMIN}
            </p>
            <p class="block alert">
                <strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Garradin a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter.
                Merci de corriger les droits des catégories maintenant.
            {/if}
        {elseif $ok == 'remove'}La sauvegarde a été supprimée.
        {/if}
    </p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Copies de sauvegarde disponibles</legend>
    {if empty($liste)}
        <p class="help">Aucune copie de sauvegarde disponible.</p>
    {else}
        <dl>
            <dt><label for="f_select">Sélectionner une sauvegarde</label></dt>
            <dd>
                <select name="file" id="f_select">
                {foreach from=$liste key="f" item="d"}
                    <option value="{$f}">{$f} — {$d|date_long}</option>
                {/foreach}
                </select>
            </dd>
            <dd class="help">
                Attention, en cas de restauration, l'intégralité des données courantes seront effacées et remplacées par celles contenues dans la sauvegarde sélectionnée. Cependant, afin de prévenir toute erreur
                une sauvegarde des données sera réalisée avant la restauration.
            </dd>
        </dl>
        <p>
            {csrf_field key="backup_manage"}
            {button type="submit" name="restore" label="Restaurer cette sauvegarde" shape="reset"}
            {button type="submit" name="delete" label="Supprimer cette sauvegarde" shape="delete"}
        </p>
    {/if}
</fieldset>

</form>

<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Sauvegarde manuelle</legend>
    <p class="submit">
        {csrf_field key="backup_create"}
        {button type="submit" name="create" label="Créer une nouvelle sauvegarde des données" shape="right" class="main"}
    </p>
</fieldset>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/donnees/reset.tpl from [b44198b72b] to [c696026876].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Remise à zéro" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="reset"}

{form_errors}

{if $ok !== null}
    <p class="confirm">La remise à zéro a été effectuée. Une sauvegarde a également été créée.</p>
    </p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Remise à zéro</legend>
	<p class="error">
		Attention : toutes les données seront effacées&nbsp;! Ceci inclut les membres, les opérations comptables, les pages du wiki, etc.
        Seul votre compte membre sera re-créé avec le même email et mot de passe.
	</p>
    <p class="help">
        Une sauvegarde sera automatiquement créée avant de procéder à la remise à zéro.
    </p>
    <dl>
        <dt><label for="f_passe_verif">Votre mot de passe</label> (pour vérification)</dt>









|







|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Remise à zéro" current="config"}

{include file="admin/config/_menu.tpl" current="donnees"}

{include file="admin/config/donnees/_menu.tpl" current="reset"}

{form_errors}

{if $ok !== null}
    <p class="block confirm">La remise à zéro a été effectuée. Une sauvegarde a également été créée.</p>
    </p>
{/if}

<form method="post" action="{$self_url_no_qs}">

<fieldset>
    <legend>Remise à zéro</legend>
	<p class="block error">
		Attention : toutes les données seront effacées&nbsp;! Ceci inclut les membres, les écritures comptables, les pages du wiki, etc.
        Seul votre compte membre sera re-créé avec le même email et mot de passe.
	</p>
    <p class="help">
        Une sauvegarde sera automatiquement créée avant de procéder à la remise à zéro.
    </p>
    <dl>
        <dt><label for="f_passe_verif">Votre mot de passe</label> (pour vérification)</dt>

Modified src/templates/admin/config/index.tpl from [6189dd4b55] to [8115cb5714].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19







20
21
22
23
24
25
26

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{include file="admin/_head.tpl" title="Configuration" current="config"}

{include file="admin/config/_menu.tpl" current="index"}

{if $ok && !$form->hasErrors()}
    <p class="confirm">
        La configuration a bien été enregistrée.
    </p>
{/if}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Garradin</legend>
        <dl>
            <dt>Version installée</dt>
            <dd class="help">{$garradin_version} <a href="{$garradin_website}">[Vérifier la disponibilité d'une nouvelle version]</a></dd>







            <dt>Informations système</dt>
            <dd class="help">
                Version PHP&nbsp;: {$php_version}<br />
                Version SQLite&nbsp;: {$sqlite_version}<br />
                Heure du serveur&nbsp;: {$server_time|date_fr}<br />
                Chiffrement GnuPG&nbsp;: {if $has_gpg_support}disponible, module activé{else}non, module PHP gnupg non installé&nbsp;?{/if}<br />
            </dd>

        </dl>
    </fieldset>

    <fieldset>
        <legend>Informations sur l'association</legend>
        <dl>
            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field data=$config name=nom_asso}" /></dd>
            <dt><label for="f_email_asso">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="email" name="email_asso" id="f_email_asso" required="required" value="{form_field data=$config name=email_asso}" /></dd>
            <dt><label for="f_adresse_asso">Adresse postale</label></dt>
            <dd><textarea cols="50" rows="5" name="adresse_asso" id="f_adresse_asso">{form_field data=$config name=adresse_asso}</textarea></dd>
            <dt><label for="f_site_asso">Site web</label></dt>
            <dd><input type="url" name="site_asso" id="f_site_asso" value="{form_field name=site_asso data=$config}" /></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Localisation</legend>





|












|
>
>
>
>
>
>
>







>











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{include file="admin/_head.tpl" title="Configuration" current="config"}

{include file="admin/config/_menu.tpl" current="index"}

{if $ok && !$form->hasErrors()}
    <p class="block confirm">
        La configuration a bien été enregistrée.
    </p>
{/if}

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Garradin</legend>
        <dl>
            <dt>Version installée</dt>
            <dd class="help">{$garradin_version}</dd>
            {if $new_version}
            <dd><p class="block alert">
                Une nouvelle version <strong>{$new_version}</strong> est disponible !<br />
                <a href="{$garradin_website}" target="_blank">Aller télécharger la nouvelle version</a>
            </p></dd>
            {/if}
            {if ENABLE_TECH_DETAILS}
            <dt>Informations système</dt>
            <dd class="help">
                Version PHP&nbsp;: {$php_version}<br />
                Version SQLite&nbsp;: {$sqlite_version}<br />
                Heure du serveur&nbsp;: {$server_time|date_fr}<br />
                Chiffrement GnuPG&nbsp;: {if $has_gpg_support}disponible, module activé{else}non, module PHP gnupg non installé&nbsp;?{/if}<br />
            </dd>
            {/if}
        </dl>
    </fieldset>

    <fieldset>
        <legend>Informations sur l'association</legend>
        <dl>
            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field data=$config name=nom_asso}" /></dd>
            <dt><label for="f_email_asso">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="email" name="email_asso" id="f_email_asso" required="required" value="{form_field data=$config name=email_asso}" /></dd>
            <dt><label for="f_adresse_asso">Adresse postale</label></dt>
            <dd><textarea cols="30" rows="5" name="adresse_asso" id="f_adresse_asso">{form_field data=$config name=adresse_asso}</textarea></dd>
            <dt><label for="f_site_asso">Site web</label></dt>
            <dd><input type="url" name="site_asso" id="f_site_asso" value="{form_field name=site_asso data=$config}" /></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Localisation</legend>
103
104
105
106
107
108
109
110
111
112
113

114
115
116
117
118
119
120
121
122
123
124
125

        </dl>
    </fieldset>

    <fieldset id="couleurs">
        <legend>Personnalisation de l'interface</legend>
        <dl>
            <dt><label for="f_couleur1">Couleur principale</label></dt>
            <dd><input type="color" pattern="#[a-f0-9]{ldelim}6{rdelim}" title="Couleur au format hexadécimal" placeholder="{$couleurs_defaut[0]}" name="couleur1" value="{form_field name=couleur1 default=$couleur1}" id="f_couleur1" /></dd>
            <dt><label for="f_couleur2">Couleur secondaire</label></dt>
            <dd><input type="color" pattern="#[a-f0-9]{ldelim}6{rdelim}" title="Couleur au format hexadécimal" placeholder="{$couleurs_defaut[1]}" name="couleur2" value="{form_field name=couleur2 default=$couleur2}" id="f_couleur2" /></dd>

        </dl>
        <input type="hidden" name="image_fond" id="f_image_fond" data-source="{$background_image_source}" value="{form_field name=image_fond}" />
    </fieldset>

    <p class="submit">
        {csrf_field key="config"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}







<
|
<
|
>

|




|





111
112
113
114
115
116
117

118

119
120
121
122
123
124
125
126
127
128
129
130
131
132

        </dl>
    </fieldset>

    <fieldset id="couleurs">
        <legend>Personnalisation de l'interface</legend>
        <dl>

            {input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$couleur1 name="couleur1" label="Couleur primaire" placeholder=$couleurs_defaut[0]}

            {input type="color" pattern="#[a-f0-9]{6}" title="Couleur au format hexadécimal" default=$couleur2 name="couleur2" label="Couleur secondaire" placeholder=$couleurs_defaut[1]}
            {input type="file" label="Image de fond" name="background" help="Il est conseillé d'utiliser une image en noir et blanc avec un fond blanc pour un meilleur rendu. Dimensions recommandées : 380x200" accept="image/*,*.jpeg,*.jpg,*.png,*.gif"}
        </dl>
        <input type="hidden" name="image_fond" id="f_image_fond" data-source="{$background_image_source}" data-default="{$background_image_default}" value="{form_field name=image_fond}" />
    </fieldset>

    <p class="submit">
        {csrf_field key="config"}
        {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/logs.tpl from [c62511fc2c] to [3872ee463d].

1
2
3
4
5

6
7
8
9

10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Journaux" current="config" custom_css=["styles/config.css"]}

{include file="admin/config/_menu.tpl" current="logs"}

{if ERRORS_ENABLE_LOG_VIEW}

<ul class="actions sub">
	<li{if $type != 'errors'} class="current"{/if}><a href="{$self_url_no_qs}">Actions utilisateurs</a></li>
	<li{if $type == 'errors'} class="current"{/if}><a href="?type=errors">Erreurs système</a></li>
</ul>

{/if}

{if isset($reports) && isset($id)}
	<section class="error">
		{foreach from=$main.errors item="error"}
			<h2 class="ruler">{$error.type}: {$error.message} [Code: {$error.errorCode}]</h2>
			{if !empty($error.backtrace)}




|
>
|
|
|
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{include file="admin/_head.tpl" title="Journaux" current="config" custom_css=["styles/config.css"]}

{include file="admin/config/_menu.tpl" current="logs"}

{if ENABLE_TECH_DETAILS}
<nav class="tabs">
	<ul class="sub">
		{*<li{if $type != 'errors'} class="current"{/if}><a href="{$self_url_no_qs}">Actions utilisateurs</a></li>*}
		<li{if $type == 'errors'} class="current"{/if}><a href="?type=errors">Erreurs système</a></li>
	</ul>
</nav>
{/if}

{if isset($reports) && isset($id)}
	<section class="error">
		{foreach from=$main.errors item="error"}
			<h2 class="ruler">{$error.type}: {$error.message} [Code: {$error.errorCode}]</h2>
			{if !empty($error.backtrace)}
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
{elseif isset($errors)}
	<p class="help">
		Liste des erreurs système et de code rencontrées par Garradin.
		Cliquer sur un des bugs pour le rapporter aux développeur⋅euses de Garradin.
	</p>

	{if !count($errors)}
		<p class="alert">Aucune erreur n'a été trouvée dans le journal error.log</p>
	{else}
		<table class="list">
			<thead>
				<tr>
					<th>Réf.</th>
					<td>Erreur</td>
					<td>Occurences</td>







|







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
{elseif isset($errors)}
	<p class="help">
		Liste des erreurs système et de code rencontrées par Garradin.
		Cliquer sur un des bugs pour le rapporter aux développeur⋅euses de Garradin.
	</p>

	{if !count($errors)}
		<p class="block alert">Aucune erreur n'a été trouvée dans le journal error.log</p>
	{else}
		<table class="list">
			<thead>
				<tr>
					<th>Réf.</th>
					<td>Erreur</td>
					<td>Occurences</td>
79
80
81
82
83
84
85
86


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
					<th><a href="?type=errors&id={$ref}">{$ref}</a></th>
					<td>
						{$error.message}<br />
						<tt>{$error.source}</tt>
					</td>
					<td>{$error.count}</td>
					<td>{$error.last_seen|date_fr}</td>
					<td class="actions"><a title="Voir les détails" class="icn" href="?type=errors&id={$ref}">𝍢</a></td>


				</tr>
				{/foreach}
			</tbody>
		</table>
	{/if}
{else}
	<p class="help">
		Cette page permet de suivre les actions effectuées par les utilisateurs.
	</p>

	{if empty($list)}
		<p class="alert">
			Aucune entrée dans le journal d'actions.
		</p>
	{/if}
{/if}

{include file="admin/_foot.tpl"}







|
>
>











|






81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
					<th><a href="?type=errors&id={$ref}">{$ref}</a></th>
					<td>
						{$error.message}<br />
						<tt>{$error.source}</tt>
					</td>
					<td>{$error.count}</td>
					<td>{$error.last_seen|date_fr}</td>
					<td class="actions">
						{linkbutton shape="menu" label="Voir les détails" href="%s?type=errors&id=%s"|args:$self_url_no_qs,$ref}
					</td>
				</tr>
				{/foreach}
			</tbody>
		</table>
	{/if}
{else}
	<p class="help">
		Cette page permet de suivre les actions effectuées par les utilisateurs.
	</p>

	{if empty($list)}
		<p class="block alert">
			Aucune entrée dans le journal d'actions.
		</p>
	{/if}
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/membres.tpl from [b07fc3c940] to [bbe25e6502].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{include file="admin/_head.tpl" current="config" js=1}

{include file="admin/config/_menu.tpl" current="fiches_membres"}

{if isset($status) && $status == 'OK'}
    <p class="confirm">
        La configuration a bien été enregistrée.
    </p>
{elseif isset($status) && $status == 'ADDED'}
    <p class="alert">
        Le champ a été ajouté à la fin de la liste. Pour sauvegarder les modifications de la fiche membre cliquer sur le bouton «&nbsp;Enregistrer&nbsp;» en base de page.
    </p>
{/if}

{form_errors}

{if $review}
    <p class="help">
        Voici ce à quoi ressemblera la nouvelle fiche de membre, vérifiez vos modifications avant d'enregistrer les changements.
    </p>
    <p class="alert">
        Attention&nbsp;! Si vous avez supprimé un champ, les données liées à celui-ci seront supprimées de toutes les fiches de tous les membres.
    </p>
    <fieldset>
        <legend>Fiche membre exemple</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {if $nom == 'passe'}{continue}{/if}
|




|



|










|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{include file="admin/_head.tpl" current="config" custom_css=['styles/config.css']}

{include file="admin/config/_menu.tpl" current="fiches_membres"}

{if isset($status) && $status == 'OK'}
    <p class="block confirm">
        La configuration a bien été enregistrée.
    </p>
{elseif isset($status) && $status == 'ADDED'}
    <p class="block alert">
        Le champ a été ajouté à la fin de la liste. Pour sauvegarder les modifications de la fiche membre cliquer sur le bouton «&nbsp;Enregistrer&nbsp;» en base de page.
    </p>
{/if}

{form_errors}

{if $review}
    <p class="help">
        Voici ce à quoi ressemblera la nouvelle fiche de membre, vérifiez vos modifications avant d'enregistrer les changements.
    </p>
    <p class="block alert">
        Attention&nbsp;! Si vous avez supprimé un champ, les données liées à celui-ci seront supprimées de toutes les fiches de tous les membres.
    </p>
    <fieldset>
        <legend>Fiche membre exemple</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {if $nom == 'passe'}{continue}{/if}
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188

    <form method="post" action="{$admin_url}config/membres.php">
        <p class="submit">
            {csrf_field key="config_membres"}
            <input type="hidden" name="champs" value="{$champs|escape:json|escape}" />
            <input type="submit" name="back" value="&larr; Retour à l'édition" class="minor" />
            <input type="submit" name="reset" value="Annuler les changements" class="minor" />
            <input type="submit" name="save" value="Enregistrer &rarr;" />
        </p>
    </form>
{else}
    <p class="help">
        Cette page vous permet de personnaliser les fiches d'information des membres de l'association.<br />
        <strong>Attention :</strong> Les champs supprimés de la fiche seront effacés de toutes les fiches de tous les membres, et les données qu'ils contenaient seront perdues.
    </p>

    {if !empty($presets)}
    <form method="post" action="{$self_url}">
    <fieldset>
        <legend>Ajouter un champ pré-défini</legend>
        <p>
            <select name="preset" required="required">
                <option></option>
                {foreach from=$presets key="name" item="preset"}
                <option value="{$name}">{$name} &mdash; {$preset.title}</option>
                {/foreach}
            </select>
            {csrf_field key="config_membres"}
            <input type="submit" name="add" value="Ajouter ce champ à la fiche membre" />
        </p>
    </fieldset>
    </form>
    {/if}

<form method="post" action="{$self_url}">
    <fieldset>
        <legend>Ajouter un champ personnalisé</legend>
        <dl>
            <dt><label for="f_name">Nom unique</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">Ne peut comporter que des lettres minuscules et des tirets bas.</dd>
            <dd><input type="text" name="new" id="f_name" value="{form_field name=new}" size="30" required="required" /></dd>
            <dt><label for="f_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="new_title" id="f_title" value="{form_field name=new_title}" size="60" required="required" /></dd>
            <dt><label for="f_type">Type de champ</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="new_type" id="f_type" required="required">
                    {foreach from=$types key="type" item="nom"}
                    <option value="{$type}" {form_field name=new_type selected=$type}>{$nom}</option>
                    {/foreach}
                </select>
            </dd>
        </dl>
        <p>
            {csrf_field key="config_membres"}
            <input type="submit" name="add" value="Ajouter ce champ à la fiche membre" />
            <input type="hidden" name="champs" value="{$champs|escape:json|escape}" />
        </p>
    </fieldset>
</form>

<form method="post" action="{$self_url}">
    <div id="orderFields">
        {foreach from=$champs item="champ" key="nom"}
        {if $nom == 'passe'}{continue}{/if}
        <fieldset id="f_{$nom}">
            <legend>{$nom}</legend>
            <dl>
                <dt><label>Type</label></dt>
                <dd><input type="hidden" name="champs[{$nom}][type]" value="{$champ.type}" />{$champ.type|get_type}</dd>
                <dt><label for="f_{$nom}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="text" name="champs[{$nom}][title]" id="f_{$nom}_title" value="{form_field data=$champs->$nom name=title}" size="60" required="required" /></dd>
                <dt><label for="f_{$nom}_help">Aide</label></dt>
                <dd><input type="text" name="champs[{$nom}][help]" id="f_{$nom}_help" value="{form_field data=$champs->$nom name=help}" size="100" /></dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][private]" value="1" {form_field data=$champs->$nom name=private checked="1"} /> Caché pour les membres</label></dt>
                <dd class="help">Si coché, ce champ ne sera pas visible par les membres dans leur espace personnel.</dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][editable]" value="1" {form_field data=$champs->$nom name=editable checked="1"} /> Modifiable par les membres</label></dt>
                <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][mandatory]" value="1" {form_field data=$champs->$nom name=mandatory checked="1"} /> Champ obligatoire</label></dt>
                <dd class="help">Si coché, ce champ ne pourra rester vide.</dd>
                {if $champ.type == 'select' || $champ.type == 'multiple'}
                    <dt><label>Options disponibles</label></dt>
                    {if $champ.type == 'multiple'}
                        <dd class="help">Attention changer l'ordre des options peut avoir des effets indésirables.</dd>
                    {else}
                        <dd class="help">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</dd>
                    {/if}
                    <dd>
                        <{if $champ.type == 'multiple'}ol{else}ul{/if} class="options">
                        {if !empty($champ.options)}
                            {foreach from=$champ.options key="key" item="opt"}
                                <li><input type="text" name="champs[{$nom}][options][]" value="{$opt}" size="50" /></li>
                            {/foreach}
                        {/if}
                        {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32}
                            <li><input type="text" name="champs[{$nom}][options][]" value="" size="50" /></li>
                        {/if}
                    </dd>
                {/if}
                <dt><label for="f_{$nom}_list_row">Numéro de colonne dans la liste des membres</label></dt>
                <dd class="help">Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.</dd>
                <dd><input type="number" id="f_{$nom}_list_row" name="champs[{$nom}][list_row]" min="0" max="10" value="{form_field data=$champs->$nom name=list_row}" /></dd>
            </dl>
        </fieldset>
        {/foreach}
    </div>

    <fieldset id="f_passe">
        <legend>Mot de passe</legend>
        <dl>
            <dt><label><input type="checkbox" name="champs[passe][private]" value="1" {form_field data=$champs.passe name=private checked="1"} /> Caché pour les membres</label></dt>
            <dd class="help">Si coché, ce champ ne sera pas visible par les membres dans leur espace personnel.</dd>
            <dt><label><input type="checkbox" name="champs[passe][editable]" value="1" {form_field data=$champs.passe name=editable checked="1"} /> Modifiable par les membres</label></dt>
            <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd>
            <dt><label><input type="checkbox" name="champs[passe][mandatory]" value="1" {form_field data=$champs.passe name=mandatory checked="1"} /> Champ obligatoire</label></dt>
            <dd class="help">Si coché, ce champ ne pourra rester vide.</dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="config_membres"}
        <input type="submit" name="reset" value="Annuler les changements" class="minor" />
        <input type="submit" name="review" value="Enregistrer &rarr;" />
        (un récapitulatif sera présenté et une confirmation sera demandée)
    </p>
</form>

<script type="text/javascript">
var champ_identifiant = "f_{$config.champ_identifiant|escape:'js'}";
var champ_identite = "f_{$config.champ_identite|escape:'js'}";








|




















|











|

|











|















|

|
|
|
<
|
<
|











|



|














<
|
<
|
|
<





|
|
|







59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

134

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

166

167
168

169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

    <form method="post" action="{$admin_url}config/membres.php">
        <p class="submit">
            {csrf_field key="config_membres"}
            <input type="hidden" name="champs" value="{$champs|escape:json|escape}" />
            <input type="submit" name="back" value="&larr; Retour à l'édition" class="minor" />
            <input type="submit" name="reset" value="Annuler les changements" class="minor" />
            {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
        </p>
    </form>
{else}
    <p class="help">
        Cette page vous permet de personnaliser les fiches d'information des membres de l'association.<br />
        <strong>Attention :</strong> Les champs supprimés de la fiche seront effacés de toutes les fiches de tous les membres, et les données qu'ils contenaient seront perdues.
    </p>

    {if !empty($presets)}
    <form method="post" action="{$self_url}">
    <fieldset>
        <legend>Ajouter un champ pré-défini</legend>
        <p>
            <select name="preset" required="required">
                <option></option>
                {foreach from=$presets key="name" item="preset"}
                <option value="{$name}">{$name} &mdash; {$preset.title}</option>
                {/foreach}
            </select>
            {csrf_field key="config_membres"}
            {button type="submit" name="add" label="Ajouter ce champ à la fiche membre" shape="plus"}
        </p>
    </fieldset>
    </form>
    {/if}

<form method="post" action="{$self_url}">
    <fieldset>
        <legend>Ajouter un champ personnalisé</legend>
        <dl>
            <dt><label for="f_name">Nom unique</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">Ne peut comporter que des lettres minuscules et des tirets bas.</dd>
            <dd><input type="text" name="new" id="f_name" value="{form_field name=new}" required="required" /></dd>
            <dt><label for="f_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="new_title" id="f_title" value="{form_field name=new_title}" required="required" /></dd>
            <dt><label for="f_type">Type de champ</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="new_type" id="f_type" required="required">
                    {foreach from=$types key="type" item="nom"}
                    <option value="{$type}" {form_field name=new_type selected=$type}>{$nom}</option>
                    {/foreach}
                </select>
            </dd>
        </dl>
        <p>
            {csrf_field key="config_membres"}
            {button type="submit" name="add" label="Ajouter ce champ à la fiche membre" shape="plus"}
            <input type="hidden" name="champs" value="{$champs|escape:json|escape}" />
        </p>
    </fieldset>
</form>

<form method="post" action="{$self_url}">
    <div id="orderFields">
        {foreach from=$champs item="champ" key="nom"}
        {if $nom == 'passe'}{continue}{/if}
        <fieldset id="f_{$nom}">
            <legend>{$nom}</legend>
            <dl>
                <dt><label>Type</label></dt>
                <dd><input type="hidden" name="champs[{$nom}][type]" value="{$champ.type}" />{$champ.type|get_type}</dd>
                <dt><label for="f_{$nom}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="text" name="champs[{$nom}][title]" id="f_{$nom}_title" value="{form_field data=$champs->$nom name=title}" required="required" /></dd>
                <dt><label for="f_{$nom}_help">Aide</label></dt>
                <dd><input type="text" name="champs[{$nom}][help]" id="f_{$nom}_help" value="{form_field data=$champs->$nom name=help}" /></dd>
                <dt>Prérences</dt>
                {input type="checkbox" name="champs[%s][private]"|args:$nom value="1" default=$champs->$nom->private label="Caché pour les membres" help="Si coché, ce champ ne sera pas visible par les membres dans leur espace personnel"}

                {input type="checkbox" name="champs[%s][editable]"|args:$nom value="1" default=$champs->$nom->editable label="Modifiable par les membres" help="Si coché, les membres pourront changer cette information depuis leur espace personnel"}

                {input type="checkbox" name="champs[%s][mandatory]"|args:$nom value="1" default=$champs->$nom->mandatory label="Champ obligatoire" help="Si coché, ce champ ne pourra rester vide"}
                {if $champ.type == 'select' || $champ.type == 'multiple'}
                    <dt><label>Options disponibles</label></dt>
                    {if $champ.type == 'multiple'}
                        <dd class="help">Attention changer l'ordre des options peut avoir des effets indésirables.</dd>
                    {else}
                        <dd class="help">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</dd>
                    {/if}
                    <dd>
                        <{if $champ.type == 'multiple'}ol{else}ul{/if} class="options">
                        {if !empty($champ.options)}
                            {foreach from=$champ.options key="key" item="opt"}
                                <li><input type="text" name="champs[{$nom}][options][]" value="{$opt}" /></li>
                            {/foreach}
                        {/if}
                        {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32}
                            <li><input type="text" name="champs[{$nom}][options][]" value="" /></li>
                        {/if}
                    </dd>
                {/if}
                <dt><label for="f_{$nom}_list_row">Numéro de colonne dans la liste des membres</label></dt>
                <dd class="help">Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.</dd>
                <dd><input type="number" id="f_{$nom}_list_row" name="champs[{$nom}][list_row]" min="0" max="10" value="{form_field data=$champs->$nom name=list_row}" /></dd>
            </dl>
        </fieldset>
        {/foreach}
    </div>

    <fieldset id="f_passe">
        <legend>Mot de passe</legend>
        <dl>

            {input type="checkbox" name="champs[passe][private]" value="1" default=$champs.passe.private label="Caché pour les membres" help="Si coché, ce champ ne sera pas visible par les membres dans leur espace personnel"}

            {input type="checkbox" name="champs[passe][editable]" value="1" default=$champs.passe.editable label="Modifiable par les membres" help="Si coché, les membres pourront changer cette information depuis leur espace personnel"}
            {input type="checkbox" name="champs[passe][mandatory]" value="1" default=$champs.passe.mandatory label="Champ obligatoire" help="Si coché, ce champ ne pourra rester vide lors de la création d'un membre"}

        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="config_membres"}
        {button type="submit" name="reset" label="Annuler les changements" shape="left"}
        {button type="submit" name="review" label="Vérifier les changements" shape="right" class="main"}
        <em class="help">(un récapitulatif sera présenté et une confirmation sera demandée)</em>
    </p>
</form>

<script type="text/javascript">
var champ_identifiant = "f_{$config.champ_identifiant|escape:'js'}";
var champ_identite = "f_{$config.champ_identite|escape:'js'}";

Modified src/templates/admin/config/plugins.tpl from [d99e6b054a] to [87c21ad0af].

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Désinstaller une extension</legend>
            <h3 class="warning">
                Êtes-vous sûr de vouloir supprimer l'extension «&nbsp;{$plugin.nom}&nbsp;» ?
            </h3>
            <p class="alert">
                <strong>Attention</strong> : cette action est irréversible et effacera toutes les
                données associées à l'extension.
            </p>
        </fieldset>

        <p class="submit">
            {csrf_field key="delete_plugin_%s"|args:$plugin.id}
            <input type="submit" name="delete" value="Désinstaller &rarr;" />
        </p>
    </form>
{else}
    {if !empty($liste_installes)}
        <table class="list">
            <thead>
                <tr>







|







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Désinstaller une extension</legend>
            <h3 class="warning">
                Êtes-vous sûr de vouloir supprimer l'extension «&nbsp;{$plugin.nom}&nbsp;» ?
            </h3>
            <p class="block alert">
                <strong>Attention</strong> : cette action est irréversible et effacera toutes les
                données associées à l'extension.
            </p>
        </fieldset>

        <p class="submit">
            {csrf_field key="delete_plugin_%s"|args:$plugin.id}
            {button type="submit" name="delete" label="Désinstaller" shape="delete" class="main"}
        </p>
    </form>
{else}
    {if !empty($liste_installes)}
        <table class="list">
            <thead>
                <tr>
79
80
81
82
83
84
85
86
87

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Extensions à installer</legend>
            <dl>
                {foreach from=$liste_telecharges item="plugin" key="id"}
                <dt>
                    <label>
                        <input type="radio" name="plugin" value="{$id}" />

                        {$plugin.nom}
                    </label>
                    (version {$plugin.version})
                </dt>
                <dd>[<a href="{$plugin.url}" onclick="return !window.open(this.href);">{$plugin.auteur}</a>] {$plugin.description}</dd>
                {/foreach}
            </dl>
        </fieldset>

        <p class="help">
            Attention : installer une extension non officielle peut présenter des risques de sécurité
            et de stabilité.
        </p>

        <p class="submit">
            {csrf_field key="install_plugin"}
            <input type="submit" name="install" value="Installer &rarr;" />
        </p>
    </form>
    {/if}
{/if}

{include file="admin/_foot.tpl"}







<
|
>
















|






79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Extensions à installer</legend>
            <dl>
                {foreach from=$liste_telecharges item="plugin" key="id"}
                <dt>

                    <input type="radio" name="plugin" value="{$id}" id="f_{$id}" />
                    <label for="f_{$id}">
                        {$plugin.nom}
                    </label>
                    (version {$plugin.version})
                </dt>
                <dd>[<a href="{$plugin.url}" onclick="return !window.open(this.href);">{$plugin.auteur}</a>] {$plugin.description}</dd>
                {/foreach}
            </dl>
        </fieldset>

        <p class="help">
            Attention : installer une extension non officielle peut présenter des risques de sécurité
            et de stabilité.
        </p>

        <p class="submit">
            {csrf_field key="install_plugin"}
            {button type="submit" name="install" label="Installer" shape="right" class="main"}
        </p>
    </form>
    {/if}
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/config/site.tpl from [dda2316333] to [2454b9048e].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

55
56

57
58
59
60
61
62
63
{include file="admin/_head.tpl" title="Configuration — Site public" current="config" js=1}

{form_errors}

{include file="admin/config/_menu.tpl" current="site"}

{if $config.desactiver_site}
    <div class="alert">
        <h3>Site public désactivé</h3>
        <p>Le site public est désactivé, les visiteurs sont redirigés automatiquement vers la page de connexion.</p>
        <form method="post" action="{$self_url}">
            <p class="submit">
                {csrf_field key="config_site"}
                <input type="submit" name="activer_site" value="Réactiver le site public &rarr;" />
            </p>
        </form>
    </div>
{elseif isset($edit)}
    <form method="post" action="{$self_url}">
        <h3>Éditer un squelette</h3>

        {if $ok}
        <p class="confirm">
            Modifications enregistrées.
        </p>
        {/if}

        <fieldset class="skelEdit">
            <legend>{$edit.file}</legend>
            <p>
                <textarea name="content" cols="90" rows="50" id="f_content">{form_field name=content data=$edit}</textarea>
            </p>
        </fieldset>

        <p class="submit">
            {csrf_field key=$csrf_key}
            <input type="submit" name="save" value="Enregistrer &rarr;" />
        </p>

    </form>

    <script type="text/javascript">
    var doc_url = "{$admin_url}doc/skel/";
    var skel_list = {$sources|escape:json};
    var skel_current = "{$edit.file|escape:'js'}";
    g.script("scripts/skel_editor.js");
    </script>
{else}

    <fieldset>
        <legend>Activation du site public</legend>
        <dl>
            <dt>
                <form method="post" action="{$self_url}">

                <input type="submit" name="desactiver_site" value="Désactiver le site public" />
                {csrf_field key="config_site"}

                </form>
            </dt>
            <dd class="help">
                En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br />
                Cette option est utile si vous avez déjà un site web et ne souhaitez pas utiliser la fonctionnalité site web de Garradin.
            </dd>
        </dl>
|






|





|








|













|

















>
|
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
{include file="admin/_head.tpl" title="Configuration — Site public" current="config"}

{form_errors}

{include file="admin/config/_menu.tpl" current="site"}

{if $config.desactiver_site}
    <div class="block alert">
        <h3>Site public désactivé</h3>
        <p>Le site public est désactivé, les visiteurs sont redirigés automatiquement vers la page de connexion.</p>
        <form method="post" action="{$self_url}">
            <p class="submit">
                {csrf_field key="config_site"}
                {button type="submit" name="activer_site" label="Réactiver le site public" shape="right" class="main"}
            </p>
        </form>
    </div>
{elseif isset($edit)}
    <form method="post" action="{$self_url}">
        <h3>Éditer un squelette</h3>

        {if $ok}
        <p class="block confirm">
            Modifications enregistrées.
        </p>
        {/if}

        <fieldset class="skelEdit">
            <legend>{$edit.file}</legend>
            <p>
                <textarea name="content" cols="90" rows="50" id="f_content">{form_field name=content data=$edit}</textarea>
            </p>
        </fieldset>

        <p class="submit">
            {csrf_field key=$csrf_key}
            {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
        </p>

    </form>

    <script type="text/javascript">
    var doc_url = "{$admin_url}doc/skel/";
    var skel_list = {$sources|escape:json};
    var skel_current = "{$edit.file|escape:'js'}";
    g.script("scripts/skel_editor.js");
    </script>
{else}

    <fieldset>
        <legend>Activation du site public</legend>
        <dl>
            <dt>
                <form method="post" action="{$self_url}">
                    <p class="submit">
                        {button type="submit" name="desactiver_site" label="Désactiver le site public" shape="right" class="main"}
                        {csrf_field key="config_site"}
                    </p>
                </form>
            </dt>
            <dd class="help">
                En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br />
                Cette option est utile si vous avez déjà un site web et ne souhaitez pas utiliser la fonctionnalité site web de Garradin.
            </dd>
        </dl>
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
    </fieldset>

    <form method="post" action="{$self_url}">
    <fieldset class="templatesList">
        <legend>Squelettes du site</legend>

        {if $reset_ok}
        <p class="confirm">
            Réinitialisation effectuée. Les squelettes ont été remis à jour
        </p>
        {/if}

        <table class="list">
            <thead>
                <tr>
                    <td class="check"></td>
                    <th>Fichier</th>
                    <td>Dernière modification</td>
                    <td></td>
                </tr>
            </thead>
            <tbody>
            {foreach from=$sources key="source" item="local"}
                <tr>
                    <td>{if $local && $local.dist}<input type="checkbox" name="select[]" value="{$source}" />{/if}</td>
                    <th><a href="{$admin_url}config/site.php?edit={$source|escape:'url'}" title="Éditer">{$source}</a></th>
                    <td>{if $local}{$local.mtime|date_fr:'d/m/Y à H:i:s'}{else}<em>(fichier non modifié)</em>{/if}</td>
                    <td class="actions">
                        <a class="icn" href="{$admin_url}config/site.php?edit={$source|escape:'url'}" title="Éditer">✎</a>
                    </td>
                </tr>
            {/foreach}
            </tbody>
        </table>

        <p class="actions">







|
















|



|







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    </fieldset>

    <form method="post" action="{$self_url}">
    <fieldset class="templatesList">
        <legend>Squelettes du site</legend>

        {if $reset_ok}
        <p class="block confirm">
            Réinitialisation effectuée. Les squelettes ont été remis à jour
        </p>
        {/if}

        <table class="list">
            <thead>
                <tr>
                    <td class="check"></td>
                    <th>Fichier</th>
                    <td>Dernière modification</td>
                    <td></td>
                </tr>
            </thead>
            <tbody>
            {foreach from=$sources key="source" item="local"}
                <tr>
                    <td>{if $local && $local.dist}<input type="checkbox" name="select[]" value="{$source}" id="f_source_{$iteration}" /><label for="f_source_{$iteration}"></label>{/if}</td>
                    <th><a href="{$admin_url}config/site.php?edit={$source|escape:'url'}" title="Éditer">{$source}</a></th>
                    <td>{if $local}{$local.mtime|date_fr:'d/m/Y à H:i:s'}{else}<em>(fichier non modifié)</em>{/if}</td>
                    <td class="actions">
                        {linkbutton shape="edit" label="Éditer" href="?edit=%s"|args:$source}
                    </td>
                </tr>
            {/foreach}
            </tbody>
        </table>

        <p class="actions">

Modified src/templates/admin/index.tpl from [68896b2b83] to [8ad9228981].

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{include file="admin/_head.tpl" title="Bonjour %s !"|args:$user.identite current="home"}

{$banniere|raw}

<ul class="actions">

    <li><a href="{$admin_url}mes_infos.php">Modifier mes informations personnelles</a></li>
    {if $cotisation}
    <li>
        {if !$cotisation.a_jour}
            <b class="error">Cotisation en retard&nbsp;!</b>
        {else}
            <b class="confirm">Cotisation à jour</b>
            {if $cotisation.expiration}
                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
            {/if}
        {/if}
    </li>
    {/if}
    <li><a href="{$admin_url}mes_cotisations.php">Suivi de mes cotisations</a></li>
</ul>


<aside class="describe">
    <h3>{$config.nom_asso}</h3>
    {if !empty($config.adresse_asso)}
    <p>
        {$config.adresse_asso|escape|nl2br}
    </p>
    {/if}
    {if !empty($config.email_asso)}
    <p>
        E-Mail : <a href="mailto:{$config.email_asso}">{$config.email_asso}</a>
    </p>
    {/if}
    {if !empty($config.site_asso)}
    <p>
        Web : <a href="{$config.site_asso}">{$config.site_asso}</a>
    </p>
    {/if}
</aside>

<div class="wikiContent">
    {$page.contenu.contenu|raw|format_wiki|liens_wiki:'wiki/?'}
</div>

{include file="admin/_foot.tpl"}




|
>
|
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|















|









1
2
3
4
5
6
7












8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{include file="admin/_head.tpl" title="Bonjour %s !"|args:$user.identite current="home"}

{$banniere|raw}

<nav class="tabs">
    <ul>
        <li><a href="{$admin_url}mes_infos.php">Modifier mes informations personnelles</a></li>












        <li><a href="{$admin_url}my_services.php">Suivi de mes activités et cotisations</a></li>
    </ul>
</nav>

<aside class="describe">
    <h3>{$config.nom_asso}</h3>
    {if !empty($config.adresse_asso)}
    <p>
        {$config.adresse_asso|escape|nl2br}
    </p>
    {/if}
    {if !empty($config.email_asso)}
    <p>
        E-Mail : <a href="mailto:{$config.email_asso}">{$config.email_asso}</a>
    </p>
    {/if}
    {if !empty($config.site_asso)}
    <p>
        Web : <a href="{$config.site_asso}" target="_blank">{$config.site_asso}</a>
    </p>
    {/if}
</aside>

<div class="wikiContent">
    {$page.contenu.contenu|raw|format_wiki|liens_wiki:'wiki/?'}
</div>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/install.tpl from [dd863dea3a] to [e0d35a0a40].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="Garradin - Installation" js=1}

{if $disabled}
    <p class="error">Garradin est déjà installé.</p>
{else}
    <p class="intro">
        Bienvenue dans Garradin !
        Veuillez remplir les quelques informations suivantes pour terminer
        l'installation.
    </p>

    {form_errors}

    <form method="post" action="{$self_url}">

    <fieldset>
        <legend>Informations sur l'association</legend>
        <dl>
            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field name=nom_asso}" /></dd>
            <dt><label for="f_email_asso">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="email" name="email_asso" id="f_email_asso" required="required" value="{form_field name=email_asso}" /></dd>
            <dt><label for="f_adresse_asso">Adresse postale</label></dt>
            <dd><textarea cols="50" rows="5" name="adresse_asso" id="f_adresse_asso">{form_field name=adresse_asso}</textarea></dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Informations sur le premier membre</legend>
        <dl>
            <dt><label for="f_nom_membre">Nom et prénom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom_membre" id="f_nom_membre" required="required" value="{form_field name=nom_membre}" /></dd>
            <dt><label for="f_cat_membre">Catégorie du membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="tip">Par exemple : bureau, conseil d'administration, présidente, trésorier, etc.</dd>
            <dd><input type="text" name="cat_membre" id="f_cat_membre" required="required" value="{form_field name=cat_membre}" /></dd>
            <dt><label for="f_email_membre">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="email" name="email_membre" id="f_email_membre" required="required" value="{form_field name=email_membre}" /></dd>
            <dt><label for="f_passe_membre">Mot de passe</label> (minimum {$password_length} caractères) <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">
                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
            </dd>
            <dd class="help">
                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
            </dd>
            <dd><input type="password" name="passe" id="f_passe_membre" value="{form_field name=passe}" pattern="{$password_pattern}" required="required" autocomplete="off" /></dd>
            <dt><label for="f_repasse_membre">Encore le mot de passe</label> (vérification) <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="password" name="passe_confirmed" id="f_repasse_membre" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" required="required" autocomplete="off" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="install"}
        <input type="submit" id="f_submit" name="save" value="Terminer l'installation &rarr;" />
    </p>

    <script type="text/javascript" src="{$admin_url}static/scripts/loader.js"></script>

    <script type="text/javascript">
    {literal}
    g.script('scripts/password.js').onload = function () {
        initPasswordField('pw_suggest', 'f_passe_membre', 'f_repasse_membre');
    };
    
    var form = $('form')[0];
    form.onsubmit = function () {
        $('#f_submit').style.opacity = 0;
        var loader = document.createElement('div');
        loader.className = 'loader install';
        loader.innerHTML = '<b>Garradin est en cours d\'installation…</b>';
        $('#f_submit').parentNode.appendChild(loader);
|


|

|














<
<
<
<




|



<
<
<




|














|






|

|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20




21
22
23
24
25
26
27
28



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
{include file="admin/_head.tpl" title="Garradin - Installation"}

{if $disabled}
    <p class="block error">Garradin est déjà installé.</p>
{else}
    <p class="help">
        Bienvenue dans Garradin !
        Veuillez remplir les quelques informations suivantes pour terminer
        l'installation.
    </p>

    {form_errors}

    <form method="post" action="{$self_url}">

    <fieldset>
        <legend>Informations sur l'association</legend>
        <dl>
            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom_asso" id="f_nom_asso" required="required" value="{form_field name=nom_asso}" /></dd>




        </dl>
    </fieldset>

    <fieldset>
        <legend>Création du compte administrateur</legend>
        <dl>
            <dt><label for="f_nom_membre">Nom et prénom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="nom_membre" id="f_nom_membre" required="required" value="{form_field name=nom_membre}" /></dd>



            <dt><label for="f_email_membre">Adresse E-Mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="email" name="email_membre" id="f_email_membre" required="required" value="{form_field name=email_membre}" /></dd>
            <dt><label for="f_passe_membre">Mot de passe</label> (minimum {$password_length} caractères) <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">
                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr
                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
            </dd>
            <dd class="help">
                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
            </dd>
            <dd><input type="password" name="passe" id="f_passe_membre" value="{form_field name=passe}" pattern="{$password_pattern}" required="required" autocomplete="off" /></dd>
            <dt><label for="f_repasse_membre">Encore le mot de passe</label> (vérification) <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="password" name="passe_confirmed" id="f_repasse_membre" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" required="required" autocomplete="off" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="install"}
        {button type="submit" name="save" label="Terminer l'installation" shape="right" class="main"}
    </p>

    <script type="text/javascript" src="{$admin_url}static/scripts/loader.js"></script>

    <script type="text/javascript">
    {literal}
    g.script('scripts/password.js', () => {
        initPasswordField('pw_suggest', 'f_passe_membre', 'f_repasse_membre');
    });

    var form = $('form')[0];
    form.onsubmit = function () {
        $('#f_submit').style.opacity = 0;
        var loader = document.createElement('div');
        loader.className = 'loader install';
        loader.innerHTML = '<b>Garradin est en cours d\'installation…</b>';
        $('#f_submit').parentNode.appendChild(loader);

Modified src/templates/admin/login.tpl from [74da885549] to [2b0fb0f94e].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19





20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

58
59




60

61
62
{include file="admin/_head.tpl" title="Connexion" js=1}

{form_errors}
{show_error if=$fail message="Connexion impossible. Vérifiez l'adresse e-mail et le mot de passe."}

{if $changed}
    <p class="confirm">
        Votre mot de passe a bien été modifié.<br />
        Vous pouvez maintenant l'utiliser pour vous reconnecter.
    </p>
{/if}

{if !$ssl_enabled && $prefer_ssl}
    <p class="alert">
        <strong>Message de sécurité</strong><br />
        Nous vous conseillons de vous connecter sur la version <a href="{$own_https_url}">chiffrée (HTTPS) de cette page</a>
        pour vous connecter.
    </p>
{/if}






<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Connexion</legend>
        <dl>
            <dt><label for="f_id">{$champ.title}</label></dt>
            <dd><input type="text" name="_id" id="f_id" value="{form_field name=_id}" /></dd>
            <dt><label for="f_passe">Mot de passe</label></dt>
            <dd><input type="password" name="passe" id="f_passe" value="" autocomplete="current-password" />
                {if $ssl_enabled}
                    <b class="icn confirm" title="Connexion chiffrée">&#x1f512;</b>
                    <span class="confirm">Connexion sécurisée</span>
                {else}
                    <b class="icn error" title="Connexion non chiffrée">&#x1f513;</b>
                    {if $prefer_ssl}
                        <span class="error">Connexion non-sécurisée&nbsp;!</span>
                        <a href="{$own_https_url}">Se connecter en HTTPS (sécurisé)</a>
                    {else}
                        <span class="alert">Connexion non-sécurisée</span>
                    {/if}
                {/if}
            </dd>
            <dd><label><input type="checkbox" name="permanent" value="1" /> Rester connecté‑e</label></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="login"}
        <input type="submit" name="login" value="Se connecter &rarr;" />
    </p>

    <p class="help">
        <a href="{$admin_url}password.php">Pas de mot de passe ou mot de passe perdu ?</a>
    </p>

</form>


<script type="text/javascript">
g.enhancePasswordField($('#f_passe'));




</script>


{include file="admin/_foot.tpl"}
|


<


|






|





>
>
>
>
>






|


|













|





|








>


>
>
>
>

>


1
2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="Connexion"}

{form_errors}


{if $changed}
    <p class="block confirm">
        Votre mot de passe a bien été modifié.<br />
        Vous pouvez maintenant l'utiliser pour vous reconnecter.
    </p>
{/if}

{if !$ssl_enabled && $prefer_ssl}
    <p class="block alert">
        <strong>Message de sécurité</strong><br />
        Nous vous conseillons de vous connecter sur la version <a href="{$own_https_url}">chiffrée (HTTPS) de cette page</a>
        pour vous connecter.
    </p>
{/if}

<p class="block error" style="display: none;" id="old_browser">
    Le navigateur que vous utilisez n'est pas supporté. Des fonctionnalités peuvent ne pas fonctionner.<br />
    Merci d'utiliser un navigateur web moderne comme <a href="https://www.getfirefox.com/" target="_blank">Firefox</a> ou <a href="https://vivaldi.com/fr/" target="_blank">Vivaldi</a>.
</p>

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Connexion</legend>
        <dl>
            <dt><label for="f_id">{$id_field_name}</label></dt>
            <dd><input type="text" name="_id" id="f_id" value="{form_field name=_id}" /></dd>
            <dt><label for="f_passe">Mot de passe</label></dt>
            <dd><input type="password" name="password" id="f_passe" value="" autocomplete="current-password" />
                {if $ssl_enabled}
                    <b class="icn confirm" title="Connexion chiffrée">&#x1f512;</b>
                    <span class="confirm">Connexion sécurisée</span>
                {else}
                    <b class="icn error" title="Connexion non chiffrée">&#x1f513;</b>
                    {if $prefer_ssl}
                        <span class="error">Connexion non-sécurisée&nbsp;!</span>
                        <a href="{$own_https_url}">Se connecter en HTTPS (sécurisé)</a>
                    {else}
                        <span class="alert">Connexion non-sécurisée</span>
                    {/if}
                {/if}
            </dd>
            {input type="checkbox" name="permanent" value="1" label="Rester connecté‑e" help="recommandé seulement sur ordinateur personnel"}
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="login"}
        {button type="submit" name="login" label="Se connecter" shape="right" class="main"}
    </p>

    <p class="help">
        <a href="{$admin_url}password.php">Pas de mot de passe ou mot de passe perdu ?</a>
    </p>

</form>

{literal}
<script type="text/javascript">
g.enhancePasswordField($('#f_passe'));

if (!!window.document.documentMode) {
    document.getElementById('old_browser').style.display = 'block';
}
</script>
{/literal}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/login_otp.tpl from [6560c7df92] to [0b5ffa4503].

12
13
14
15
16
17
18
19
20
21
22
23
24
            <dd class="help">Entrez ici le code donné par l'application d'authentification double facteur.</dd>
            <dd><input type="text" name="code" id="f_code" value="{form_field name=code}" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="otp"}
        <input type="submit" name="login" value="Se connecter &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}







|





12
13
14
15
16
17
18
19
20
21
22
23
24
            <dd class="help">Entrez ici le code donné par l'application d'authentification double facteur.</dd>
            <dd><input type="text" name="code" id="f_code" value="{form_field name=code}" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="otp"}
        {button type="submit" name="login" label="Se connecter" shape="right" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/_list_actions.tpl from [0b734d6ba4] to [143fb71a16].

1
2
3
4
5
6
7
8
9
10
		<tfoot>
			<tr>
				{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" /></td>{/if}
				<td class="actions" colspan="{$colspan}">
					<em>Pour les membres cochés :</em>
					{csrf_field key="membres_action"}
					<select name="action">
						<option value="">— Choisir une action à effectuer —</option>
						<option value="move">Changer de catégorie</option>
						<option value="csv">Exporter en tableau CSV</option>


|







1
2
3
4
5
6
7
8
9
10
		<tfoot>
			<tr>
				{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td>{/if}
				<td class="actions" colspan="{$colspan}">
					<em>Pour les membres cochés :</em>
					{csrf_field key="membres_action"}
					<select name="action">
						<option value="">— Choisir une action à effectuer —</option>
						<option value="move">Changer de catégorie</option>
						<option value="csv">Exporter en tableau CSV</option>

Modified src/templates/admin/membres/_nav.tpl from [308ec1b1ac] to [419563ac94].

1

2
3
4
5
6
7
8
9

<ul class="actions">

    <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}membres/">Liste des membres</a></li>
    <li{if $current == 'recherche'} class="current"{/if}><a href="{$admin_url}membres/recherche.php">Recherche avancée</a></li>
    <li{if $current == 'recherches'} class="current"{/if}><a href="{$admin_url}membres/recherches.php">Recherches enregistrées</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li{if $current == 'sql'} class="current"{/if}><a href="{$admin_url}membres/recherche_sql.php">Recherche SQL</a></li>
        <li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}membres/import.php">Import &amp; export</a></li>
    {/if}
</ul>

|
>
|
|
|
|
<
|
|
|
>
1
2
3
4
5
6

7
8
9
10
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}membres/">Liste des membres</a></li>
		<li{if $current == 'recherche'} class="current"{/if}><a href="{$admin_url}membres/recherche.php">Recherche avancée</a></li>
		<li{if $current == 'recherches'} class="current"{/if}><a href="{$admin_url}membres/recherches.php">Recherches enregistrées</a></li>
		{if $session->canAccess('membres', Membres::DROIT_ADMIN)}

			<li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}membres/import.php">Import &amp; export</a></li>
		{/if}
	</ul>
</nav>

Modified src/templates/admin/membres/action.tpl from [a933fed19a] to [076d7f7b64].

1
2
3
4
5
6
7
8
9
10
{include file="admin/_head.tpl" title="Action collective sur les membres" current="membres"}

<p class="alert">
    {$selected|count} membres sélectionnés
</p>

{form_errors}

<form method="post" action="{$self_url}">
    {foreach from=$selected item="id"}


|







1
2
3
4
5
6
7
8
9
10
{include file="admin/_head.tpl" title="Action collective sur les membres" current="membres"}

<p class="block alert">
    {$selected|count} membres sélectionnés
</p>

{form_errors}

<form method="post" action="{$self_url}">
    {foreach from=$selected item="id"}
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="membres_action"}
        <input type="hidden" name="action" value="move" />
        <input type="submit" name="confirm" value="Enregistrer &rarr;" />
    </p>

    {elseif $action == 'delete'}
    <fieldset>
        <legend>Supprimer les membres sélectionnés ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer les {$nb_selected} membres sélectionnés ?
        </h3>
        <p class="alert">
            <strong>Attention</strong> : cette action est irréversible et effacera toutes les
            données personnelles et l'historique de ces membres.
        </p>
        <p class="help">
            Alternativement, il est aussi possible de déplacer les membres qui ne font plus
            partie de l'association dans une catégorie «&nbsp;Anciens membres&nbsp;», plutôt
            que de les effacer complètement.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="membres_action"}
        <input type="hidden" name="action" value="delete" />
        <input type="submit" name="confirm" value="Oui, supprimer ces membres &rarr;" />
    </p>
    {/if}

</form>

{include file="admin/_foot.tpl"}







|








|













|






27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="membres_action"}
        <input type="hidden" name="action" value="move" />
        {button type="submit" name="confirm" label="Enregistrer" shape="right" class="main"}
    </p>

    {elseif $action == 'delete'}
    <fieldset>
        <legend>Supprimer les membres sélectionnés ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer les {$nb_selected} membres sélectionnés ?
        </h3>
        <p class="block alert">
            <strong>Attention</strong> : cette action est irréversible et effacera toutes les
            données personnelles et l'historique de ces membres.
        </p>
        <p class="help">
            Alternativement, il est aussi possible de déplacer les membres qui ne font plus
            partie de l'association dans une catégorie «&nbsp;Anciens membres&nbsp;», plutôt
            que de les effacer complètement.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="membres_action"}
        <input type="hidden" name="action" value="delete" />
        {button type="submit" name="confirm" label="Oui, supprimer ces membres" shape="delete" class="main"}
    </p>
    {/if}

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/ajouter.tpl from [0fe46b0cf5] to [e4d16e8863].

1
2
3
4
5




6
7
8
9
10
11
12
{include file="admin/_head.tpl" title="Ajouter un membre" current="membres/ajouter" js=1}

{form_errors}

<form method="post" action="{$self_url}">





    <fieldset>
        <legend>Informations personnelles</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {html_champ_membre config=$champ name=$nom}
            {/foreach}
|




>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Ajouter un membre" current="membres/ajouter"}

{form_errors}

<form method="post" action="{$self_url}">
    <!-- This is to avoid chrome autofill, Chrome developers you suck -->
    <input type="text" style="display: none;" name="email" />
    {if $id_field_name != 'email'}<input type="text" style="display: none;" name="{$id_field_name}" />{/if}
    <input type="password" style="display: none;" name="password" />

    <fieldset>
        <legend>Informations personnelles</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {html_champ_membre config=$champ name=$nom}
            {/foreach}
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
            </dd>
            <dd class="help">
                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
            </dd>
            <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern="{$password_pattern}" autocomplete="off" /></dd>
            <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt>
            <dd><input type="password" name="passe_confirmed" id="f_repasse" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" autocomplete="off" /></dd>
        </dl>
    </fieldset>

    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
    <fieldset>
        <legend>Général</legend>
        <dl>







|

|







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
                Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
                et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
            </dd>
            <dd class="help">
                Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
                <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
            </dd>
            <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern="{$password_pattern}" autocomplete="new-password" /></dd>
            <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt>
            <dd><input type="password" name="passe_confirmed" id="f_repasse" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" autocomplete="new-password" /></dd>
        </dl>
    </fieldset>

    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
    <fieldset>
        <legend>Général</legend>
        <dl>
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
            </dd>
        </dl>
    </fieldset>
    {/if}

    <p class="submit">
        {csrf_field key="new_member"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

<script type="text/javascript">
{literal}
g.script('scripts/password.js').onload = function () {
    initPasswordField('pw_suggest', 'f_passe', 'f_repasse');
};
{/literal}
</script>


{include file="admin/_foot.tpl"}







|






|

|





49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
            </dd>
        </dl>
    </fieldset>
    {/if}

    <p class="submit">
        {csrf_field key="new_member"}
        {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
    </p>

</form>

<script type="text/javascript">
{literal}
g.script('scripts/password.js', () => {
    initPasswordField('pw_suggest', 'f_passe', 'f_repasse');
});
{/literal}
</script>


{include file="admin/_foot.tpl"}

Deleted src/templates/admin/membres/cotisations.tpl version [329d9b5092].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
{include file="admin/_head.tpl" title="Cotisations du membre" current="membres/cotisations"}

<ul class="actions">
    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>{/if}
    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li class="current"><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>

<dl class="cotisation">
{if $cotisation}
    <dt>Cotisation obligatoire</dt>
    <dd>{$cotisation.intitule} — 
        {if $cotisation.duree}
            {$cotisation.duree} jours
        {elseif $cotisation.debut}
            du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french}
        {else}
            ponctuelle
        {/if}
        — {$cotisation.montant|escape|html_money} {$config.monnaie}
    </dd>
    <dt>À jour de cotisation ?</dt>
    <dd>
        {if !$cotisation.a_jour}
            <span class="error"><b>Non</b>, cotisation non payée</span>
        {else}
            <b class="confirm">&#10003; Oui</b>
            {if $cotisation.expiration}
                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
            {/if}
        {/if}
    </dd>
{/if}
    <dt>
        {if $nb_activites == 1}
            {$nb_activites} cotisation enregistrée
        {elseif $nb_activites}
            {$nb_activites} cotisations enregistrées
        {else}
            Aucune cotisation enregistrée
        {/if} 
    </dt>
{if !empty($cotisations_membre)}
    {foreach from=$cotisations_membre item="co"}
    <dd>{$co.intitule} — 
        {if $co.a_jour}
            <span class="confirm">À jour</span>{if $co.expiration} — Expire le {$co.expiration|format_sqlite_date_to_french}{/if}
        {else}
            <span class="error">En retard</span>
        {/if}
        — <a href="{$admin_url}membres/cotisations/rappels.php?id={$membre.id}">Suivi des rappels</a>
    </dd>
    {/foreach}
{/if}
{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
    <dt><form method="get" action="{$admin_url}membres/cotisations/ajout.php"><input type="submit" value="Enregistrer une cotisation &rarr;" /><input type="hidden" name="id" value="{$membre.id}" /></form></dt>
{/if}
</dl>

{if !empty($cotisations)}
<table class="list">
    <thead>
        <th>Date</th>
        <td>Cotisation</td>
        <td></td>
        <td class="actions"></td>
    </thead>
    <tbody>
        {foreach from=$cotisations item="c"}
            <tr>
                <td>{$c.date|format_sqlite_date_to_french}</td>
                <td>
                    {$c.intitule} — 
                    {if $c.duree}
                        {$c.duree} jours
                    {elseif $c.debut}
                        du {$c.debut|format_sqlite_date_to_french} au {$c.fin|format_sqlite_date_to_french}
                    {else}
                        ponctuelle
                    {/if}
                    — {$c.montant|escape|html_money} {$config.monnaie}
                </td>
                <td>
                    {if $session->canAccess('compta', Membres::DROIT_ECRITURE) && !empty($c.nb_operations)}
                        <a href="{$admin_url}compta/operations/cotisation.php?id={$c.id}">{$c.nb_operations} écriture{if $c.nb_operations > 1}s{/if}</a>
                    {/if}
                </td>
                <td class="actions">
                    <a class="icn" href="{$admin_url}membres/cotisations/voir.php?id={$c.id_cotisation}" title="Liste des membres inscrits à cette cotisation">👪</a>
                    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
                    <a class="icn" href="{$admin_url}membres/cotisations/supprimer.php?id={$c.id}" title="Supprimer cette cotisation pour ce membre">✘</a>
                    {/if}
                </td>
            </tr>
        {/foreach}
    </tbody>
</table>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































Deleted src/templates/admin/membres/cotisations/ajout.tpl version [6a2c71a215].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
{if $membre}
    {include file="admin/_head.tpl" title="Enregistrer une cotisation pour le membre" current="membres/cotisations" js=1}

    <ul class="actions">
        <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}"><b>{$membre.identite}</b></a></li>
        <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
        {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
            <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
        {/if}
        <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
    </ul>
{else}
    {include file="admin/_head.tpl" title="Enregistrer une cotisation" current="membres/cotisations" js=1}

    <ul class="actions">
        <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
        <li class="current"><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
        {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
            <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
        {/if}
    </ul>
{/if}

{form_errors}

{if $session->canAccess('compta', Membres::DROIT_ECRITURE)}
    <p class="help">
        Cette page sert à enregistrer les cotisations des membres de l'association.
        Pour enregistrer un don ou une dépense, comme le paiement d'un prestataire ou une facture, il est possible de <a href="{$admin_url}compta/operations/saisir.php">saisir une opération comptable</a>.
    </p>
{/if}

{if !count($cotisations)}
    <div class="alert">
        <p>Il n'y a aucun type de cotisation créé. Il n'est pas possible d'affecter une cotisation à un membre.</p>
        <form method="get" action="{$admin_url}membres/cotisations/">
            <p><input type="submit" value="Ajouter une cotisation &rarr;" /></p>
        </form>
    </div>
{else}
    <form method="post" action="{$self_url}">
        <fieldset>
            <legend>Enregistrer une cotisation</legend>
            <dl>
                <dt><label for="f_id_cotisation">Cotisation</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd>
                    <select id="f_id_cotisation" required="required" name="id_cotisation">
                        {foreach from=$cotisations item="co"}
                        <option value="{$co.id}" {form_field name="id_cotisation" selected=$co.id default=$default_co} data-compta="{$co.id_categorie_compta}" data-amount="{$co.montant}">
                            {$co.intitule}
                            — {$co.montant|escape|html_money} {$config.monnaie}
                            — {if $co.duree}pour {$co.duree} jours
                            {elseif $co.debut}
                                du {$co.debut|format_sqlite_date_to_french} au {$co.fin|format_sqlite_date_to_french}
                            {else}
                                ponctuelle
                            {/if}
                        </option>
                        {/foreach}
                    </select>
                </dd>
                <dt><label for="f_date">Date</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="date" name="date" id="f_date" value="{form_field name=date default=$default_date}" required="required" /></dd>
                {if !$membre}
                <dt><label for="f_numero_membre">Numéro de membre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="number" name="numero_membre" id="f_numero_membre" value="{form_field name=numero_membre}" step="1" min="1" required="required" /></dd>
                {/if}
                <dt class="f_compta"><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd class="f_compta"><input type="number" name="montant" step="0.01" min="0.00" id="f_montant" value="{form_field name=montant default=$default_amount}" /></dd>
                <dt class="f_compta"><label for="f_moyen_paiement">Moyen de paiement</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd class="f_compta">
                    <select name="moyen_paiement" id="f_moyen_paiement">
                    {foreach from=$moyens_paiement item="moyen"}
                        <option value="{$moyen.code}"{if $moyen.code == $moyen_paiement} selected="selected"{/if}>{$moyen.nom}</option>
                    {/foreach}
                    </select>
                </dd>
                <dd class="f_compta f_a_encaisser">
                    <input type="checkbox" name="a_encaisser" value="1" id="f_a_encaisser" {form_field name=a_encaisser checked="1"} />
                    <label for="f_a_encaisser">En attente d'encaissement</label>
                </dd>
                <dt class="f_compta f_cheque"><label for="f_numero_cheque">Numéro de chèque</label></dt>
                <dd class="f_compta f_cheque"><input type="text" name="numero_cheque" id="f_numero_cheque" value="{form_field name=numero_cheque}" /></dd>
                <dt class="f_compta f_banque"><label for="f_banque">Compte bancaire</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd class="f_compta f_banque">
                    <select name="banque" id="f_banque">
                    {foreach from=$comptes_bancaires item="compte"}
                        <option value="{$compte.id}"{if $compte.id == $banque} selected="selected"{/if}>{$compte.libelle} - {$compte.banque}</option>
                    {/foreach}
                    </select>
                </dd>
                <dt class="f_compta"><label for="f_numero_piece">Numéro de pièce comptable</label></dt>
                <dd class="f_compta"><input type="text" name="numero_piece" id="f_numero_piece" value="{form_field name=numero_piece}" /></dd>
                <dt class="f_compta"><label for="f_remarques">Remarques</label></dt>
                <dd class="f_compta"><textarea name="remarques" id="f_remarques" rows="4" cols="30">{form_field name=remarques}</textarea></dd>
            </dl>
        </fieldset>

        <p class="submit">
            {csrf_field key="add_cotisation"}
            {if $membre}<input type="hidden" name="id_membre" value="{$membre.id}" />{/if}
            <input type="submit" name="add" value="Enregistrer &rarr;" />
        </p>
    </form>

    <script type="text/javascript">
    {literal}
    (function () {
        window.changeMoyenPaiement = function()
        {
            var elm = $('#f_moyen_paiement');
            g.toggle('.f_cheque', elm.value == 'CH');
            g.toggle('.f_banque', elm.value != 'ES');

            g.toggle('.f_a_encaisser', elm.value == 'CB' || elm.value == 'CH');
            cocherAEncaisser();
        };

        function cocherAEncaisser()
        {
            var elm = $('#f_a_encaisser');
            g.toggle('.f_banque', !elm.checked && $('#f_moyen_paiement').value != 'ES');
        }

        changeMoyenPaiement();
        cocherAEncaisser();

        $('#f_moyen_paiement').onchange = changeMoyenPaiement;
        $('#f_a_encaisser').onchange = cocherAEncaisser;

        function changeCotisation()
        {
            var s = $('#f_id_cotisation');

            if (s.options[s.selectedIndex].getAttribute('data-compta'))
            {
                $('#f_montant').value = s.options[s.selectedIndex].getAttribute('data-amount'); 
                g.toggle('.f_compta', true);
                changeMoyenPaiement();
                cocherAEncaisser();
            }
            else
            {
                g.toggle('.f_compta', false);
            }
        }

        $('#f_id_cotisation').onchange = changeCotisation;

        if (!$('#f_id_cotisation').options[$('#f_id_cotisation').selectedIndex].getAttribute('data-compta'))
        {
            g.toggle('.f_compta', false);
        }
        else
        {
            changeCotisation();
        }
    } ());
    {/literal}
    </script>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































































































































































































































































































































Deleted src/templates/admin/membres/cotisations/gestion/modifier.tpl version [0430bb0891].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
{include file="admin/_head.tpl" title="Modifier une cotisation" current="membres/cotisations" js=1}

<ul class="actions">
    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
    {/if}
</ul>

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Modifier une cotisation</legend>
        <dl>
            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule data=$cotisation}" required="required" /></dd>
            <dt><label for="f_description">Description</label></dt>
            <dd><textarea name="description" id="f_description" cols="50" rows="3">{form_field name=description data=$cotisation}</textarea></dd>
            <dt><label for="f_montant">Montant</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="number" name="montant" step="0.01" min="0.00" id="f_montant" value="{form_field default=20 name=montant default=0.00 data=$cotisation}" required="required" /></dd>

            <dt><label for="f_periodicite_jours">Période de validité</label></dt>
            <dd><input type="radio" name="periodicite" id="f_periodicite_ponctuel" value="ponctuel" {form_field checked="ponctuel" name=periodicite default="ponctuel" data=$cotisation} /> <label for="f_periodicite_ponctuel">Pas de période (activité ou cotisation ponctuelle)</label></dd>

            <dd><input type="radio" name="periodicite" id="f_periodicite_jours" value="jours" {form_field checked="jours" name=periodicite data=$cotisation} /> <label for="f_periodicite_jours">En nombre de jours</label>
                <dl class="periode_jours">
                    <dt><label for="f_duree">Durée de validité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                    <dd><input type="number" step="1" size="5" min="1" name="duree" id="f_duree" value="{form_field name="duree" data=$cotisation}" /></dd>
                </dl>
            </dd>
            <dd><input type="radio" name="periodicite" id="f_periodicite_dates" value="date" {form_field checked="date" name=periodicite data=$cotisation} /> <label for="f_periodicite_dates">Période définie</label>
                <dl class="periode_dates">
                    <dt><label for="f_date_debut">Date de début</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                    <dd><input type="date" name="debut" value="{form_field name=debut data=$cotisation}" id="f_date_debut" /></dd>
                    <dt><label for="f_date_fin">Date de fin</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                    <dd><input type="date" name="fin" value="{form_field name=fin data=$cotisation}" id="f_date_fin" /></dd>
                </dl>
            </dd>
            <dt>
                <input type="checkbox" name="categorie" id="f_categorie" value="1" {form_field name="categorie" checked=1 data=$cotisation} /> <label for="f_categorie">Enregistrer les cotisations des membres dans la comptabilité</label>
            </dt>
            <dd class="help cat_compta">
                Si coché, à chaque enregistrement de cotisation d'un membre une opération 
                du montant de la cotisation sera enregistrée dans la comptabilité selon
                la catégorie choisie.
            </dd>
            <dt class="cat_compta"><label for="f_id_categorie_compta">Catégorie comptable</label></dt>
            <dd class="cat_compta">
                <select name="id_categorie_compta" id="f_id_categorie_compta">
                {foreach from=$categories item="cat"}
                    <option value="{$cat.id}" {form_field name="id_categorie_compta" selected=$cat.id data=$cotisation}>{$cat.intitule}
                    {if !empty($cat.description)}
                        — <em>{$cat.description}</em>
                    {/if}
                    </option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_co_%s"|args:$cotisation.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

<script type="text/javascript">
{literal}
(function () {
    var hide = [];

    if (!$('#f_categorie').checked)
        hide.push('.cat_compta');

    if (!$('#f_periodicite_jours').checked)
        hide.push('.periode_jours');

    if (!$('#f_periodicite_dates').checked)
        hide.push('.periode_dates');

    g.toggle(hide, false);

    $('#f_categorie').onchange = function() {
        g.toggle('.cat_compta', this.checked);
        return true;
    };

    function togglePeriode()
    {
        g.toggle(['.periode_jours', '.periode_dates'], false);

        if (this.checked && this.value == 'jours')
            g.toggle('.periode_jours', true);
        else if (this.checked && this.value == 'date')
            g.toggle('.periode_dates', true);
    }

    $('#f_periodicite_ponctuel').onchange = togglePeriode;
    $('#f_periodicite_dates').onchange = togglePeriode;
    $('#f_periodicite_jours').onchange = togglePeriode;
})();
{/literal}
</script>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































































Deleted src/templates/admin/membres/cotisations/gestion/supprimer.tpl version [002d1f0636].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{include file="admin/_head.tpl" title="Supprimer une cotisation" current="membres/cotisations"}

<ul class="actions">
    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
    {/if}
</ul>

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer cette cotisation ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer la cotisation «&nbsp;{$cotisation.intitule}&nbsp;» ?
        </h3>
        <p class="help">
            Attention, l'historique des membres ayant cotisé à cette cotisation sera supprimé.
            Si des écritures comptables sont liées à l'historique des cotisations, elles ne seront pas supprimées,
            et la comptabilité demeurera inchangée.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_co_"|cat:$cotisation.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































Deleted src/templates/admin/membres/cotisations/rappels.tpl version [da02475b4a].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
{include file="admin/_head.tpl" title="Rappels pour cotisations du membre" current="membres/cotisations" js=1}

<ul class="actions">
    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}"><b>{$membre.identite}</b></a></li>
    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
        <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
    {/if}
    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li class="current"><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>

{if !count($cotisations)}
    <p class="alert">Ce membre n'est lié à aucune cotisation, il n'est donc pas possible de lui associer un rappel de cotisation.</p>
{elseif $session->canAccess('membres', Membres::DROIT_ECRITURE)}
<form method="post" action="{$self_url}">
    <fieldset>
        <legend>Enregistrer un rappel fait à ce membre</legend>
        <dl>
            <dt><label for="f_id_cotisation">Cotisation</label></dt>
            <dd>
                <select id="f_id_cotisation" name="id_cotisation">
                {foreach from=$cotisations item="co"}
                    <option value="{$co.id}">{$co.intitule} {if $co.nb_jours}

                        {if $co.a_jour}
                            Expire dans {$co.nb_jours} jours
                        {else}
                            EXPIRÉE depuis {$co.nb_jours} jours
                        {/if}
                    {/if}
                    </option>
                {/foreach}
                </select>
            </dd>
            <dt><label for="f_date">Date du rappel</label></dt>
            <dd><input type="date" name="date" id="f_date" required="required" value="{form_field name="date" default=$default_date}" /></dd>
            <dt><label for="f_media_email">Moyen de communication utilisé</label></dt>
            <dd>
                <label>
                    <input id="f_media_email" type="radio" name="media" value="{$rappels_envoyes::MEDIA_EMAIL}" />
                    E-Mail
                </label>
            </dd>
            {* FIXME: proposer d'envoyer un email au membre *}
            <dd>
                <label>
                    <input type="radio" name="media" value="{$rappels_envoyes::MEDIA_TELEPHONE}" />
                    Téléphone
                </label>
            </dd>
            {* FIXME: afficher les différents numéros de téléphone de la fiche membre *}
            <dd>
                <label>
                    <input type="radio" name="media" value="{$rappels_envoyes::MEDIA_COURRIER}" />
                    Courrier postal
                </label>
            </dd>
            <dd>
                <label>
                    <input type="radio" name="media" value="{$rappels_envoyes::MEDIA_AUTRE}" />
                    Autre
                </label>
            </dd>
        </dl>
        <p class="submit">
            {csrf_field key="add_rappel_%s"|args:$membre.id}
            <input type="submit" name="save" value="Enregistrer le rappel &rarr;" />
        </p>
    </fieldset>
</form>
{/if}

{if !empty($rappels)}
<table class="list">
    <thead>
        <th>Date du rappel</th>
        <td>Moyen de communication</td>
        <td>Cotisation</td>
        <td class="actions"></td>
    </thead>
    <tbody>
        {foreach from=$rappels item="r"}
            <tr>
                <th>{$r.date|format_sqlite_date_to_french}</th>
                <td>
                    {if $r.media == Rappels_envoyes::MEDIA_AUTRE}
                        Autre
                    {elseif $r.media == Rappels_envoyes::MEDIA_COURRIER}
                        Courrier
                    {elseif $r.media == Rappels_envoyes::MEDIA_TELEPHONE}
                        Téléphone
                    {else}
                        E-Mail
                    {/if}
                </td>
                <td>
                    {$r.intitule} — 
                    {$r.montant|escape|html_money} {$config.monnaie}
                </td>
                <td class="actions">
                </td>
            </tr>
        {/foreach}
    </tbody>
</table>
{/if}

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































































Modified src/templates/admin/membres/fiche.tpl from [99f7b69c9c] to [4cc42f5c24].

1
2
3

4
5
6
7
8
9
10

11
12
13
14
15

16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34
35


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{include file="admin/_head.tpl" title="%s (%s)"|args:$membre.identite:$categorie.nom current="membres"}

<ul class="actions">

    <li class="current"><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>{/if}
    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>


<dl class="cotisation">
{if $cotisation}
    <dt>Cotisation obligatoire</dt>
    <dd>{$cotisation.intitule} — 

        {if $cotisation.duree}
            {$cotisation.duree} jours
        {elseif $cotisation.debut}
            du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french}
        {else}
            ponctuelle
        {/if}
        — {$cotisation.montant|escape|html_money} {$config.monnaie}
    </dd>
    <dt>À jour de cotisation ?</dt>

    <dd>
        {if !$cotisation.a_jour}
            <span class="error"><b>Non</b>, cotisation non payée</span>
        {else}
            <b class="confirm">&#10003; Oui</b>
            {if $cotisation.expiration}
                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
            {/if}
        {/if}
    </dd>


{/if}
    <dt>
        {if $nb_activites == 1}
            {$nb_activites} cotisation enregistrée
        {elseif $nb_activites}
            {$nb_activites} cotisations enregistrées
        {else}
            Aucune cotisation enregistrée
        {/if} 
    </dt>
    <dd>
        <a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Voir l'historique</a>
    </dd>
    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
        <dd><form method="get" action="{$admin_url}membres/cotisations/ajout.php"><input type="submit" value="Enregistrer une cotisation &rarr;" /><input type="hidden" name="id" value="{$membre.id}" /></form></dd>
        {if !empty($nb_operations)}
            <dt>Écritures comptables</dt>



            <dd>{$nb_operations} écritures comptables
            <a href="{$admin_url}compta/operations/membre.php?id={$membre.id}">Voir la liste des écritures ajoutées par ce membre</a>
            </dd>
        {/if}
    {/if}
</dl>

<aside class="describe">
	<dl class="describe">
		<dt>Catégorie</dt>
		<dd>{$categorie.nom} <span class="droits">{format_droits droits=$categorie}</span></dd>
		<dt>Inscription</dt>
		<dd>{$membre.date_inscription|date_fr:'d/m/Y'}</dd>
		<dt>Dernière connexion</dt>
		<dd>{if empty($membre.date_connexion)}Jamais{else}{$membre.date_connexion|date_fr:'d/m/Y à H:i'}{/if}</dd>
		<dt>Mot de passe</dt>
		<dd>
			{if empty($membre.passe)}
				Pas de mot de passe configuré
			{else}
				<b class="icn">☑</b> Oui
				{if !empty($membre.secret_otp)}


|
>
|
|
|
|
|
<
|
>


|
|
|
>
|
<
|
|
|
|
|
<

<
>

|
<
<
|
<
<
|
<
|
>
>
|
|
|
<
<
<
<
<
|
<
<
<

|
<
|
|
>
>
>
|
|
<









|

|







1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18

19
20
21
22
23

24

25
26
27


28


29

30
31
32
33
34
35





36



37
38

39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{include file="admin/_head.tpl" title="%s (%s)"|args:$membre.identite:$categorie.nom current="membres"}

<nav class="tabs">
    <ul>
        <li class="current"><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
        {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}<li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>{/if}
        {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
            <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
        {/if}

    </ul>
</nav>

<dl class="cotisation">
    <dt>Activités et cotisations</dt>
    {foreach from=$services item="service"}
    <dd>
        {$service.label}
        {if $service.status == -1 && $service.end_date} — terminée

        {elseif $service.status == -1} — <b class="error">en retard</b>
        {elseif $service.status == 1 && $service.end_date} — <b class="confirm">en cours</b>
        {elseif $service.status == 1} — <b class="confirm">à jour</b>{/if}
        {if $service.status.expiry_date} — expire le {$service.expiry_date|date_short}{/if}
        {if !$service.paid} — <b class="error">À payer&nbsp;!</b>{/if}

    </dd>

    {foreachelse}
    <dd>
        Ce membre n'est inscrit à aucune activité ou cotisation.


    </dd>


    {/foreach}

    <dd>
        {if count($services)}
            {linkbutton href="!services/user.php?id=%d"|args:$membre.id label="Liste des inscriptions aux activités" shape="menu"}
        {/if}
        {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
            {linkbutton href="!services/save.php?user=%d"|args:$membre.id label="Inscrire à une activité" shape="plus"}





        {/if}



    </dd>
    {if $session->canAccess('membres', Membres::DROIT_ACCES)}

        {if !empty($transactions_linked)}
            <dt>Écritures comptables liées</dt>
            <dd><a href="{$admin_url}acc/transactions/user.php?id={$membre.id}">{$transactions_linked} écritures comptables liées à ce membre</a></dd>
        {/if}
        {if !empty($transactions_created)}
            <dt>Écritures comptables créées</dt>
            <dd><a href="{$admin_url}acc/transactions/creator.php?id={$membre.id}">{$transactions_created} écritures comptables créées par ce membre</a></dd>

        {/if}
    {/if}
</dl>

<aside class="describe">
	<dl class="describe">
		<dt>Catégorie</dt>
		<dd>{$categorie.nom} <span class="droits">{format_droits droits=$categorie}</span></dd>
		<dt>Inscription</dt>
		<dd>{$membre.date_inscription|date_short}</dd>
		<dt>Dernière connexion</dt>
		<dd>{if empty($membre.date_connexion)}Jamais{else}{$membre.date_connexion|date_long}{/if}</dd>
		<dt>Mot de passe</dt>
		<dd>
			{if empty($membre.passe)}
				Pas de mot de passe configuré
			{else}
				<b class="icn">☑</b> Oui
				{if !empty($membre.secret_otp)}
90
91
92
93
94
95
96
97
98
99
100
101
102
103


104
105
106
107
108
109
110
111
        {elseif empty($membre->$c)}
            <em>(Non renseigné)</em>
        {elseif $c == $c_config.champ_identite}
            <strong>{$membre->$c}</strong>
        {elseif $c_config.type == 'email'}
            <a href="mailto:{$membre->$c|escape:'url'}">{$membre->$c}</a>
            {if $c == 'email'}
                | <a href="{$admin_url}membres/message.php?id={$membre.id}"><b class="icn action">✉</b> Envoyer un message</a>
            {/if}
        {elseif $c_config.type == 'tel'}
            <a href="tel:{$membre->$c}">{$membre->$c|format_tel}</a>
        {elseif $c_config.type == 'country'}
            {$membre->$c|get_country_name}
        {elseif $c_config.type == 'date' || $c_config.type == 'datetime'}


            {$membre->$c|format_sqlite_date_to_french}
        {elseif $c_config.type == 'password'}
            *******
        {elseif $c_config.type == 'multiple'}
            <ul>
            {foreach from=$c_config.options key="b" item="name"}
                {if $membre->$c & (0x01 << $b)}
                    <li>{$name}</li>







|





|
>
>
|







80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
        {elseif empty($membre->$c)}
            <em>(Non renseigné)</em>
        {elseif $c == $c_config.champ_identite}
            <strong>{$membre->$c}</strong>
        {elseif $c_config.type == 'email'}
            <a href="mailto:{$membre->$c|escape:'url'}">{$membre->$c}</a>
            {if $c == 'email'}
                {linkbutton href="!membres/message.php?id=%d"|args:$membre.id label="Envoyer un message" shape="mail"}
            {/if}
        {elseif $c_config.type == 'tel'}
            <a href="tel:{$membre->$c}">{$membre->$c|format_tel}</a>
        {elseif $c_config.type == 'country'}
            {$membre->$c|get_country_name}
        {elseif $c_config.type == 'date'}
            {$membre->$c|date_short}
        {elseif $c_config.type == 'datetime'}
            {$membre->$c|date_fr}
        {elseif $c_config.type == 'password'}
            *******
        {elseif $c_config.type == 'multiple'}
            <ul>
            {foreach from=$c_config.options key="b" item="name"}
                {if $membre->$c & (0x01 << $b)}
                    <li>{$name}</li>

Modified src/templates/admin/membres/import.tpl from [1c6c043793] to [203c781562].

1
2
3
4

5
6
7
8
9

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

90
91
92
93
94
95
{include file="admin/_head.tpl" title="Import & export des membres" current="membres" js=1}

{include file="admin/membres/_nav.tpl" current="import"}


<ul class="actions sub">
    <li class="current"><a href="{$admin_url}membres/import.php">Importer</a></li>
    <li><a href="{$admin_url}membres/import.php?export=csv">Exporter en CSV</a></li>
    <li><a href="{$admin_url}membres/import.php?export=ods">Exporter en classeur Office</a></li>
</ul>


{form_errors}

{if $ok}
    <p class="confirm">
        L'import s'est bien déroulé.
    </p>
{/if}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

    {if $csv_file}

    <fieldset>
        <legend>Importer depuis un fichier CSV générique</legend>
        <p class="help">{$csv_file|count} lignes trouvées dans le fichier</p>
        <dl>
            <dt><label><input type="checkbox" name="skip_first_line" value="1" checked="checked" /> Ne pas importer la première ligne</label></dt>
            <dd class="help">Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes mais des données.</dd>
            <dt><label>Correspondance des champs</label></dt>
            <dd class="help">Indiquer la correspondance entre colonnes du CSV et champs des fiches membre.</dd>
            <dd>
                <table class="list auto">
                    <tbody>
                    {foreach from=$csv_first_line key="index" item="csv_field"}
                        <tr>
                            <th>{$csv_field}</th>
                            <td>
                                <select name="csv_translate[{$index}]">
                                    <option value="">-- Ne pas importer ce champ</option>
                                    {foreach from=$garradin_champs item="champ" key="name"}
                                        {if $champ.type == 'multiple' || $champ.type == 'file' || $name == 'passe'}{continue}{/if}
                                        <option value="{$name}">{$champ.title}</option>
                                    {/foreach}
                                </select>
                            </td>
                        </tr>
                    {/foreach}
                    </tbody>
                </table>
            </dd>
            <dd class="help">Pour fusionner des colonnes, il suffit d'indiquer le même nom de champ pour plusieurs colonnes.</dd>
        </dl>
    </fieldset>

    <input type="hidden" name="csv_encoded" value="{$csv_file|escape:'json'|escape}" />

    {else}

    <fieldset>
        <legend>Importer depuis un fichier</legend>
        <dl>
            <dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">La taille maximale du fichier est de {$max_upload_size|format_bytes}.</dd>
            <dd><input type="file" name="upload" id="f_file" required="required" /></dd>
            <dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
                <label for="f_type">Fichier CSV de Garradin</label>
            </dd>
            <dd class="help">
                Export de la liste des membres au format CSV provenant de Garradin.
                Les lignes comportant un numéro de membre mettront à jour les fiches des membres ayant ce numéro (si le numéro existe),
                les lignes sans numéro ou avec un numéro inexistant créeront de nouveaux membres.
            </dd>
            <dd>
                <input type="radio" name="type" id="f_type_csv" value="csv" {form_field name=type checked="csv"} />
                <label for="f_type_csv">Fichier CSV générique</label>
            </dd>
            <dd class="help">
                Vous pourrez choisir la correspondance entre colonnes du CSV et champs des fiches membres
                dans le prochain écran.
            </dd>
        </dl>
    </fieldset>

    {/if}

    <p class="submit">
        {csrf_field key="membres_import"}

        <input type="submit" name="import" value="Importer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|



>
|
|
|
|
|
>




|






|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<




















|












|
>
|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24































25

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{include file="admin/_head.tpl" title="Import & export des membres" current="membres"}

{include file="admin/membres/_nav.tpl" current="import"}

<nav class="tabs">
    <ul class="sub">
        <li class="current"><a href="{$admin_url}membres/import.php">Importer</a></li>
        <li><a href="{$admin_url}membres/import.php?export=csv">Exporter en CSV</a></li>
        <li><a href="{$admin_url}membres/import.php?export=ods">Exporter en classeur Office</a></li>
    </ul>
</nav>

{form_errors}

{if $ok}
    <p class="block confirm">
        L'import s'est bien déroulé.
    </p>
{/if}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

    {if $csv->loaded()}
































        {include file="common/_csv_match_columns.tpl"}


    {else}

    <fieldset>
        <legend>Importer depuis un fichier</legend>
        <dl>
            <dt><label for="f_file">Fichier à importer</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">La taille maximale du fichier est de {$max_upload_size|format_bytes}.</dd>
            <dd><input type="file" name="upload" id="f_file" required="required" /></dd>
            <dt><label for="f_type">Type de fichier</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <input type="radio" name="type" id="f_type" value="garradin" {form_field name=type checked="garradin" default="garradin"} />
                <label for="f_type">Fichier CSV de Garradin</label>
            </dd>
            <dd class="help">
                Export de la liste des membres au format CSV provenant de Garradin.
                Les lignes comportant un numéro de membre mettront à jour les fiches des membres ayant ce numéro (si le numéro existe),
                les lignes sans numéro ou avec un numéro inexistant créeront de nouveaux membres.
            </dd>
            <dd>
                <input type="radio" name="type" id="f_type_csv" value="custom" {form_field name=type checked="csv"} />
                <label for="f_type_csv">Fichier CSV générique</label>
            </dd>
            <dd class="help">
                Vous pourrez choisir la correspondance entre colonnes du CSV et champs des fiches membres
                dans le prochain écran.
            </dd>
        </dl>
    </fieldset>

    {/if}

    <p class="submit">
        {csrf_field key=$csrf_key}
        {if $csv->loaded()}{button type="submit" name="cancel" value="1" label="Annuler" shape="left"}{/if}
        {button type="submit" name="import" label="Importer" shape="upload" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/index.tpl from [079c03ecc2] to [32ce18713d].

1
2
3
4
5
6
7
8
9
10
11
12
13
{include file="admin/_head.tpl" title="Liste des membres" current="membres" js=1}

{include file="admin/membres/_nav.tpl" current="index"}

{if $sent}
    <p class="confirm">Votre message a été envoyé.</p>
{/if}

{if !empty($membres_cats)}
<form method="get" action="{$self_url}" class="shortFormRight">
    <fieldset>
        <legend>Filtrer par catégorie</legend>
        <select name="cat" id="f_cat" onchange="this.form.submit();">
|




|







1
2
3
4
5
6
7
8
9
10
11
12
13
{include file="admin/_head.tpl" title="Liste des membres" current="membres"}

{include file="admin/membres/_nav.tpl" current="index"}

{if $sent}
    <p class="block confirm">Votre message a été envoyé.</p>
{/if}

{if !empty($membres_cats)}
<form method="get" action="{$self_url}" class="shortFormRight">
    <fieldset>
        <legend>Filtrer par catégorie</legend>
        <select name="cat" id="f_cat" onchange="this.form.submit();">
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

<form method="post" action="action.php" class="memberList">

{if !empty($liste)}
    <table class="list">
        <thead class="userOrder">
            <tr>
                {if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" title="Tout cocher / décocher" /></td>{/if}
                {foreach from=$champs key="c" item="champ"}
                    <td class="{if $order == $c} cur {if $desc}desc{else}asc{/if}{/if}">{if $c == "numero"}#{else}{$champ.title}{/if} <a href="?o={$c}&amp;a&amp;cat={$current_cat}" class="icn up">&uarr;</a><a href="?o={$c}&amp;d&amp;cat={$current_cat}" class="icn dn">&darr;</a></td>
                {/foreach}
                <td></td>
            </tr>
        </thead>
        <tbody>
            {foreach from=$liste item="membre"}
                <tr>
                    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" name="selected[]" value="{$membre.id}" /></td>{/if}
                    {foreach from=$champs key="c" item="cfg"}
                        <td>
                            {if $c == $config.champ_identite}<a href="{$admin_url}membres/fiche.php?id={$membre.id}">{/if}
                            {$membre->$c|raw|display_champ_membre:$cfg}
                            {if $c == $config.champ_identite}</a>{/if}
                        </td>
                    {/foreach}
                    <td class="actions">
                        <a class="icn" href="{$admin_url}membres/fiche.php?id={$membre.id}" title="Fiche membre">👤</a>
                        {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}<a class="icn" href="{$admin_url}membres/modifier.php?id={$membre.id}" title="Modifier la fiche membre">✎</a>{/if}


                    </td>
                </tr>
            {/foreach}
        </tbody>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        {include file="admin/membres/_list_actions.tpl" colspan=count((array)$champs)+1}
    {/if}
    </table>

    {pagination url=$pagination_url page=$page bypage=$bypage total=$total}
{else}
    <p class="alert">
        Aucun membre trouvé.
    </p>
{/if}

</form>

{include file="admin/_foot.tpl"}







|

|







|








|
|
>
>











|







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

<form method="post" action="action.php" class="memberList">

{if !empty($liste)}
    <table class="list">
        <thead class="userOrder">
            <tr>
                {if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>{/if}
                {foreach from=$champs key="c" item="champ"}
                    <td class="{if $order == $c} cur {if $desc}desc{else}asc{/if}{/if}">{if $c == "numero"}Num.{else}{$champ.title}{/if} <a href="?o={$c}&amp;a&amp;cat={$current_cat}" class="icn up">&uarr;</a><a href="?o={$c}&amp;d&amp;cat={$current_cat}" class="icn dn">&darr;</a></td>
                {/foreach}
                <td></td>
            </tr>
        </thead>
        <tbody>
            {foreach from=$liste item="membre"}
                <tr>
                    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check">{input type="checkbox" name="selected[]" value=$membre.id}</td>{/if}
                    {foreach from=$champs key="c" item="cfg"}
                        <td>
                            {if $c == $config.champ_identite}<a href="{$admin_url}membres/fiche.php?id={$membre.id}">{/if}
                            {$membre->$c|raw|display_champ_membre:$cfg}
                            {if $c == $config.champ_identite}</a>{/if}
                        </td>
                    {/foreach}
                    <td class="actions">
                        {linkbutton label="Fiche membre" shape="user" href="!membres/fiche.php?id=%d"|args:$membre.id}
                        {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
                            {linkbutton label="Modifier" shape="edit" href="!membres/modifier.php?id=%d"|args:$membre.id}
                        {/if}
                    </td>
                </tr>
            {/foreach}
        </tbody>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        {include file="admin/membres/_list_actions.tpl" colspan=count((array)$champs)+1}
    {/if}
    </table>

    {pagination url=$pagination_url page=$page bypage=$bypage total=$total}
{else}
    <p class="block alert">
        Aucun membre trouvé.
    </p>
{/if}

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/message.tpl from [3012105144] to [4059d1bc38].

19
20
21
22
23
24
25
26
27
28
29
30
31
                <label for="f_copie">Recevoir par e-mail une copie du message envoyé</label>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="send_message_"|cat:$membre.id}
        <input type="submit" name="save" value="Envoyer &rarr;" />
    </p>
</form>


{include file="admin/_foot.tpl"}







|





19
20
21
22
23
24
25
26
27
28
29
30
31
                <label for="f_copie">Recevoir par e-mail une copie du message envoyé</label>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="send_message_"|cat:$membre.id}
        {button type="submit" name="save" label="Envoyer" shape="right" class="main"}
    </p>
</form>


{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/message_collectif.tpl from [873e17c7a4] to [526fe5883b].

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
					Seulement les membres à jour de cotisation
				</label>
			</dd>
			*}
			<dt><label for="f_sujet">Sujet</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet}" required="required" /></dd>
			<dt><label for="f_message">Message</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><textarea name="message" id="f_message" cols="72" rows="25" required="required">{form_field name=message}</textarea></dd>
			<dd>
				<input type="checkbox" name="copie" id="f_copie" value="1" />
				<label for="f_copie">Recevoir par e-mail une copie du message envoyé</label>
			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="send_message_co"}
		<input type="submit" name="send" value="Envoyer &rarr;" />
	</p>
</form>


{include file="admin/_foot.tpl"}







|









|





29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
					Seulement les membres à jour de cotisation
				</label>
			</dd>
			*}
			<dt><label for="f_sujet">Sujet</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet}" required="required" /></dd>
			<dt><label for="f_message">Message</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><textarea name="message" id="f_message" cols="35" rows="25" required="required">{form_field name=message}</textarea></dd>
			<dd>
				<input type="checkbox" name="copie" id="f_copie" value="1" />
				<label for="f_copie">Recevoir par e-mail une copie du message envoyé</label>
			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="send_message_co"}
		{button type="submit" name="send" label="Envoyer" shape="right" class="main"}
	</p>
</form>


{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/modifier.tpl from [557472298c] to [be24573e1c].

1
2
3

4
5
6
7
8
9
10

11
12
13
14




15
16
17
18
19
20
21
{include file="admin/_head.tpl" title="Modifier un membre" current="membres" js=1}

<ul class="actions">

    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
    <li class="current"><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>


{form_errors}

<form method="post" action="{$self_url}">





    <fieldset>
        <legend>Informations personnelles</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {html_champ_membre config=$champ name=$nom data=$membre}
            {/foreach}
|

|
>
|
|
|
|
|
<
|
>




>
>
>
>







1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="admin/_head.tpl" title="Modifier un membre" current="membres"}

<nav class="tabs">
    <ul>
        <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
        <li class="current"><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
        {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
            <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
        {/if}

    </ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}">
    <!-- This is to avoid chrome autofill, Chrome developers you suck -->
    <input type="text" style="display: none;" name="email" />
    {if $id_field_name != 'email'}<input type="text" style="display: none;" name="{$id_field_name}" />{/if}
    <input type="password" style="display: none;" name="password" />

    <fieldset>
        <legend>Informations personnelles</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {html_champ_membre config=$champ name=$nom data=$membre}
            {/foreach}
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    </fieldset>

    {if $membre.secret_otp || $membre.clef_pgp}
    <fieldset>
        <legend>Options de sécurité</legend>
        <dl>
        {if $membre.secret_otp}
            <dt><label><input type="checkbox" name="clear_otp" value="1" /> Désactiver l'authentification à double facteur TOTP</label></dt>
        {/if}
        {if $membre.clef_pgp}
            <dt><label><input type="checkbox" name="clear_pgp" value="1" /> Supprimer la clé PGP associée au membre</label></dt>
        {/if}
        </dl>
    </fieldset>
    {/if}

    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
    <fieldset>







|


|







51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    </fieldset>

    {if $membre.secret_otp || $membre.clef_pgp}
    <fieldset>
        <legend>Options de sécurité</legend>
        <dl>
        {if $membre.secret_otp}
            {input type="checkbox" name="clear_otp" value="1" label="Désactiver l'authentification à double facteur TOTP"}
        {/if}
        {if $membre.clef_pgp}
            {input type="checkbox" name="clear_pgp" value="1" label="Supprimer la clé PGP associée au membre"}
        {/if}
        </dl>
    </fieldset>
    {/if}

    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
    <fieldset>
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
            </dd>
        </dl>
    </fieldset>
    {/if}

    <p class="submit">
        {csrf_field key="edit_member_"|cat:$membre.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

<script type="text/javascript">
{literal}
g.script('scripts/password.js').onload = function () {
    initPasswordField('pw_suggest', 'f_passe', 'f_repasse');
};
{/literal}
</script>

{include file="admin/_foot.tpl"}







|






|

|




78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
            </dd>
        </dl>
    </fieldset>
    {/if}

    <p class="submit">
        {csrf_field key="edit_member_"|cat:$membre.id}
        {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
    </p>

</form>

<script type="text/javascript">
{literal}
g.script('scripts/password.js', () => {
    initPasswordField('pw_suggest', 'f_passe', 'f_repasse');
});
{/literal}
</script>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/recherche.tpl from [f88de1f28b] to [9258528c92].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
{include file="admin/_head.tpl" title="Recherche de membre" current="membres" js=1 custom_js=['query_builder.min.js']}

{include file="admin/membres/_nav.tpl" current="recherche"}

{form_errors}

<form method="post" action="{$admin_url}membres/recherche.php" id="queryBuilderForm">
	<fieldset>
		<legend>Rechercher un membre</legend>
		<div class="queryBuilder" id="queryBuilder"></div>
		<p class="actions">
			<label>Trier par 
				<select name="order">
					{foreach from=$colonnes key="colonne" item="config"}
					<option value="{$colonne}"{form_field name="order" selected=$colonne}>{$config.label}</option>
					{/foreach}
				</select>
			</label>
			<label><input type="checkbox" name="desc" value="1" {form_field name="desc" checked=1 default=$desc} /> Tri inversé</label>
			<label>Limiter à <input type="number" value="{$limit}" name="limit" size="5" /> résultats</label>
		</p>
		<p class="submit">
			<input type="submit" value="Chercher &rarr;" id="send" />
			<input type="hidden" name="q" id="jsonQuery" />
			<input type="hidden" name="id" value="{$id}" />
			<input type="submit" name="save" value="{if $id}Enregistrer : {$recherche.intitule|truncate:40:"…":true}{else}Enregistrer cette recherche{/if}" class="minor" />
		</p>
	</fieldset>
</form>

<script type="text/javascript">
var colonnes = {$colonnes|escape:'json'};

{literal}
var traductions = {
	"after": "après",
	"before": "avant",
	"is equal to": "est égal à",
	"is equal to one of": "est égal à une des ces options",
	"is not equal to one of": "n'est pas égal à une des ces options",
	"is not equal to": "n'est pas égal à",
	"is greater than": "est supérieur à",
	"is greater than or equal to": "est supérieur ou égal à",
	"is less than": "est inférieur à",
	"is less than or equal to": "est inférieur ou égal à",
	"is between": "est situé entre",
	"is not between": "n'est pas situé entre",
	"is null": "est nul",
	"is not null": "n'est pas nul",
	"begins with": "commence par",
	"doesn't begin with": "ne commence pas par",
	"ends with": "se termine par",
	"doesn't end with": "ne se termine pas par",
	"contains": "contient",
	"doesn't contain": "ne contient pas",
	"matches one of": "correspond à",
	"is true": "oui",
	"is false": "non",
	"Matches ALL of the following conditions:": "Correspond à TOUS les critères suivants :",
	"Matches ANY of the following conditions:": "Correspond à UN des critères suivants :",
	"Add a new set of conditions below this one": "-- Ajouter un groupe de critères",
	"Remove this set of conditions": "-- Supprimer ce groupe de critères"
};

var q = new SQLQueryBuilder(colonnes);
q.__ = function (str) {
	return traductions[str];
};
q.loadDefaultOperators();
q.buildInput = function (type, label, column) {
	if (label == '+')
	{
		label = '➕';
	}
	else if (label == '-')
	{
		label = '➖';
	}

	var i = document.createElement('input');
	i.type = type == 'integer' ? 'number' : type;
	i.value = label;

	if (type == 'button')
	{
		i.className = 'icn action';
	}

	return i;
};
q.init(document.getElementById('queryBuilder'));

$('#queryBuilderForm').onsubmit = function () {
	$('#jsonQuery').value = JSON.stringify(q.export());
};
{/literal}
q.import({$query|escape:'json'});
</script>


{if !empty($result)}
	{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
		<form method="post" action="{$admin_url}membres/action.php" class="memberList">
	{/if}

	<p class="help">{$result|count} membres trouvés pour cette recherche.</p>
	<table class="list search">
		<thead>
			<tr>
				{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" /></td>{/if}
				{foreach from=$result_header key="c" item="cfg"}
					<td>{$cfg.title}</td>
				{/foreach}
				<td></td>
			</tr>
		</thead>
		<tbody>
			{foreach from=$result item="row"}
				<tr>
					{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" name="selected[]" value="{$row.id}" /></td>{/if}
					{foreach from=$row key="key" item="value"}
						<?php $link = false; ?>
						{if isset($result_header[$key])}
							<td>
								{if !$link}
									<a href="{$admin_url}membres/fiche.php?id={$row.id}">
								{/if}

								{$value|raw|display_champ_membre:$result_header[$key]}

								{if !$link}
									<?php $link = true; ?>
									</a>
								{/if}
							</td>
						{/if}
					{/foreach}
					<td class="actions">
						<a class="icn" href="{$admin_url}membres/fiche.php?id={$row.id}" title="Fiche membre">👤</a>
						{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
						<a class="icn" href="{$admin_url}membres/modifier.php?id={$row.id}" title="Modifier la fiche membre">✎</a>
						{/if}
					</td>
				</tr>
			{/foreach}
		</tbody>
	{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
		{include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1}
	{/if}
	</table>

	{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
		</form>
	{/if}

{elseif $result !== null}

	<p class="alert">
		Aucun membre trouvé.
	</p>

	</form>
{/if}


{include file="admin/_foot.tpl"}
|



<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










|









|








|









|

|
















|








1
2
3
4

5





























































































6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="Recherche de membre" current="membres" custom_js=['query_builder.min.js']}

{include file="admin/membres/_nav.tpl" current="recherche"}


{include file="common/search/advanced.tpl" action_url=$self_url}






























































































{if !empty($result)}
	{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
		<form method="post" action="{$admin_url}membres/action.php" class="memberList">
	{/if}

	<p class="help">{$result|count} membres trouvés pour cette recherche.</p>
	<table class="list search">
		<thead>
			<tr>
				{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>{/if}
				{foreach from=$result_header key="c" item="cfg"}
					<td>{$cfg.title}</td>
				{/foreach}
				<td></td>
			</tr>
		</thead>
		<tbody>
			{foreach from=$result item="row"}
				<tr>
					{if $session->canAccess('membres', Membres::DROIT_ADMIN)}<td class="check">{input type="checkbox" name="selected[]" value=$row.id}</td>{/if}
					{foreach from=$row key="key" item="value"}
						<?php $link = false; ?>
						{if isset($result_header[$key])}
							<td>
								{if !$link}
									<a href="{$admin_url}membres/fiche.php?id={$row.id}">
								{/if}

								{$value|raw|display_champ_membre:$key}

								{if !$link}
									<?php $link = true; ?>
									</a>
								{/if}
							</td>
						{/if}
					{/foreach}
					<td class="actions">
						{linkbutton shape="user" label="Fiche membre" href="!membres/fiche.php?id=%d"|args:$row.id}
						{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
						{linkbutton shape="edit" label="Modifier" href="!membres/modifier.php?id=%d"|args:$row.id}
						{/if}
					</td>
				</tr>
			{/foreach}
		</tbody>
	{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
		{include file="admin/membres/_list_actions.tpl" colspan=count($result_header)+1}
	{/if}
	</table>

	{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
		</form>
	{/if}

{elseif $result !== null}

	<p class="block alert">
		Aucun membre trouvé.
	</p>

	</form>
{/if}


{include file="admin/_foot.tpl"}

Deleted src/templates/admin/membres/recherche_sql.tpl version [eb6cda8094].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
{include file="admin/_head.tpl" title="Recherche par requête SQL" current="membres" js=1}

{include file="admin/membres/_nav.tpl" current="sql"}

{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
	<form method="get" action="{$admin_url}membres/recherche_sql.php">
		<fieldset>
			<legend>Schéma des tables SQL</legend>
			<pre class="sql_schema">{$schema.membres}</pre>
			<dl>
				<dt><label for="f_query">Requête SQL</label></dt>
				<dd class="help">Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée.</dd>
				<dd><textarea name="query" id="f_query" cols="70" rows="7" required="required">{$query}</textarea></dd>
			</dl>
			<p class="submit">
				<input type="submit" name="run" value="Exécuter &rarr;" />
				{if $query}
					{if $id}<input type="hidden" name="id" value="{$id}" />{/if}
					<input type="submit" name="save" value="{if $id}Enregistrer : {$recherche.intitule}{else}Enregistrer cette recherche{/if}" class="minor" />
				{/if}
			</p>
		</fieldset>
	</form>
{/if}

{form_errors}

<form method="post" action="{$admin_url}membres/action.php" class="memberList">

{if !empty($result)}

	<p class="alert">{$result|count} résultats retournés.</p>

	<table class="list search">
		<thead>
			{if isset($result[0]->id)}
			<td class="check"><input type="checkbox" value="Tout cocher / décocher" onclick="g.checkUncheck();" /></td>
			{/if}
			{foreach from=$result[0] key="col" item="ignore"}
				<td>{$col}</td>
			{/foreach}
			{if isset($result[0]->id)}
			<td></td>
			{/if}
		</thead>
		<tbody>
			{foreach from=$result item="row"}
				<tr>
					{if $session->canAccess('membres', Membres::DROIT_ADMIN) && isset($result[0]->id)}
						<td class="check">{if !empty($row.id)}<input type="checkbox" name="selected[]" value="{$row.id}" />{/if}</td>
					{/if}
					{foreach from=$row item="col"}
						<td>{$col}</td>
					{/foreach}
					{if isset($row.id)}
					<td class="actions">
						{if !empty($row.id)}
						<a class="icn" href="{$admin_url}membres/fiche.php?id={$row.id}" title="Fiche membre">👤</a>
						<a class="icn" href="{$admin_url}membres/modifier.php?id={$row.id}" title="Modifier ce membre">✎</a>
						{/if}
					</td>
					{/if}
				</tr>
			{/foreach}
		</tbody>
		{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
			{include file="admin/membres/_list_actions.tpl" colspan=count((array)$result[0])+1}
		{/if}
	</table>

{else}
	<p class="alert">
		Aucun membre trouvé.
	</p>
{/if}

</form>

{include file="admin/_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































Added src/templates/admin/membres/selector.tpl version [6df59fc8ca].

































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{include file="admin/_head.tpl" title="Sélectionner un compte" body_id="popup" is_popup=true}

<form method="get" action="{$self_url_no_qs}">
	<h2 class="ruler">
		<input type="text" placeholder="Recherche rapide de membre" value="{$query}" name="q" />
		<input type="submit" value="Chercher &rarr;" />
	</h2>
</form>

<table class="list">
	<tbody>
    {foreach from=$list item="row"}
        <tr>
        	<td class="num">
        		{$row.numero}
        	</td>
            <th>
                {$row.identite}
            </th>
            <td class="actions">
				<button class="icn-btn" value="{$row.id}" data-label="{$row.numero} — {$row.identite}" data-icon="&rarr;">Sélectionner</button>
			</td>
		</tr>
	{/foreach}
	</tbody>
</table>

{literal}
<script type="text/javascript">
var buttons = document.querySelectorAll('button');

buttons.forEach((e) => {
	e.onclick = () => {
		window.parent.g.inputListSelected(e.value, e.getAttribute('data-label'));
	};
});

if (buttons.length) {
	buttons[0].focus();
}

var rows = document.querySelectorAll('table tbody tr');

if (rows.length == 1) {
	rows[0].querySelector('button').click();
}

rows.forEach((e) => {
	e.classList.add('clickable');

	e.onclick = (evt) => {
		if (evt.target.tagName && evt.target.tagName == 'BUTTON') {
			return;
		}

		e.querySelector('button').click();
	};
});

document.querySelector('input').focus();
</script>
{/literal}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/membres/supprimer.tpl from [06cf105473] to [82816cbfa7].

1
2
3

4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{include file="admin/_head.tpl" title="Supprimer un membre" current="membres"}

<ul class="actions">

    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li class="current"><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>


{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer ce membre ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer le membre «&nbsp;{$membre.identite}&nbsp;» ?
        </h3>
        <p class="alert">
            <strong>Attention</strong> : cette action est irréversible et effacera toutes les
            données personnelles et l'historique de ces membres.
        </p>
        <p class="help">
            Alternativement, il est aussi possible de déplacer les membres qui ne font plus
            partie de l'association dans une catégorie «&nbsp;Anciens membres&nbsp;», plutôt
            que de les effacer complètement.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_membre_"|cat:$membre.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}


|
>
|
|
|
|
|
<
|
>










|












|





1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{include file="admin/_head.tpl" title="Supprimer un membre" current="membres"}

<nav class="tabs">
    <ul>
        <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">{$membre.identite}</a></li>
        <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
        {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
            <li class="current"><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
        {/if}

    </ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer ce membre ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer le membre «&nbsp;{$membre.identite}&nbsp;» ?
        </h3>
        <p class="block alert">
            <strong>Attention</strong> : cette action est irréversible et effacera toutes les
            données personnelles et l'historique de ces membres.
        </p>
        <p class="help">
            Alternativement, il est aussi possible de déplacer les membres qui ne font plus
            partie de l'association dans une catégorie «&nbsp;Anciens membres&nbsp;», plutôt
            que de les effacer complètement.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_membre_"|cat:$membre.id}
        {button type="submit" name="delete" label="Supprimer" shape="delete" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/mes_infos.tpl from [e0d2fe1382] to [acc847c498].

1
2
3

4
5
6

7
8
9
10
11
12
13
{include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1}

<ul class="actions">

    <li class="current"><a href="{$admin_url}mes_infos.php">Mes informations personnelles</a></li>
    <li><a href="{$admin_url}mes_infos_securite.php">Mot de passe et options de sécurité</a></li>
</ul>


{form_errors membre=1}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Informations personnelles</legend>
|

|
>
|
|
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos"}

<nav class="tabs">
    <ul>
        <li class="current"><a href="{$admin_url}mes_infos.php">Mes informations personnelles</a></li>
        <li><a href="{$admin_url}mes_infos_securite.php">Mot de passe et options de sécurité</a></li>
    </ul>
</nav>

{form_errors membre=1}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Informations personnelles</legend>
23
24
25
26
27
28
29
30
31
32
33
34
35
    <fieldset>
        <legend>Changer mon mot de passe</legend>
            <p><a href="{$admin_url}mes_infos_securite.php">Modifier mon mot de passe ou autres informations de sécurité.</a></p>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_me"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}







|





25
26
27
28
29
30
31
32
33
34
35
36
37
    <fieldset>
        <legend>Changer mon mot de passe</legend>
            <p><a href="{$admin_url}mes_infos_securite.php">Modifier mon mot de passe ou autres informations de sécurité.</a></p>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_me"}
        {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/admin/mes_infos_securite.tpl from [6446fe3bc3] to [99becf7d6d].

1
2
3

4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{include file="admin/_head.tpl" title="Mes informations de connexion et sécurité" current="mes_infos" js=1}

<ul class="actions">

    <li><a href="{$admin_url}mes_infos.php">Mes informations personnelles</a></li>
    <li class="current"><a href="{$admin_url}mes_infos_securite.php">Mot de passe et options de sécurité</a></li>
</ul>


{if $ok}
<p class="confirm">
    Changements enregistrés.
</p>
{/if}

{form_errors}

{if $confirm}
    <form method="post" action="{$self_url_no_qs}">

    {if !empty($otp) && $otp == 'disable'}
        <p class="alert">
            Confirmez la désactivation de l'authentification à double facteur TOTP.
        </p>
    {elseif !empty($otp)}
        <p class="alert">
            Confirmez l'activation de l'authentification à double facteur TOTP en l'utilisant une première fois.
        </p>

        <fieldset>
            <legend>Confirmer l'activation de l'authentification à double facteur (2FA)</legend>
            <img class="qrcode" src="{$otp.qrcode}" alt="" />
            <dl>
|

|
>
|
|
|
>


|










|



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{include file="admin/_head.tpl" title="Mes informations de connexion et sécurité" current="mes_infos"}

<nav class="tabs">
    <ul>
        <li><a href="{$admin_url}mes_infos.php">Mes informations personnelles</a></li>
        <li class="current"><a href="{$admin_url}mes_infos_securite.php">Mot de passe et options de sécurité</a></li>
    </ul>
</nav>

{if $ok}
<p class="block confirm">
    Changements enregistrés.
</p>
{/if}

{form_errors}

{if $confirm}
    <form method="post" action="{$self_url_no_qs}">

    {if !empty($otp) && $otp == 'disable'}
        <p class="block alert">
            Confirmez la désactivation de l'authentification à double facteur TOTP.
        </p>
    {elseif !empty($otp)}
        <p class="block alert">
            Confirmez l'activation de l'authentification à double facteur TOTP en l'utilisant une première fois.
        </p>

        <fieldset>
            <legend>Confirmer l'activation de l'authentification à double facteur (2FA)</legend>
            <img class="qrcode" src="{$otp.qrcode}" alt="" />
            <dl>
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
        {csrf_field key="edit_me_security"}
        <input type="hidden" name="passe" value="{form_field name="passe"}" />
        <input type="hidden" name="passe_confirmed" value="{form_field name="passe_confirmed"}" />
        <input type="hidden" name="clef_pgp" value="{form_field name="clef_pgp"}" />
        {if !empty($otp)}
        <input type="hidden" name="otp_secret" value="{$otp.secret}" />
        {/if}
        <input type="submit" name="confirm" value="Confirmer &rarr;" />
    </p>

    </form>
{else}

    <form method="post" action="{$self_url_no_qs}">








|







55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
        {csrf_field key="edit_me_security"}
        <input type="hidden" name="passe" value="{form_field name="passe"}" />
        <input type="hidden" name="passe_confirmed" value="{form_field name="passe_confirmed"}" />
        <input type="hidden" name="clef_pgp" value="{form_field name="clef_pgp"}" />
        {if !empty($otp)}
        <input type="hidden" name="otp_secret" value="{$otp.secret}" />
        {/if}
        {button type="submit" name="confirm" label="Confirmer" shape="right" class="main"}
    </p>

    </form>
{else}

    <form method="post" action="{$self_url_no_qs}">

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
            <legend>Authentification à double facteur (2FA)</legend>
            <p class="help">Pour renforcer la sécurité de votre connexion en cas de vol de votre mot de passe, vous pouvez activer
                l'authentification à double facteur. Cela nécessite d'installer une application comme <a href="https://freeotp.github.io/">FreeOTP</a>
                sur votre téléphone.</p>
            <dl>
                <dt>Authentification à double facteur (TOTP)</dt>
            {if $membre.secret_otp}
                <dd><label><input type="radio" name="otp" value="" checked="checked" /> <strong>Activée</strong></label></dd>
                <dd><label><input type="radio" name="otp" value="generate" /> Régénérer une nouvelle clé secrète</label></dd>
                <dd><label><input type="radio" name="otp" value="disable" /> Désactiver l'authentification à double facteur</label></dd>
            {else}
                <dd><em>Désactivée</em></dd>
                <dd><label><input type="checkbox" name="otp" value="generate" /> Activer</label></dd>
            {/if}
            </dl>
        </fieldset>

        {if $pgp_disponible}
        <fieldset>
            <legend>Protéger mes mails personnels par chiffrement PGP/GnuPG</legend>
            <dl>
                <dt><label for="f_clef_pgp">Ma clé publique PGP</label></dt>
                <dd class="help">En inscrivant ici votre clé publique, tous les emails personnels (non collectifs) qui vous
                    sont envoyés seront chiffrés (cryptés) avec cette clé&nbsp;: messages envoyés par les membres, rappels de cotisation,
                    procédure de récupération de mot de passe, etc.</dd>
                <dd><textarea name="clef_pgp" id="f_clef_pgp" cols="90" rows="5">{form_field name="clef_pgp" data=$user}</textarea></dd>
                {if $clef_pgp_fingerprint}<dd class="help">L'empreinte de la clé est&nbsp;: <code>{$clef_pgp_fingerprint}</code></dd>{/if}
            </dl>
            <p class="alert">
                Attention&nbsp;: en inscrivant ici votre clé PGP, les emails de récupération de mot de passe perdu vous seront envoyés chiffrés
                et ne pourront être lus sans utiliser le mot de passe protégeant votre clé privée correspondante.
            </p>
        </fieldset>
        {/if}

        <p class="submit">
            {csrf_field key="edit_me_security"}
            <input type="submit" name="save" value="Enregistrer &rarr;" />
        </p>

    </form>

    <script type="text/javascript">
    {literal}
    g.script('scripts/password.js').onload = function () {
        initPasswordField('pw_suggest', 'f_passe', 'f_passe_confirmed');
    };
    {/literal}
    </script>
{/if}

{include file="admin/_foot.tpl"}







|
|
|


|















|








|






|

|





94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
            <legend>Authentification à double facteur (2FA)</legend>
            <p class="help">Pour renforcer la sécurité de votre connexion en cas de vol de votre mot de passe, vous pouvez activer
                l'authentification à double facteur. Cela nécessite d'installer une application comme <a href="https://freeotp.github.io/">FreeOTP</a>
                sur votre téléphone.</p>
            <dl>
                <dt>Authentification à double facteur (TOTP)</dt>
            {if $membre.secret_otp}
                {input type="radio" name="otp" value="" default="" label="Activée"}
                {input type="radio" name="otp" value="generate" label="Re-générer une nouvelle clé secrète" help="Si la clé a été compromise ou perdue"}
                {input type="radio" name="otp" value="disable" label="Désactiver l'authentification à double facteur"}
            {else}
                <dd><em>Désactivée</em></dd>
                {input type="checkbox" name="otp" value="generate" label="Activer"}
            {/if}
            </dl>
        </fieldset>

        {if $pgp_disponible}
        <fieldset>
            <legend>Protéger mes mails personnels par chiffrement PGP/GnuPG</legend>
            <dl>
                <dt><label for="f_clef_pgp">Ma clé publique PGP</label></dt>
                <dd class="help">En inscrivant ici votre clé publique, tous les emails personnels (non collectifs) qui vous
                    sont envoyés seront chiffrés (cryptés) avec cette clé&nbsp;: messages envoyés par les membres, rappels de cotisation,
                    procédure de récupération de mot de passe, etc.</dd>
                <dd><textarea name="clef_pgp" id="f_clef_pgp" cols="90" rows="5">{form_field name="clef_pgp" data=$user}</textarea></dd>
                {if $clef_pgp_fingerprint}<dd class="help">L'empreinte de la clé est&nbsp;: <code>{$clef_pgp_fingerprint}</code></dd>{/if}
            </dl>
            <p class="block alert">
                Attention&nbsp;: en inscrivant ici votre clé PGP, les emails de récupération de mot de passe perdu vous seront envoyés chiffrés
                et ne pourront être lus sans utiliser le mot de passe protégeant votre clé privée correspondante.
            </p>
        </fieldset>
        {/if}

        <p class="submit">
            {csrf_field key="edit_me_security"}
            {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
        </p>

    </form>

    <script type="text/javascript">
    {literal}
    g.script('scripts/password.js', () => {
        initPasswordField('pw_suggest', 'f_passe', 'f_passe_confirmed');
    });
    {/literal}
    </script>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/password.tpl from [d9cc01423e] to [c3884ccc30].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Mot de passe oublié ou pas de mot de passe ?"}


{if !empty($sent)}
    <p class="confirm">
        Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail
        pour modifier votre mot de passe.
    </p>
    <p class="alert">
        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
    </p>

{else}

    {form_errors}





|



|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Mot de passe oublié ou pas de mot de passe ?"}


{if !empty($sent)}
    <p class="block confirm">
        Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail
        pour modifier votre mot de passe.
    </p>
    <p class="block alert">
        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
    </p>

{else}

    {form_errors}

27
28
29
30
31
32
33
34
35
36
37
38
39
40
                <dt><label for="f_id">{$champ.title}</label></dt>
                <dd><input type="text" name="id" id="f_id" value="{form_field name=id}" /></dd>
            </dl>
        </fieldset>

        <p class="submit">
            {csrf_field key="recoverPassword"}
            <input type="submit" name="recover" value="Envoyer &rarr;" />
        </p>

    </form>
{/if}

{include file="admin/_foot.tpl"}







|






27
28
29
30
31
32
33
34
35
36
37
38
39
40
                <dt><label for="f_id">{$champ.title}</label></dt>
                <dd><input type="text" name="id" id="f_id" value="{form_field name=id}" /></dd>
            </dl>
        </fieldset>

        <p class="submit">
            {csrf_field key="recoverPassword"}
            {button type="submit" name="recover" label="Envoyer" shape="right" class="main"}
        </p>

    </form>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/password_change.tpl from [bfd4bc96b0] to [d897f07b91].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{include file="admin/_head.tpl" title="Changement de mot de passe" js=1}


{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Modifier mon mot de passe</legend>
		<dl>
			<dt><label for="f_passe_membre">Mot de passe</label> (minimum {$password_length} caractères) <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd class="help">
				Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
				et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
			</dd>
			<dd class="help">
				Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
				<input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
			</dd>
			<dd><input type="password" name="passe" id="f_passe_membre" value="{form_field name=passe}" pattern="{$password_pattern}" required="required" /></dd>
			<dt><label for="f_repasse_membre">Encore le mot de passe</label> (vérification) <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><input type="password" name="passe_confirmed" id="f_repasse_membre" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" required="required" /></dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="changePassword"}
		<input type="submit" name="change" value="Modifier le mot de passe &rarr;" />
	</p>


	<script type="text/javascript">
	{literal}
	g.script('scripts/password.js').onload = function () {
		initPasswordField('pw_suggest', 'f_passe_membre', 'f_repasse_membre');
	};
	{/literal}
	</script>
</form>


{include file="admin/_foot.tpl"}
|


















|

|





|





|
|
|






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{include file="admin/_head.tpl" title="Changement de mot de passe"}


{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Modifier mon mot de passe</legend>
		<dl>
			<dt><label for="f_passe_membre">Mot de passe</label> (minimum {$password_length} caractères) <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd class="help">
				Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr 
				et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres.
			</dd>
			<dd class="help">
				Pas d'idée&nbsp;? Voici une suggestion choisie au hasard :
				<input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="pw_suggest" value="{$passphrase}" autocomplete="off" />
			</dd>
			<dd><input type="password" name="passe" id="f_passe_membre" value="{form_field name=passe}" pattern="{$password_pattern}" required="required" autocomplete="new-password" /></dd>
			<dt><label for="f_repasse_membre">Encore le mot de passe</label> (vérification) <b title="(Champ obligatoire)">obligatoire</b></dt>
			<dd><input type="password" name="passe_confirmed" id="f_repasse_membre" value="{form_field name=passe_confirmed}" pattern="{$password_pattern}" required="required" autocomplete="new-password" /></dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="changePassword"}
		{button type="submit" name="change" label="Modifier le mot de passe" shape="right" class="main"}
	</p>


	<script type="text/javascript">
	{literal}
	g.script('scripts/password.js', () => {
		initPasswordField('pw_suggest', 'f_passe', 'f_repasse');
	});
	{/literal}
	</script>
</form>


{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/_fichiers.tpl from [7aaf5c2a52] to [528fa66749].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{include file="admin/_head.tpl" title="Inclure un fichier" current="wiki" body_id="transparent" is_popup=true js=1}

{form_errors}

<form method="post" enctype="multipart/form-data" action="{$self_url}" id="f_upload">
    <fieldset>
        <legend>Téléverser un fichier</legend>
        <input type="hidden" name="MAX_FILE_SIZE" value="{$max_size}" id="f_maxsize" />
        <dl>
            <dd class="help">Taille maximale : {$max_size|format_bytes}</dd>
            <dd class="fileUpload"><input type="file" name="fichier" id="f_fichier" data-hash-check /></dd>
        </dl>
        <p class="submit">
            {csrf_field key=$csrf_id}
            <input type="submit" name="upload" id="f_submit" value="Envoyer le fichier" />
        </p>
    </fieldset>
</form>

<form method="get" action="#" style="display: none;" id="insertImage">
    <fieldset>
        <h3>Insérer une image dans le texte</h3>
|













|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{include file="admin/_head.tpl" title="Inclure un fichier" current="wiki" body_id="transparent" is_popup=true}

{form_errors}

<form method="post" enctype="multipart/form-data" action="{$self_url}" id="f_upload">
    <fieldset>
        <legend>Téléverser un fichier</legend>
        <input type="hidden" name="MAX_FILE_SIZE" value="{$max_size}" id="f_maxsize" />
        <dl>
            <dd class="help">Taille maximale : {$max_size|format_bytes}</dd>
            <dd class="fileUpload"><input type="file" name="fichier" id="f_fichier" data-hash-check /></dd>
        </dl>
        <p class="submit">
            {csrf_field key=$csrf_id}
            {button type="submit" name="upload" label="Envoyer le fichier" shape="upload" class="main" id="f_submit"}
        </p>
    </fieldset>
</form>

<form method="get" action="#" style="display: none;" id="insertImage">
    <fieldset>
        <h3>Insérer une image dans le texte</h3>
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{if !empty($images)}
<ul class="gallery">
{foreach from=$images item="file"}
    <li>
        <figure>
            <a href="{$file.url}" data-id="{$file.id}"><img src="{$file.thumb}" alt="" title="{$file.nom}" /></a>
            <form class="actions" method="post" action="{$self_url}">
                <a href="{$file.url}" onclick="return !window.open(this.href);" class="icn" title="Télécharger">⇓</a>
                {csrf_field key=$csrf_id}
                <input type="hidden" name="delete" value="{$file.id}" />
                <noscript><input type="submit" value="Supprimer" /></noscript>
            </form>
        </figure>
    </li>
{/foreach}
</ul>
{/if}

{if !empty($fichiers)}
<table class="list">
    <tbody>
    {foreach from=$fichiers item="file"}
        <tr>
            <th>{$file.nom}</th>
            <td>{if $file.type}{$file.type}{/if}</td>
            <td class="actions">
                <form class="actions" method="post" action="{$self_url}">
                    <a href="{$file.url}" onclick="return !window.open(this.href);" class="icn" title="Télécharger">⇓</a>
                    {csrf_field key=$csrf_id}
                    <input type="hidden" name="delete" value="{$file.id}" />
                    <noscript><input type="submit" value="Supprimer" /></noscript>
                </form>
            </td>
        </tr>
    {/foreach}
    </tbody>
</table>
{/if}

{include file="admin/_foot.tpl"}







|



















|












42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{if !empty($images)}
<ul class="gallery">
{foreach from=$images item="file"}
    <li>
        <figure>
            <a href="{$file.url}" data-id="{$file.id}"><img src="{$file.thumb}" alt="" title="{$file.nom}" /></a>
            <form class="actions" method="post" action="{$self_url}">
                {linkbutton shape="download" label="Télécharger" href=$file.url target="_blank"}
                {csrf_field key=$csrf_id}
                <input type="hidden" name="delete" value="{$file.id}" />
                <noscript><input type="submit" value="Supprimer" /></noscript>
            </form>
        </figure>
    </li>
{/foreach}
</ul>
{/if}

{if !empty($fichiers)}
<table class="list">
    <tbody>
    {foreach from=$fichiers item="file"}
        <tr>
            <th>{$file.nom}</th>
            <td>{if $file.type}{$file.type}{/if}</td>
            <td class="actions">
                <form class="actions" method="post" action="{$self_url}">
                    {linkbutton shape="download" label="Télécharger" href=$file.url target="_blank"}
                    {csrf_field key=$csrf_id}
                    <input type="hidden" name="delete" value="{$file.id}" />
                    <noscript><input type="submit" value="Supprimer" /></noscript>
                </form>
            </td>
        </tr>
    {/foreach}
    </tbody>
</table>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/chercher.tpl from [779cd678e9] to [b5b27035db].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{include file="admin/_head.tpl" title="Recherche" current="wiki/chercher"}

<form method="get" action="{$admin_url}wiki/chercher.php" class="wikiSearch">
    <fieldset>
        <legend>Rechercher une page</legend>
        <p>
            <input type="text" name="q" value="{$recherche}" size="25" />
            <input type="submit" value="Chercher" />
        </p>
    </fieldset>
</form>


{if !$recherche}
    <p class="alert">
        Aucun terme recherché.
    </p>
{else}
    <p class="alert">
        <strong>{$nb_resultats}</strong> pages trouvées pour «&nbsp;{$recherche}&nbsp;»
    </p>

    <div class="wikiResults">
    {foreach from=$resultats item="page"}
        <h3><a href="./?{$page.uri}">{$page.titre}</a></h3>
        <p>{$page.snippet|escape|clean_snippet}</p>
    {/foreach}
    </div>
{/if}

{include file="admin/_foot.tpl"}





|

|






|



|












1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{include file="admin/_head.tpl" title="Recherche" current="wiki/chercher"}

<form method="get" action="{$admin_url}wiki/chercher.php" class="wikiSearch">
    <fieldset>
        <legend>Rechercher une page</legend>
        <p class="submit">
            <input type="text" name="q" value="{$recherche}" size="25" />
            {button type="submit" name="search" label="Chercher" shape="search" class="main"}
        </p>
    </fieldset>
</form>


{if !$recherche}
    <p class="block alert">
        Aucun terme recherché.
    </p>
{else}
    <p class="block alert">
        <strong>{$nb_resultats}</strong> pages trouvées pour «&nbsp;{$recherche}&nbsp;»
    </p>

    <div class="wikiResults">
    {foreach from=$resultats item="page"}
        <h3><a href="./?{$page.uri}">{$page.titre}</a></h3>
        <p>{$page.snippet|escape|clean_snippet}</p>
    {/foreach}
    </div>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/creer.tpl from [f972f094f4] to [e927f05760].

10
11
12
13
14
15
16
17
18
19
20
21
22
23
            <dt><label for="f_titre">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="titre" id="f_titre" value="{form_field name=titre}" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="wiki_create"}
        <input type="submit" name="create" value="Créer cette page" />
    </p>

</form>


{include file="admin/_foot.tpl"}







|






10
11
12
13
14
15
16
17
18
19
20
21
22
23
            <dt><label for="f_titre">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="titre" id="f_titre" value="{form_field name=titre}" /></dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="wiki_create"}
        {button type="submit" name="create" label="Créer cette page" shape="right" class="main"}
    </p>

</form>


{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/editer.tpl from [8014427a9f] to [fa476c25ad].

1
2
3
4
5
6
7
8
{include file="admin/_head.tpl" title="Éditer une page" current="wiki" js=1}

{form_errors}

<form method="post" action="{$self_url}" id="f_form">

    <fieldset class="wikiMain">
        <legend>Informations générales</legend>
|







1
2
3
4
5
6
7
8
{include file="admin/_head.tpl" title="Éditer une page" current="wiki"}

{form_errors}

<form method="post" action="{$self_url}" id="f_form">

    <fieldset class="wikiMain">
        <legend>Informations générales</legend>
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    </fieldset>

    <p class="submit">
        {csrf_field key="wiki_edit_%d"|args:$page.id}
        <input type="hidden" name="revision_edition" value="{form_field name=revision_edition default=$page.revision}" />
        <input type="hidden" name="debut_edition" value="{form_field name=debut_edition default=$time}" />
        <input id="f_id" value="{$page.id}" type="hidden" />
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

<script type="text/javascript">
var page_id = '{$page.id}';
{literal}







|







100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    </fieldset>

    <p class="submit">
        {csrf_field key="wiki_edit_%d"|args:$page.id}
        <input type="hidden" name="revision_edition" value="{form_field name=revision_edition default=$page.revision}" />
        <input type="hidden" name="debut_edition" value="{form_field name=debut_edition default=$time}" />
        <input id="f_id" value="{$page.id}" type="hidden" />
        {button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
    </p>

</form>

<script type="text/javascript">
var page_id = '{$page.id}';
{literal}

Modified src/templates/admin/wiki/historique.tpl from [7c0d2ecc52] to [ad74fa04e5].

1
2
3

4
5

6
7
8
9
10
11
12
{include file="admin/_head.tpl" title="Historique : %s"|args:$page.titre current="wiki"}

<ul class="actions">

    <li><a href="{$admin_url}wiki/?{$page.uri}">Retour à la page</a></li>
</ul>


{if !empty($revisions)}
    <table class="list wikiRevisions">
    {foreach from=$revisions item="rev"}
        <tr>
            <td>
                {if $rev.chiffrement}


|
>
|
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
{include file="admin/_head.tpl" title="Historique : %s"|args:$page.titre current="wiki"}

<nav class="tabs">
    <ul>
        <li><a href="{$admin_url}wiki/?{$page.uri}">Retour à la page</a></li>
    </ul>
</nav>

{if !empty($revisions)}
    <table class="list wikiRevisions">
    {foreach from=$revisions item="rev"}
        <tr>
            <td>
                {if $rev.chiffrement}
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
                    {if $rev.revision == 1}
                        diff
                    {else}
                        <a href="?id={$page.id}&amp;diff={$rev.revision-1}.{$rev.revision}">diff</a>
                    {/if}
                {/if}
            </td>
            <th>{$rev.date|date_fr:'d/m/Y à H:i'}</th>
            <td>
                {if $session->canAccess('membres', Membres::DROIT_ACCES)}
                <a href="{$admin_url}membres/fiche.php?id={$rev.id_auteur}">{$rev.nom_auteur}</a>
                {/if}
            </td>
            <td class="length">
                {$rev.taille} octets







|







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
                    {if $rev.revision == 1}
                        diff
                    {else}
                        <a href="?id={$page.id}&amp;diff={$rev.revision-1}.{$rev.revision}">diff</a>
                    {/if}
                {/if}
            </td>
            <th>{$rev.date|date_long}</th>
            <td>
                {if $session->canAccess('membres', Membres::DROIT_ACCES)}
                <a href="{$admin_url}membres/fiche.php?id={$rev.id_auteur}">{$rev.nom_auteur}</a>
                {/if}
            </td>
            <td class="length">
                {$rev.taille} octets
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
            {/if}
            </td>
        </tr>
    {/foreach}
    </table>
{elseif !empty($diff)}
    <div class="wikiRevision revisionLeft">
        <h3>Version du {$rev1.date|date_fr:'d/m/Y à H:i'}</h3>
        {if $session->canAccess('membres', Membres::DROIT_ACCES)}
            <h4>De <a href="{$admin_url}membres/fiche.php?id={$rev1.id_auteur}">{$rev1.nom_auteur}</a></h4>
        {/if}
        {if $rev1.modification}
            <p><em>{$rev1.modification}</em></p>
        {/if}
    </div>
    <div class="wikiRevision revisionRight">
        <h3>Version {if $rev2.revision == $page.revision}actuelle en date{/if} du {$rev2.date|date_fr:'d/m/Y à H:i'}</h3>
        {if $session->canAccess('membres', Membres::DROIT_ACCES)}
            <h4>De <a href="{$admin_url}membres/fiche.php?id={$rev2.id_auteur}">{$rev2.nom_auteur}</a></h4>
        {/if}
        {if $rev2.modification}
            <p><em>{$rev2.modification}</em></p>
        {/if}
    </div>
    {diff old=$rev1.contenu new=$rev2.contenu}
{else}
    <p class="alert">
        Cette page n'a pas d'historique.
    </p>
{/if}


{include file="admin/_foot.tpl"}







|








|









|






51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
            {/if}
            </td>
        </tr>
    {/foreach}
    </table>
{elseif !empty($diff)}
    <div class="wikiRevision revisionLeft">
        <h3>Version du {$rev1.date|date_long}</h3>
        {if $session->canAccess('membres', Membres::DROIT_ACCES)}
            <h4>De <a href="{$admin_url}membres/fiche.php?id={$rev1.id_auteur}">{$rev1.nom_auteur}</a></h4>
        {/if}
        {if $rev1.modification}
            <p><em>{$rev1.modification}</em></p>
        {/if}
    </div>
    <div class="wikiRevision revisionRight">
        <h3>Version {if $rev2.revision == $page.revision}actuelle en date{/if} du {$rev2.date|date_long}</h3>
        {if $session->canAccess('membres', Membres::DROIT_ACCES)}
            <h4>De <a href="{$admin_url}membres/fiche.php?id={$rev2.id_auteur}">{$rev2.nom_auteur}</a></h4>
        {/if}
        {if $rev2.modification}
            <p><em>{$rev2.modification}</em></p>
        {/if}
    </div>
    {diff old=$rev1.contenu new=$rev2.contenu}
{else}
    <p class="block alert">
        Cette page n'a pas d'historique.
    </p>
{/if}


{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/page.tpl from [29bcb6b0e7] to [fc2fcb0602].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{if !empty($page.titre) && $can_read}
    {include file="admin/_head.tpl" title=$page.titre current="wiki" js=1}
{else}
    {include file="admin/_head.tpl" title="Wiki" current="wiki"}
{/if}

<ul class="actions">

    {if $session->canAccess('wiki', Membres::DROIT_ECRITURE)}
        <li><a href="{$admin_url}wiki/creer.php?parent={if $page && $config.accueil_wiki != $page.uri}{$page.id}{else}0{/if}"><strong>Créer une nouvelle page</strong></a></li>
    {/if}
    {if $can_edit}
        <li><a href="{$admin_url}wiki/editer.php?id={$page.id}">Éditer</a></li>
    {/if}
    {if $can_read && $page && $page.contenu}
        <li><a href="{$admin_url}wiki/historique.php?id={$page.id}">Historique</a>
        {if $page.droit_lecture == Wiki::LECTURE_PUBLIC}
            <li><a href="{$www_url}{$page.uri}{if $has_public_children}/{/if}">Voir sur le site</a>
        {/if}
    {/if}
    {if $can_edit && $session->canAccess('wiki', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}wiki/supprimer.php?id={$page.id}">Supprimer</a></li>
    {/if}
</ul>


{if !$can_read}
    <p class="alert">Vous n'avez pas le droit de lire cette page.</p>
{else}
    <div class="breadCrumbs">
        <ul>
            <li><a href="./">Wiki</a></li>
            {if !empty($breadcrumbs)}
            {foreach from=$breadcrumbs item="crumb"}
            <li><a href="?{$crumb.uri}">{$crumb.titre}</a></li>
            {/foreach}
            {/if}
        </ul>
    </div>

    {if !$page}
        <p class="error">
            Cette page n'existe pas.
        </p>

        {if $can_edit}
        <form method="post" action="{$admin_url}wiki/creer.php">
            <p class="submit">
                {csrf_field key="wiki_create"}
                <input type="hidden" name="titre" value="{$uri}" />
                <input type="submit" name="create" value="Créer cette page" />
            </p>
        </form>
        {/if}
    {else}

        {if !empty($children)}
        <div class="wikiChildren">
            <h4>Dans cette rubrique</h4>
            <ul>
            {foreach from=$children item="child"}
                <li><a href="?{$child.uri}">{$child.titre}</a></li>
            {/foreach}
            </ul>
        </div>
        {/if}

        {if !$page.contenu}
            <p class="alert">Cette page est vide, cliquez sur « Éditer » pour la modifier.</p>
        {else}

            {if $page.contenu.chiffrement}
                <noscript>
                    <div class="error">
                        Vous dever activer javascript pour pouvoir déchiffrer cette page.
                    </div>
                </noscript>
                <script type="text/javascript" src="{$admin_url}static/scripts/wiki-encryption.js"></script>
                <div id="wikiEncryptedMessage">
                    <p class="alert">Cette page est chiffrée.
                        <input type="button" onclick="return wikiDecrypt(false);" value="Entrer le mot de passe" />
                    </p>
                </div>
                <div class="wikiContent" style="display: none;" id="wikiEncryptedContent">
                    {$page.contenu.contenu}
                </div>
            {else}

|




|
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>


|













|








|

















|










|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
{if !empty($page.titre) && $can_read}
    {include file="admin/_head.tpl" title=$page.titre current="wiki"}
{else}
    {include file="admin/_head.tpl" title="Wiki" current="wiki"}
{/if}

<nav class="tabs">
    <ul>
        {if $session->canAccess('wiki', Membres::DROIT_ECRITURE)}
            <li><a href="{$admin_url}wiki/creer.php?parent={if $page && $config.accueil_wiki != $page.uri}{$page.id}{else}0{/if}"><strong>Créer une nouvelle page</strong></a></li>
        {/if}
        {if $can_edit}
            <li><a href="{$admin_url}wiki/editer.php?id={$page.id}">Éditer</a></li>
        {/if}
        {if $can_read && $page && $page.contenu}
            <li><a href="{$admin_url}wiki/historique.php?id={$page.id}">Historique</a></li>
            {if $page.droit_lecture == Wiki::LECTURE_PUBLIC}
                <li><a href="{$www_url}{$page.uri}{if $has_public_children}/{/if}">Voir sur le site</a></li>
            {/if}
        {/if}
        {if $can_edit && $session->canAccess('wiki', Membres::DROIT_ADMIN)}
            <li><a href="{$admin_url}wiki/supprimer.php?id={$page.id}">Supprimer</a></li>
        {/if}
    </ul>
</nav>

{if !$can_read}
    <p class="block alert">Vous n'avez pas le droit de lire cette page.</p>
{else}
    <div class="breadCrumbs">
        <ul>
            <li><a href="./">Wiki</a></li>
            {if !empty($breadcrumbs)}
            {foreach from=$breadcrumbs item="crumb"}
            <li><a href="?{$crumb.uri}">{$crumb.titre}</a></li>
            {/foreach}
            {/if}
        </ul>
    </div>

    {if !$page}
        <p class="block error">
            Cette page n'existe pas.
        </p>

        {if $can_edit}
        <form method="post" action="{$admin_url}wiki/creer.php">
            <p class="submit">
                {csrf_field key="wiki_create"}
                <input type="hidden" name="titre" value="{$uri}" />
                {button type="submit" name="create" label="Créer cette page" shape="right" class="main"}
            </p>
        </form>
        {/if}
    {else}

        {if !empty($children)}
        <div class="wikiChildren">
            <h4>Dans cette rubrique</h4>
            <ul>
            {foreach from=$children item="child"}
                <li><a href="?{$child.uri}">{$child.titre}</a></li>
            {/foreach}
            </ul>
        </div>
        {/if}

        {if !$page.contenu}
            <p class="block alert">Cette page est vide, cliquez sur « Éditer » pour la modifier.</p>
        {else}

            {if $page.contenu.chiffrement}
                <noscript>
                    <div class="error">
                        Vous dever activer javascript pour pouvoir déchiffrer cette page.
                    </div>
                </noscript>
                <script type="text/javascript" src="{$admin_url}static/scripts/wiki-encryption.js"></script>
                <div id="wikiEncryptedMessage">
                    <p class="block alert">Cette page est chiffrée.
                        <input type="button" onclick="return wikiDecrypt(false);" value="Entrer le mot de passe" />
                    </p>
                </div>
                <div class="wikiContent" style="display: none;" id="wikiEncryptedContent">
                    {$page.contenu.contenu}
                </div>
            {else}
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
                {/if}

                {if !empty($fichiers)}
                <ul class="files">
                    {foreach from=$fichiers item="file"}
                        <li>
                            <aside class="fichier" class="internal-file"><a href="{$file.url}">{$file.nom}</a>
                            <small>({$file.type}, {$file.taille|format_bytes})</small>
                       </li>
                    {/foreach}
                </ul>
                {/if}
            </div>
            {/if}

            <p class="wikiFooter">
                Dernière modification le {$page.date_modification|date_fr:'d/m/Y à H:i'}
                {if $session->canAccess('membres', Membres::DROIT_ACCES)}
                par <a href="{$admin_url}membres/fiche.php?id={$page.contenu.id_auteur}">{$auteur}</a>
                {/if}
            </p>
        {/if}
    {/if}
{/if}


{include file="admin/_foot.tpl"}







|








|










108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
                {/if}

                {if !empty($fichiers)}
                <ul class="files">
                    {foreach from=$fichiers item="file"}
                        <li>
                            <aside class="fichier" class="internal-file"><a href="{$file.url}">{$file.nom}</a>
                            <small>({$file.type}, {$file.taille|format_bytes})</small></aside>
                       </li>
                    {/foreach}
                </ul>
                {/if}
            </div>
            {/if}

            <p class="wikiFooter">
                Dernière modification le {$page.date_modification|date_long}
                {if $session->canAccess('membres', Membres::DROIT_ACCES)}
                par <a href="{$admin_url}membres/fiche.php?id={$page.contenu.id_auteur}">{$auteur}</a>
                {/if}
            </p>
        {/if}
    {/if}
{/if}


{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/recent.tpl from [7d673143bc] to [b616432b4b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="admin/_head.tpl" title="Pages modifiées récemment" current="wiki/recent"}

{if !empty($list)}
    <table class="list">
        <tbody>
        {foreach from=$list item="page"}
        <tr>
            <th><a href="{$admin_url}wiki/?{$page.uri}">{$page.titre}</a></th>
            <td>{$page.date_modification|date_fr:'d/m/Y à H:i'}</td>
        </tr>
        {/foreach}
        </tbody>
    </table>

    {pagination url="?p=[ID]" page=$current_page bypage=$bypage total=$total}
{else}
    <p class="alert">Pas de modification récente.</p>
{/if}

{include file="admin/_foot.tpl"}








|







|



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="admin/_head.tpl" title="Pages modifiées récemment" current="wiki/recent"}

{if !empty($list)}
    <table class="list">
        <tbody>
        {foreach from=$list item="page"}
        <tr>
            <th><a href="{$admin_url}wiki/?{$page.uri}">{$page.titre}</a></th>
            <td>{$page.date_modification|date_long}</td>
        </tr>
        {/foreach}
        </tbody>
    </table>

    {pagination url="?p=[ID]" page=$current_page bypage=$bypage total=$total}
{else}
    <p class="block alert">Pas de modification récente.</p>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/admin/wiki/supprimer.tpl from [dc6d537e13] to [0d139503af].

1
2
3

4
5
6
7
8

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{include file="admin/_head.tpl" title="Supprimer : %s"|args:$page.titre current="wiki"}

<ul class="actions">

    <li><a href="{$admin_url}wiki/"><strong>Wiki</strong></a></li>
    <li><a href="{$admin_url}wiki/chercher.php">Rechercher</a></li>
    <li><a href="{$admin_url}wiki/?{$page.uri}">Voir la page</a></li>
    <li><a href="{$admin_url}wiki/editer.php?id={$page.id}">Éditer</a></li>
</ul>


{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer cette page du wiki ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer la page «&nbsp;{$page.titre}&nbsp;» ?
        </h3>
        <p class="help">
            La page ne pourra pas être supprimée si d'autres pages l'utilisent comme rubrique
            parente.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_wiki_"|cat:$page.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}


|
>
|
|
|
|
|
>


















|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{include file="admin/_head.tpl" title="Supprimer : %s"|args:$page.titre current="wiki"}

<nav class="tabs">
    <ul>
        <li><a href="{$admin_url}wiki/"><strong>Wiki</strong></a></li>
        <li><a href="{$admin_url}wiki/chercher.php">Rechercher</a></li>
        <li><a href="{$admin_url}wiki/?{$page.uri}">Voir la page</a></li>
        <li><a href="{$admin_url}wiki/editer.php?id={$page.id}">Éditer</a></li>
    </ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer cette page du wiki ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer la page «&nbsp;{$page.titre}&nbsp;» ?
        </h3>
        <p class="help">
            La page ne pourra pas être supprimée si d'autres pages l'utilisent comme rubrique
            parente.
        </p>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_wiki_"|cat:$page.id}
        {button type="submit" name="delete" label="Supprimer" shape="right" class="main"}
    </p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/common/_csv_help.tpl version [2cbd2ae88a].























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
<dd class="help">
	Règles à suivre pour créer le fichier CSV&nbsp;:
	<ul>
		<li>Il est recommandé d'utiliser LibreOffice pour créer le fichier CSV</li>
		<li>Le fichier doit être en UTF-8</li>
		<li>Le séparateur doit être le point-virgule ou la virgule</li>
		<li>Cocher l'option <em>"Mettre en guillemets toutes les cellules du texte"</em></li>
		<li>Le fichier doit comporter les colonnes suivantes : <em>{$csv->getColumnsString()}</em></li>
		<li>Le fichier peut également comporter les colonnes suivantes : <em>{$csv->getMandatoryColumnsString()}</em></li>
	</ul>
</dd>

Added src/templates/common/_csv_match_columns.tpl version [f8a953f97c].

























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<fieldset>
	<legend>Importer depuis un fichier CSV générique</legend>
	<dl>
		<dd class="help">{$csv->count()} lignes trouvées dans le fichier</dd>
		<dt>{input type="checkbox" name="skip_first_line" value="1" label="Ne pas importer la première ligne" help="Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes, mais des données" default=1}
		<dt><label>Correspondance des colonnes</label></dt>
		<dd>
			<table class="list auto">
				<tbody>
				{foreach from=$csv->getFirstLine() key="index" item="csv_field"}
					<tr>
						<th>{$csv_field}</th>
						<td>
							<select name="translation_table[{$index}]">
							<?php $selected = $csv->getSelectedTable(); ?>
								<option value="">-- Ne pas importer cette colonne</option>
								{foreach from=$csv->getColumns() item="label" key="key"}
									<option value="{$key}" {if $selected[$index] == $key}selected="selected"{/if}>{$label}</option>
								{/foreach}
							</select>
						</td>
					</tr>
				{/foreach}
				</tbody>
			</table>
		</dd>
	</dl>
</fieldset>

Added src/templates/common/delete_file.tpl version [42a5f97ba7].

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
{include file="admin/_head.tpl" title="Supprimer un fichier" current=null}

{include file="common/delete_form.tpl"
	legend="Supprimer ce fichier ?"
	warning="Êtes-vous sûr de vouloir supprimer le fichier « %s » ?"|args:$file.nom
}

{include file="admin/_foot.tpl"}

Added src/templates/common/delete_form.tpl version [089a0cfbe1].























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>{$legend}</legend>
		<h3 class="warning">
			{$warning}
		</h3>
		{if isset($alert)}
		<p class="block alert">
			{$alert}
		</p>
		{/if}
		{if isset($info)}
		<p class="help">
			{$info}
		</p>
		{/if}
		{if isset($confirm)}
		<p>
			{input type="checkbox" name="confirm_delete" value=1 label=$confirm}
		</p>
		{/if}
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="delete" label="Supprimer" shape="delete" class="main"}
		{if isset($extra)}
			{foreach from=$extra key="key" item="value"}
				{if is_array($value)}
					{foreach from=$value key="subkey" item="subvalue"}
						<input type="hidden" name="{$key}[{$subkey}]" value="{$subvalue}" />
					{/foreach}
				{else}
					<input type="hidden" name="{$key}" value="{$value}" />
				{/if}
			{/foreach}
		{/if}
	</p>

</form>

Added src/templates/common/dynamic_list_head.tpl version [0d6279460e].







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<table class="list">
	<thead class="userOrder">
		<tr>
			{if !empty($check)}
			<td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>
			{/if}
			{foreach from=$list->getHeaderColumns() key="key" item="column"}
			<td class="{if $list->order == $key}cur {if $list->desc}desc{else}asc{/if}{/if}">
				{$column.label}
				{if !array_key_exists('select', $column) || !is_null($column['select'])}
				<a href="{$list->orderURL($key, false)}" class="icn up">&uarr;</a>
				<a href="{$list->orderURL($key, true)}" class="icn dn">&darr;</a>
				{/if}
			</td>
			{/foreach}
			<td></td>
		</tr>
	</thead>
	<tbody>

Added src/templates/common/search/advanced.tpl version [793be9f367].





































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<?php
assert(isset($columns));
assert(isset($action_url));
assert(isset($query));
assert(isset($is_admin));
$sql_disabled = !$is_admin;
?>

{form_errors}

<form method="post" action="{$action_url}" id="queryBuilderForm">
	<fieldset>
	{if $sql_query}
		<legend>Schéma des tables SQL</legend>
		<pre class="sql_schema">{foreach from=$schema item="table"}{$table}<br />{/foreach}</pre>
		<dl>
			{input type="textarea" name="sql_query" cols="100" rows="7" required=1 label="Requête SQL" help="Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée." default=$sql_query disabled=$sql_disabled}
		</dl>
		<p class="submit">
			{button type="submit" name="run" label="Exécuter" shape="search" class="main"}
			<input type="hidden" name="id" value="{$search.id}" />
			{if $search.id}
				{button name="save" value=1 type="submit" label="Enregistrer : %s"|args:$search.intitule|truncate:40:"…":true shape="upload"}
			{else}
				{button name="save" value=1 type="submit" label="Enregistrer cette recherche" shape="upload"}
			{/if}
		</p>
	{else}
		<legend>Rechercher</legend>
		<div class="queryBuilder" id="queryBuilder"></div>
		<p class="actions">
			<label>Trier par
				<select name="order">
					{foreach from=$columns key="column" item="properties"}
					<option value="{$column}"{if $query.order == $column} selected="selected"{/if}>{$properties.label}</option>
					{/foreach}
				</select>
			</label>
			<input type="checkbox" name="desc" value="1" {if $query.desc}checked="checked"{/if} id="f_desc" /> <label for="f_desc">Tri inversé</label>
			<label>Limiter à <input type="number" value="{$query.limit}" name="limit" size="5" /> résultats</label>
		</p>
		<p class="submit">
			{button name="search" value=1 type="submit" label="Chercher" shape="search" id="send" class="main"}
			<input type="hidden" name="q" id="jsonQuery" />
			<input type="hidden" name="id" value="{$search.id}" />
			{if $search.id}
				{button name="save" value=1 type="submit" label="Enregistrer : %s"|args:$search.intitule|truncate:40:"…":true shape="upload"}
			{else}
				{button name="save" value=1 type="submit" label="Enregistrer cette recherche" shape="upload"}
			{/if}
			{if $is_admin}
				{button name="to_sql" value=1 type="submit" label="Recherche SQL" shape="edit"}
			{/if}
		</p>
	{/if}
	</fieldset>
</form>

<script type="text/javascript">
var columns = {$columns|escape:'json'};

{literal}
var translations = {
	"after": "après",
	"before": "avant",
	"is equal to": "est égal à",
	"is equal to one of": "est égal à une des ces options",
	"is not equal to one of": "n'est pas égal à une des ces options",
	"is not equal to": "n'est pas égal à",
	"is greater than": "est supérieur à",
	"is greater than or equal to": "est supérieur ou égal à",
	"is less than": "est inférieur à",
	"is less than or equal to": "est inférieur ou égal à",
	"is between": "est situé entre",
	"is not between": "n'est pas situé entre",
	"is null": "est nul",
	"is not null": "n'est pas nul",
	"begins with": "commence par",
	"doesn't begin with": "ne commence pas par",
	"ends with": "se termine par",
	"doesn't end with": "ne se termine pas par",
	"contains": "contient",
	"doesn't contain": "ne contient pas",
	"matches one of": "correspond à",
	"is true": "oui",
	"is false": "non",
	"Matches ALL of the following conditions:": "Correspond à TOUS les critères suivants :",
	"Matches ANY of the following conditions:": "Correspond à UN des critères suivants :",
	"Add a new set of conditions below this one": "-- Ajouter un groupe de critères",
	"Remove this set of conditions": "-- Supprimer ce groupe de critères"
};

var q = new SQLQueryBuilder(columns);
q.__ = function (str) {
	return translations[str];
};
q.loadDefaultOperators();
q.buildInput = function (type, label, column) {
	if (label == '+')
	{
		label = '➕';
	}
	else if (label == '-')
	{
		label = '➖';
	}

	if (type == 'button')
	{
		var i = document.createElement('button');
		i.className = 'icn-btn';
		i.type = 'button';
		i.setAttribute('data-icon', label);
	}
	else {
		var i = document.createElement('input');
		i.type = type == 'integer' ? 'number' : type;
		i.value = label;
	}

	return i;
};
q.init(document.getElementById('queryBuilder'));

$('#queryBuilderForm').onsubmit = function () {
	$('#jsonQuery').value = JSON.stringify(q.export());
};
{/literal}
q.import({$query.query|escape:'json'});
</script>

Modified src/templates/common/search/saved_searches.tpl from [3685bb7adf] to [22a975d3e5].

1
2

3








4

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

68
69
70
71
72
73
74
75
76
{include file="admin/_head.tpl" title="Recherches enregistrées" current="membres"}


{include file="admin/membres/_nav.tpl" current="recherches"}










{form_errors}

{if $mode == 'edit'}
	<form method="post" action="{$self_url}">
		<fieldset>
			<legend>Modifier une recherche enregistrée</legend>
			<dl>
				<dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
				<dd><input type="text" name="intitule" id="f_intitule" value="{form_field name="intitule" data=$recherche}" size="80" required="required" /></dd>
				<dt>Statut</dt>
				<dd><label><input type="radio" name="prive" value="1" {if $recherche.id_membre}checked="checked"{/if} /> Recherche privée</label> — Visible seulement par moi-même</dd>
				<dd><label><input type="radio" name="prive" value="0" {if !$recherche.id_membre}checked="checked"{/if} /> Recherche publique</label> — Visible et exécutable par tous les membres ayant accès à la gestion des membres</dd>
				<dt>Type</dt>
				<dd>{if $recherche.type == Recherche::TYPE_JSON}Avancée{else}SQL{/if}</dd>
				<dt>Cible</dt>
				<dd>{$recherche.cible}</dd>
			</dl>
		</fieldset>

		<p class="submit">
			{csrf_field key="edit_recherche_%s"|args:$recherche.id}
			<input type="submit" name="save" value="Enregistrer &rarr;" />
		</p>
	</form>
{elseif $mode == 'delete'}

	<form method="post" action="{$self_url}">
		<fieldset>
			<legend>Supprimer une recherche enregistrée</legend>
			<h3 class="warning">
				Êtes-vous sûr de vouloir supprimer la recherche enregistrée
				{$recherche.intitule}&nbsp;?
			</h3>
		</fieldset>

		<p class="submit">
			{csrf_field key="del_recherche_%s"|args:$recherche.id}
			<input type="submit" name="delete" value="Supprimer &rarr;" />
		</p>
	</form>
{elseif count($liste) == 0}
	<p class="alert">Aucune recherche enregistrée. <a href="{$admin_url}membres/recherche.php">Faire une nouvelle recherche</a></p>
{else}
	<table class="list">
		<thead>
			<tr>
				<th>Recherche</th>
				<th>Type</th>
				<th>Statut</th>
				<th></th>
			</tr>
		</thead>
		<tbody>
			{foreach from=$liste item="recherche"}
			<tr>
				<th>{$recherche.intitule}</th>
				<td>{if $recherche.type == Recherche::TYPE_JSON}Avancée{else}SQL{/if}</td>
				<td>{if !$recherche.id_membre}Publique{else}Privée{/if}</td>
				<td class="actions">
					<a href="{$admin_url}membres/recherche{if $recherche.type == Recherche::TYPE_SQL}_sql{/if}.php?id={$recherche.id}" class="icn" title="Exécuter">𝍢</a>
					{if $recherche.id_membre || $session->canAccess('membres', Membres::DROIT_ADMIN)}
					<a href="{$admin_url}membres/recherches.php?edit={$recherche.id}" class="icn" title="Modifier">✎</a>
					<a href="{$admin_url}membres/recherches.php?delete={$recherche.id}" class="icn" title="Supprimer">✘</a>

					{/if}
				</td>
			</tr>
			{/foreach}
		</tbody>
	</table>
{/if}

{include file="admin/_foot.tpl"}
|

>
|
>
>
>
>
>
>
>
>

>
|

<








|









|




|
<
|
<
|
|
<
<
|
|
<
<
<
<

|













|



|
|
|
|
>









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41

42
43


44
45




46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{include file="admin/_head.tpl" title="Recherches enregistrées" current=$target}

{if $target == 'membres'}
	{include file="admin/membres/_nav.tpl" current="recherches"}
{else}
	<nav class="tabs">
		<ul>
			<li><a href="search.php">Recherche</a></li>
			<li class="current"><a href="saved_searches.php">Recherches enregistrées</a></li>
		</ul>
	</nav>
{/if}

{if $mode == 'edit'}
	{form_errors}


	<form method="post" action="{$self_url}">
		<fieldset>
			<legend>Modifier une recherche enregistrée</legend>
			<dl>
				<dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
				<dd><input type="text" name="intitule" id="f_intitule" value="{form_field name="intitule" data=$recherche}" size="80" required="required" /></dd>
				<dt>Statut</dt>
				<dd><label><input type="radio" name="prive" value="1" {if $recherche.id_membre}checked="checked"{/if} /> Recherche privée</label> — Visible seulement par moi-même</dd>
				<dd><label><input type="radio" name="prive" value="0" {if !$recherche.id_membre}checked="checked"{/if} /> Recherche publique</label> — Visible et exécutable par tous les membres ayant accès à la gestion {$target}</dd>
				<dt>Type</dt>
				<dd>{if $recherche.type == Recherche::TYPE_JSON}Avancée{else}SQL{/if}</dd>
				<dt>Cible</dt>
				<dd>{$recherche.cible}</dd>
			</dl>
		</fieldset>

		<p class="submit">
			{csrf_field key="edit_recherche_%s"|args:$recherche.id}
			{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
		</p>
	</form>
{elseif $mode == 'delete'}

	{include file="common/delete_form.tpl"

		legend="Supprimer cette recherche enregistrée ?"

		warning="Êtes-vous sûr de vouloir supprimer la recherche enregistrée « %s » ?"|args:$recherche.intitule
		csrf_key="del_recherche_%s"|args:$recherche.id


	}





{elseif count($liste) == 0}
	<p class="block alert">Aucune recherche enregistrée. <a href="{$search_url}">Faire une nouvelle recherche</a></p>
{else}
	<table class="list">
		<thead>
			<tr>
				<th>Recherche</th>
				<th>Type</th>
				<th>Statut</th>
				<th></th>
			</tr>
		</thead>
		<tbody>
			{foreach from=$liste item="recherche"}
			<tr>
				<th><a href="{$search_url}?id={$recherche.id}">{$recherche.intitule}</a></th>
				<td>{if $recherche.type == Recherche::TYPE_JSON}Avancée{else}SQL{/if}</td>
				<td>{if !$recherche.id_membre}Publique{else}Privée{/if}</td>
				<td class="actions">
					{linkbutton href="%s?id=%d"|args:$search_url,$recherche.id shape="search" label="Rechercher"}
					{if $recherche.id_membre || $session->canAccess($target, Membres::DROIT_ADMIN)}
						{linkbutton href="?duplicate=%d"|args:$recherche.id shape="export" label="Dupliquer"}
						{linkbutton href="?edit=%d"|args:$recherche.id shape="edit" label="Modifier"}
						{linkbutton href="?delete=%d"|args:$recherche.id shape="delete" label="Supprimer"}
					{/if}
				</td>
			</tr>
			{/foreach}
		</tbody>
	</table>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/error.tpl from [5457db8bcb] to [6a0842893f].

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    </style>
</head>

<body>

<h1>{if empty($title)}Erreur{else}{$title}{/if}</h1>

<p class="error">
    {$error|escape|nl2br}
</p>

<p>
    <a href="{$admin_url}" onclick="history.back(); return false;">&larr; Retour</a>
</p>

</body>
</html>







|









29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    </style>
</head>

<body>

<h1>{if empty($title)}Erreur{else}{$title}{/if}</h1>

<p class="block error">
    {$error|escape|nl2br}
</p>

<p>
    <a href="{$admin_url}" onclick="history.back(); return false;">&larr; Retour</a>
</p>

</body>
</html>

Modified src/templates/my_services.tpl from [2423b2fa48] to [79bef75320].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70

71
72
73
74
75
76

77
78


79
80
81
{include file="admin/_head.tpl" title="Mes cotisations" current="mes_cotisations"}

<dl class="cotisation">
    <dt>
        {if $nb_activites == 1}
            Vous avez {$nb_activites} cotisation enregistrée.
        {elseif $nb_activites}
            Vous avez {$nb_activites} cotisations enregistrées.
        {else}
            Vous n'avez aucune cotisation enregistrée.
        {/if} 
    </dt>
{if $cotisation}
    <dt>Cotisation obligatoire</dt>
    <dd>{$cotisation.intitule} — 
        {if $cotisation.duree}
            {$cotisation.duree} jours
        {elseif $cotisation.debut}
            du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french}
        {else}
            ponctuelle
        {/if}
        — {$cotisation.montant|escape|html_money} {$config.monnaie}
    </dd>

    <dd>

        {if !$cotisation.a_jour}
            <b class="error">Vous n'êtes pas à jour de cotisation</b>
        {else}
            <b class="confirm">&#10003; À jour de cotisation</b>
            {if $cotisation.expiration}
                (expire le {$cotisation.expiration|format_sqlite_date_to_french})
            {/if}
        {/if}
    </dd>
{/if}
{if !empty($cotisations_membre)}
    <dt>Cotisations en cours</dt>
    {foreach from=$cotisations_membre item="co"}
    <dd>{$co.intitule} — 
        {if $co.a_jour}
            <span class="confirm">À jour</span>{if $co.expiration} — Expire le {$co.expiration|format_sqlite_date_to_french}{/if}
        {else}
            <span class="error">En retard</span>
        {/if}

    </dd>
    {/foreach}
{/if}
</dl>

{if !empty($cotisations)}
<div class="infos">
    <h3>Historique des cotisations</h3>
</div>

<table class="list">
    <thead>
        <th>Date</th>
        <td>Cotisation</td>
    </thead>
    <tbody>
        {foreach from=$cotisations item="c"}
            <tr>

                <td>{$c.date|format_sqlite_date_to_french}</td>
                <td>
                    {$c.intitule} — 
                    {if $c.duree}
                        {$c.duree} jours
                    {elseif $c.debut}
                        du {$c.debut|format_sqlite_date_to_french} au {$c.fin|format_sqlite_date_to_french}
                    {else}

                        ponctuelle
                    {/if}
                    — {$c.montant|escape|html_money} {$config.monnaie}
                </td>
            </tr>
        {/foreach}

    </tbody>
</table>


{/if}

{include file="admin/_foot.tpl"}
|


<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
>
|
>
|
|
|
|
<
<
|
|
|
<
<
<
|
|
<
<
<
<
<
>
|
|
<


|
|
|
<

<
|
|
<
<
<
|
|
>
|
|
<
|
<
<
<
|
>
|
<
<
|
|
|
>
|
|
>
>



1
2
3









4











5
6
7
8
9
10
11


12
13
14



15
16





17
18
19

20
21
22
23
24

25

26
27



28
29
30
31
32

33



34
35
36


37
38
39
40
41
42
43
44
45
46
47
{include file="admin/_head.tpl" title="Mes activités & cotisations" current="my_services"}

<dl class="cotisation">









	<dt>Mes activités et cotisations</dt>











	{foreach from=$services item="service"}
	<dd>
		{$service.label}
		{if $service.status == -1 && $service.end_date} — terminée
		{elseif $service.status == -1}  <b class="error">en retard</b>
		{elseif $service.status == 1 && $service.end_date} — <b class="confirm">en cours</b>
		{elseif $service.status == 1} — <b class="confirm">à jour</b>{/if}


		{if $service.status.expiry_date}  expire le {$service.expiry_date|date_short}{/if}
		{if !$service.paid}  <b class="error">À payer&nbsp;!</b>{/if}
	</dd>



	{foreachelse}
	<dd>





		Vous n'êtes inscrit à aucune activité ou cotisation.
	</dd>
	{/foreach}

</dl>

{if $list->count()}

	<h2 class="ruler">Historique des inscriptions</h2>



	{include file="common/dynamic_list_head.tpl"}




		{foreach from=$list->iterate() item="row"}
			<tr>
				<th>{$row.label}</th>
				<td>{$row.date|date_short}</td>
				<td>{$row.expiry|date_short}</td>

				<td>{$row.fee}</td>



				<td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td>
				<td>{$row.amount|raw|money_currency}</td>
				<td class="actions">


				</td>
			</tr>
		{/foreach}

		</tbody>
	</table>

	{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}
{/if}

{include file="admin/_foot.tpl"}

Added src/templates/services/_nav.tpl version [a7923367e8].



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}services/">Activités et cotisations</a></li>
		<li{if $current == 'save'} class="current"{/if}><a href="{$admin_url}services/save.php">Inscrire à une activité</a></li>
		{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
			<li{if $current == 'reminders'} class="current"{/if}><a href="{$admin_url}services/reminders/">Gestion des rappels automatiques</a></li>
		{/if}
	</ul>

	{if isset($current_service)}
	<ul class="sub">
		<li class="title">
			{$current_service.label} —
			{if $current_service.duration}
				{$current_service.duration} jours
			{elseif $current_service.start_date}
				du {$current_service.start_date|date_short} au {$current_service.end_date|date_short}
			{else}
				ponctuelle
			{/if}
		</li>
		<li{if $service_page == 'index'} class="current"{/if}><a href="{$admin_url}services/fees/?id={$current_service.id}"><strong>Gestion des tarifs</strong></a></li>
		<li{if $service_page == 'paid'} class="current"{/if}><a href="{$admin_url}services/details.php?id={$current_service.id}">À jour et payés</a></li>
		<li{if $service_page == 'expired'} class="current"{/if}><a href="{$admin_url}services/details.php?id={$current_service.id}&amp;type=expired">Inscription expirée</a></li>
		<li{if $service_page == 'unpaid'} class="current"{/if}><a href="{$admin_url}services/details.php?id={$current_service.id}&amp;type=unpaid">En attente de règlement</a></li>
	</ul>
	{/if}

	{if isset($current_fee)}
	<ul class="sub">
		<li class="title">
			{$current_fee.label}
			{if $current_fee.amount} — {$current_fee.amount|money_currency|raw}{/if}
		</li>
		<li{if $fee_page == 'paid'} class="current"{/if}><a href="{$admin_url}services/fees/details.php?id={$current_fee.id}">À jour et payés</a></li>
		<li{if $fee_page == 'expired'} class="current"{/if}><a href="{$admin_url}services/fees/details.php?id={$current_fee.id}&amp;type=expired">Inscription expirée</a></li>
		<li{if $fee_page == 'unpaid'} class="current"{/if}><a href="{$admin_url}services/fees/details.php?id={$current_fee.id}&amp;type=unpaid">En attente de règlement</a></li>
	</ul>
	{/if}

</nav>

Added src/templates/services/_service_form.tpl version [00234cd4d6].

































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>{$legend}</legend>
		<dl>
			{input name="label" type="text" required=1 label="Libellé" source=$service}
			{input name="description" type="textarea" label="Description" source=$service}

			<dt><label for="f_periodicite_jours">Période de validité</label></dt>
			<dd class="help">Attention, une modification de la période renseignée ici ne modifie pas la date d'expiration des activités déjà enregistrées.</dd>
			{input name="period" type="radio" value="0" label="Pas de période (cotisation ponctuelle)" default=$period}
			{input name="period" type="radio" value="1" label="En nombre de jours" default=$period}
			<dd class="period_1">
				<dl>
				{input name="duration" type="number" step="1" label="Durée de validité" size="5" source=$service}
				</dl>
			</dd>
			{input name="period" type="radio" value="2" label="Période définie (date à date)" default=$period}
			<dd class="period_2">
				<dl class="periode_dates">
					{input type="date" name="start_date" label="Date de début" source=$service}
					{input type="date" name="end_date" label="Date de fin" source=$service}
				</dl>
			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript">
{literal}
(function () {
	var hide = [];
	if (!$('#f_period_1').checked)
		hide.push('.period_1');

	if (!$('#f_period_2').checked)
		hide.push('.period_2');

	g.toggle(hide, false);

	function togglePeriod()
	{
		g.toggle(['.period_1', '.period_2'], false);

		if (this.checked && this.value == 1)
			g.toggle('.period_1', true);
		else if (this.checked && this.value == 2)
			g.toggle('.period_2', true);
	}

	$('#f_period_0').onchange = togglePeriod;
	$('#f_period_1').onchange = togglePeriod;
	$('#f_period_2').onchange = togglePeriod;
})();
{/literal}
</script>

Modified src/templates/services/delete.tpl from [979b1b65a7] to [e828c3fad7].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{include file="admin/_head.tpl" title="Supprimer une cotisation pour le membre n°%s"|args:$membre.id current="membres/cotisations"}

<ul class="actions">
    <li><a href="{$admin_url}membres/fiche.php?id={$membre.id}">Membre n°{$membre.id}</a></li>
    <li><a href="{$admin_url}membres/modifier.php?id={$membre.id}">Modifier</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ADMIN) && $user.id != $membre.id}
        <li><a href="{$admin_url}membres/supprimer.php?id={$membre.id}">Supprimer</a></li>
    {/if}
    <li class="current"><a href="{$admin_url}membres/cotisations.php?id={$membre.id}">Suivi des cotisations</a></li>
</ul>

{form_errors}

<form method="post" action="{$self_url}">
    <fieldset>
        <legend>Supprimer une cotisation membre</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer la cotisation membre
            du {$cotisation.date|format_sqlite_date_to_french}&nbsp;?
        </h3>
        {if $nb_operations > 0}
            <p class="alert">
                Attention il y a {$nb_operations} écritures comptables liées à cette cotisation.
                Celles-ci ne seront pas supprimées lors de la suppression de la cotisation membre.
            </p>
        {else}
            <p class="help">
                Aucune écriture comptable n'est liée à cette cotisation.
            </p>
        {/if}
    </fieldset>

    <p class="submit">
        {csrf_field key="del_cotisation_%s"|args:$cotisation.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>
</form>


{include file="admin/_foot.tpl"}
|

<
<
<
<
<
<
<
<
|
<

|
<
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
|


1
2








3

4
5

6

7













8





9
10
11
{include file="admin/_head.tpl" title="Supprimer une activité" current="membres/services"}









{include file="services/_nav.tpl" current="index"}


{include file="common/delete_form.tpl"

	legend="Supprimer cette activité ?"

	warning="Êtes-vous sûr de vouloir supprimer l'activité « %s » ?"|args:$service.label













	alert="Attention, cela supprimera également l'historique des membres inscrits à cette activité, ainsi que les rappels associés."





	info="Les écritures comptables liées à l'historique des membres inscrits à cette activité ne seront pas supprimées, et la comptabilité demeurera inchangée."}

{include file="admin/_foot.tpl"}

Modified src/templates/services/details.tpl from [79533ebb65] to [3f202e10e4].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


26
27
28
29


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53




54
55


56
57
58


59





60

61
62
63
64

65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="Membres ayant cotisé" current="membres/cotisations"}

<ul class="actions">
    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
        <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    {/if}
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
    {/if}
</ul>

<dl class="cotisation">
    <dt>Cotisation</dt>
    <dd>{$cotisation.intitule} — 
        {if $cotisation.duree}
            {$cotisation.duree} jours
        {elseif $cotisation.debut}
            du {$cotisation.debut|format_sqlite_date_to_french} au {$cotisation.fin|format_sqlite_date_to_french}
        {else}
            ponctuelle
        {/if}
        — {$cotisation.montant|escape|html_money} {$config.monnaie}
    </dd>
    <dd>


        {if !$cats}
            <a href="?id={$cotisation.id}&amp;cats=1">Afficher les membres des catégories pour lesquelles cette cotisation est obligatoire</a>
        {else}
            <a href="?id={$cotisation.id}">Afficher seulement les membres à jour (ou qui ont déjà payé, mais ne sont plus à jour)</a>


        {/if}
    </dd>    <dt>Nombre de membres ayant cotisé</dt>
    <dd>
        {$cotisation.nb_membres}
        <small class="help">(incluant les membres des catégories cachées)</small>
    </dd>
</dl>

{if !empty($liste)}
    <table class="list">
        <thead class="userOrder">
            <tr>
                <td class="{if $order == "id"} cur {if $desc}desc{else}asc{/if}{/if}"><a href="?id={$cotisation.id}&amp;o=id&amp;a&amp;cats={$cats}" class="icn up">&uarr;</a><a href="?id={$cotisation.id}&amp;o=id&amp;d&amp;cats={$cats}" class="icn dn">&darr;</a></td>
                <th class="{if $order == "identite"} cur {if $desc}desc{else}asc{/if}{/if}">Membre <a href="?id={$cotisation.id}&amp;o=identite&amp;a&amp;cats={$cats}" class="icn up">&uarr;</a><a href="?id={$cotisation.id}&amp;o=identite&amp;d&amp;cats={$cats}" class="icn dn">&darr;</a></th>
                <td class="{if $order == "a_jour"} cur {if $desc}desc{else}asc{/if}{/if}">Statut <a href="?id={$cotisation.id}&amp;o=a_jour&amp;a&amp;cats={$cats}" class="icn up">&uarr;</a><a href="?id={$cotisation.id}&amp;o=a_jour&amp;d&amp;cats={$cats}" class="icn dn">&darr;</a></td>
                <td class="{if $order == "date"} cur {if $desc}desc{else}asc{/if}{/if}">Date de cotisation <a href="?id={$cotisation.id}&amp;o=date&amp;a" class="icn up">&uarr;</a><a href="?id={$cotisation.id}&amp;o=date&amp;d&amp;cats={$cats}" class="icn dn">&darr;</a></td>
                <td></td>
            </tr>
        </thead>
        <tbody>
            {foreach from=$liste item="co"}
                <tr>
                    <td class="num">{$co.numero}</td>
                    <th><a href="{$admin_url}membres/fiche.php?id={$co.id_membre}" class="icn">{$co.nom}</a></th>




                    <td>{if $co.a_jour}<b class="confirm">À jour</b>{else}<b class="error">En retard</b>{/if}</td>
                    <td>{$co.date|format_sqlite_date_to_french}</td>


                    <td class="actions">
                        {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
                        <a class="icn" href="{$admin_url}membres/cotisations/ajout.php?id={$co.id_membre}&amp;cotisation={$cotisation.id}" title="Saisir une cotisation">➕</a>


                        {/if}





                        <a class="icn" href="{$admin_url}membres/cotisations.php?id={$co.id_membre}" title="Voir toutes les cotisations de ce membre">𝍢</a>

                        <a class="icn" href="{$admin_url}membres/cotisations/rappels.php?id={$co.id_membre}" title="Rappels envoyés à ce membre">⚠</a>
                    </td>
                </tr>
            {/foreach}

        </tbody>
    </table>

    {pagination url=$pagination_url page=$page bypage=$bypage total=$total}
{/if}


{include file="admin/_foot.tpl"}
|

|
<
<
<
<
<
<
<
<


|
<
<
<
<
<
<
<
<
<
<
|
>
>
|
<
<
<
>
>
|
<
<
<
<
|


<
<
<
<
<
<
<
<
<
<
|
|
|
|
<
|
>
>
>
>
|
|
>
>
|
<
<
>
>
|
>
>
>
>
>
|
>
|
|
|
|
>
|
|

|
<



1
2
3








4
5
6










7
8
9
10



11
12
13




14
15
16










17
18
19
20

21
22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
{include file="admin/_head.tpl" title="%s — Liste des membres inscrits"|args:$service.label current="membres/services"}

{include file="services/_nav.tpl" current="index" current_service=$service service_page=$type}









<dl class="cotisation">
	<dt>Nombre de membres trouvés</dt>










	<dd>
		{$list->count()}
		<em class="help">(N'apparaît ici que l'inscription la plus récente de chaque membre.)</em>
		{if $session->canAccess('membres', Membres::DROIT_ADMIN)}



		{linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"}
		{linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export tableur"}
		{/if}




	</dd>
</dl>











{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>

			<th><a href="../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>
				{if $row.status == 1 && $row.end_date}
					En cours
				{elseif $row.status == 1}
					<b class="confirm">À jour</b>
				{elseif $row.status == -1 && $row.end_date}
					Terminée
				{elseif $row.status == -1}
					<b class="error">En retard</b>


				{else}
					Pas d'expiration
				{/if}
			</td>
			<td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td>
			<td>{$row.expiry|date_short}</td>
			<td>{$row.fee}</td>
			<td>{$row.date|date_short}</td>
			<td class="actions">
				{linkbutton shape="user" label="Toutes les activités de ce membre" href="!services/user.php?id=%d"|args:$row.id_user}
				{linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user}
			</td>
		</tr>
	{/foreach}

	</tbody>
</table>

{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}



{include file="admin/_foot.tpl"}

Added src/templates/services/edit.tpl version [c0cc23129d].















>
>
>
>
>
>
>
1
2
3
4
5
6
7
{include file="admin/_head.tpl" title="Modifier une activité" current="membres/services"}

{include file="services/_nav.tpl" current="index"}

{include file="services/_service_form.tpl" legend="Modifier une activité"}

{include file="admin/_foot.tpl"}

Added src/templates/services/fees/_fee_form.tpl version [9191e678a6].

























































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php
assert(isset($legend));
assert(isset($csrf_key));
assert(isset($submit_label));
$targets = Entities\Accounting\Account::TYPE_REVENUE;
?>

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>{$legend}</legend>
		<dl>
			{input name="label" type="text" required=1 label="Libellé" source=$fee}
			{input name="description" type="textarea" label="Description" source=$fee}

			<dt><label for="f_amount_type">Montant de la cotisation</label></dt>
			{input name="amount_type" type="radio" value="0" label="Gratuite ou prix libre" default=$amount_type}
			{input name="amount_type" type="radio" value="1" label="Montant fixe ou prix libre conseillé" default=$amount_type}
			<dd class="amount_type_1">
				<dl>
					{input name="amount" type="money" label="Montant" source=$fee fake_required=1}
				</dl>
			</dd>
			{input name="amount_type" type="radio" value="2" label="Montant variable" default=$amount_type}
			<dd class="amount_type_2">
				<dl>
					{input name="formula" type="textarea" label="Formule de calcul" source=$fee fake_required=1}
					<dd class="help">
						<a href="https://fossil.kd2.org/garradin/wiki?name=Formule_calcul_activit%C3%A9">Aide sur les formules de calcul</a>
					</dd>
				</dl>
			</dd>
			<dt><strong>Comptabilité</strong></dt>
			{input name="accounting" type="checkbox" value="1" label="Enregistrer en comptabilité" default=$accounting_enabled}
			<dd class="help">Laissez cette case décochée si vous n'utilisez pas Garradin pour la comptabilité. Il ne sera pas possible de suivre le montant des règlements effectués pour ce tarif.</dd>
		</dl>
	</fieldset>

	<fieldset class="accounting">
		<legend>Enregistrer en comptabilité</legend>
		<p class="help">Chaque règlement d'un membre lié à ce tarif sera enregistré dans la comptabilité, permettant de suivre le montant des règlements effectués.</p>
		{if !count($years)}
			<p class="error block">Il n'y a aucun exercice ouvert dans la comptabilité, il n'est donc pas possible d'enregistrer les activités dans la comptabilité. Merci de commencer par <a href="{$admin_url}acc/years/new.php">créer un exercice</a>.</p>
		{else}
		<dl>
			<dt><label for="f_id_year">Exercice</label> <b>(obligatoire)</b></dt>
			<dd>
				<select id="f_id_year" name="id_year">
					{foreach from=$years item="year"}
					<option value="{$year.id}">{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</option>
					{/foreach}
				</select>
			</dd>
			{input type="list" target="acc/charts/accounts/selector.php?targets=%s&year=%d"|args:$targets,$fee.id_year name="account" label="Compte à utiliser" default=$account required=1}
		</dl>
		{/if}
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript">
{literal}
(function () {
	var hide = [];
	if (!$('#f_amount_type_1').checked)
		hide.push('.amount_type_1');

	if (!$('#f_amount_type_2').checked)
		hide.push('.amount_type_2');

	g.toggle(hide, false);
	g.toggle('.accounting', $('#f_accounting_1').checked);

	function togglePeriod()
	{
		g.toggle(['.amount_type_1', '.amount_type_2'], false);

		if (this.checked && this.value == 1)
			g.toggle('.amount_type_1', true);
		else if (this.checked && this.value == 2)
			g.toggle('.amount_type_2', true);
	}

	$('#f_amount_type_0').onchange = togglePeriod;
	$('#f_amount_type_1').onchange = togglePeriod;
	$('#f_amount_type_2').onchange = togglePeriod;
	$('#f_accounting_1').onchange = () => { g.toggle('.accounting', $('#f_accounting_1').checked); };

	function toggleYearForSelector()
	{
		var btn = document.querySelector('#f_account_container button');
		btn.value = btn.value.replace(/year=\d+/, 'year=' + y.value);
	}

	var y = $('#f_id_year')

	y.onchange = toggleYearForSelector;
	toggleYearForSelector();
})();
{/literal}
</script>

Added src/templates/services/fees/delete.tpl version [ab39799ac0].























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
{include file="admin/_head.tpl" title="Supprimer un tarif" current="membres/services"}

{include file="services/_nav.tpl" current="index"}

{include file="common/delete_form.tpl"
	legend="Supprimer ce tarif ?"
	warning="Êtes-vous sûr de vouloir supprimer le tarif « %s » ?"|args:$fee.label
	alert="Attention, cela supprimera également l'historique des membres ayant réglé ce tarif."
	info="Les écritures comptables liées à l'historique des membres ayant réglé ce tarif ne seront pas supprimées, et la comptabilité demeurera inchangée."}

{include file="admin/_foot.tpl"}

Added src/templates/services/fees/details.tpl version [0b08e607ec].













































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{include file="admin/_head.tpl" title="Tarif : %s — Liste des membres inscrits"|args:$fee.label current="membres/services"}

{include file="services/_nav.tpl" current="index" current_service=$service service_page="index" current_fee=$fee fee_page=$type}

<dl class="cotisation">
	<dt>Nombre de membres trouvés</dt>
	<dd>
		{$list->count()}
		<em class="help">(N'apparaît ici que l'inscription la plus récente de chaque membre.)</em>
		{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
			{linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"}
			{linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>

{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>
			<th><a href="../../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td>
			<td class="money">{$row.paid_amount|raw|money_currency}</td>
			<td>{$row.date|date_short}</td>
			<td class="actions">
				{linkbutton shape="user" label="Toutes les activités de ce membre" href="!services/user.php?id=%d"|args:$row.id_user}
				{linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user}
			</td>
		</tr>
	{/foreach}

	</tbody>
</table>

{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}

Added src/templates/services/fees/edit.tpl version [5f56ed8c69].















>
>
>
>
>
>
>
1
2
3
4
5
6
7
{include file="admin/_head.tpl" title="%s — Tarifs"|args:$service.label current="membres/services"}

{include file="services/_nav.tpl" current="index" current_service=$service service_page="index"}

{include file="services/fees/_fee_form.tpl" legend="Modifier un tarif" submit_label="Enregistrer"}

{include file="admin/_foot.tpl"}

Added src/templates/services/fees/index.tpl version [da9ebe99b8].









































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{include file="admin/_head.tpl" title="%s — Tarifs"|args:$service.label current="membres/services"}

{include file="services/_nav.tpl" current="index" current_service=$service service_page="index"}

{if count($list)}
	<table class="list">
		<thead>
			<th>Tarif</th>
			<td>Montant</td>
			<td>Membres à jour et ayant payé</td>
			<td>Membres expirés</td>
			<td>Membres en attente de règlement</td>
			<td></td>
		</thead>
		<tbody>
			{foreach from=$list item="row"}
				<tr>
					<th><a href="details.php?id={$row.id}">{$row.label}</a></th>
					<td>
						{if $row.formula}
							Formule
						{elseif $row.amount}
							{$row.amount|money_currency|raw}
						{else}
							-
						{/if}
					</td>
					<td class="num"><a href="details.php?id={$row.id}">{$row.nb_users_ok}</a></td>
					<td class="num"><a href="details.php?id={$row.id}&amp;type=expired">{$row.nb_users_expired}</a></td>
					<td class="num"><a href="details.php?id={$row.id}&amp;type=unpaid">{$row.nb_users_unpaid}</td>
					<td class="actions">
						{linkbutton shape="users" label="Liste des inscrits" href="!services/fees/details.php?id=%d"|args:$row.id}
						{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
							{linkbutton shape="edit" label="Modifier" href="!services/fees/edit.php?id=%d"|args:$row.id}
							{linkbutton shape="delete" label="Supprimer" href="!services/fees/delete.php?id=%d"|args:$row.id}
						{/if}
					</td>
				</tr>
			{/foreach}
		</tbody>
	</table>
{else}
	<p class="block alert">
		Il n'y a aucun tarif enregistré. Créez un premier tarif pour l'activité «&nbsp;{$service.label}&nbsp;» pour pouvoir y inscrire des membres.
	</p>
{/if}

{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
	{include file="services/fees/_fee_form.tpl" legend="Ajouter un tarif" submit_label="Ajouter" csrf_key="fee_add" fee=null amount_type=0 account=null}
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/services/index.tpl from [754a0411a5] to [c030049f17].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
{include file="admin/_head.tpl" title="Cotisations" current="membres/cotisations" js=1}

<ul class="actions">
    <li class="current"><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    {if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
        <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    {/if}
    {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
        <li><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
    {/if}
</ul>

{if count($liste)}
    <table class="list">
        <thead>
            <th>Cotisation</th>
            <td>Période</td>
            <td>Montant</td>
            <td>Membres inscrits</td>
            <td>Membres à jour</td>
            <td></td>
        </thead>
        <tbody>
            {foreach from=$liste item="co"}
                <tr>
                    <th><a href="{$admin_url}membres/cotisations/voir.php?id={$co.id}">{$co.intitule}</a></th>
                    <td>
                        {if $co.duree}
                            {$co.duree} jours
                        {elseif $co.debut}
                            du {$co.debut|format_sqlite_date_to_french} au {$co.fin|format_sqlite_date_to_french}
                        {else}
                            ponctuelle
                        {/if}
                    </td>
                    <td class="num">{$co.montant|escape|html_money} {$config.monnaie}</td>
                    <td class="num">{$co.nb_membres}</td>
                    <td class="num">{$co.nb_a_jour}</td>
                    <td class="actions">
                        <a class="icn" href="{$admin_url}membres/cotisations/voir.php?id={$co.id}" title="Liste des membres cotisants">👪</a>

                        {if $session->canAccess('membres', Membres::DROIT_ADMIN)}
                            <a class="icn" href="{$admin_url}membres/cotisations/gestion/modifier.php?id={$co.id}" title="Modifier">✎</a>

                            <a class="icn" href="{$admin_url}membres/cotisations/gestion/supprimer.php?id={$co.id}" title="Supprimer">✘</a>
                        {/if}
                    </td>
                </tr>
            {/foreach}
        </tbody>
    </table>
{else}
    <p class="alert">Il n'y a aucun type de cotisation enregistré.</p>
{/if}

{if $session->canAccess('membres', Membres::DROIT_ADMIN)}

{form_errors}

<p class="help">
    Idée : les cotisations peuvent également être utilisées pour suivre les activités auxquelles
    sont inscrits les membres de l'association.
</p>

<form method="post" action="{$self_url}" id="f_add">

    <fieldset>
        <legend>Ajouter une cotisation</legend>
        <dl>
            <dt><label for="f_intitule">Intitulé</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="intitule" id="f_intitule" value="{form_field name=intitule}" required="required" /></dd>
            <dt><label for="f_description">Description</label></dt>
            <dd><textarea name="description" id="f_description" cols="50" rows="3">{form_field name=description}</textarea></dd>
            <dt><label for="f_montant">Montant minimal</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd class="help">Laisser à <b>0</b> pour une cotisation à prix libre</dd>
            <dd><input type="number" name="montant" step="0.01" min="0.00" id="f_montant" value="{form_field name=montant default=0.00}" required="required" /></dd>

            <dt><label for="f_periodicite_jours">Période de validité</label></dt>
            <dd><input type="radio" name="periodicite" id="f_periodicite_ponctuel" value="ponctuel" {form_field checked="ponctuel" name=periodicite default="ponctuel"} /> <label for="f_periodicite_ponctuel">Pas de période (cotisation ponctuelle)</label></dd>

            <dd><input type="radio" name="periodicite" id="f_periodicite_jours" value="jours" {form_field checked="jours" name=periodicite} /> <label for="f_periodicite_jours">En nombre de jours</label>
                <dl class="periode_jours">
                    <dt><label for="f_duree">Durée de validité</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                    <dd><input type="number" step="1" size="5" min="1" name="duree" id="f_duree" value="{form_field name="duree"}" /></dd>
                </dl>
            </dd>
            <dd><input type="radio" name="periodicite" id="f_periodicite_dates" value="date" {form_field checked="date" name=periodicite} /> <label for="f_periodicite_dates">Période définie</label>
                <dl class="periode_dates">
                    <dt><label for="f_date_debut">Date de début</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                    <dd><input type="date" name="debut" value="{form_field name=debut}" id="f_date_debut" /></dd>
                    <dt><label for="f_date_fin">Date de fin</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                    <dd><input type="date" name="fin" value="{form_field name=fin}" id="f_date_fin" /></dd>
                </dl>
            </dd>
            <dt>
                <input type="checkbox" name="categorie" id="f_categorie" value="1" {form_field name="categorie" checked=1} /> <label for="f_categorie">Enregistrer les cotisations des membres dans la comptabilité</label>
            </dt>
            <dd class="help cat_compta">
                Si coché, à chaque enregistrement de cotisation d'un membre une opération 
                du montant de la cotisation sera enregistrée dans la comptabilité selon
                la catégorie choisie.
            </dd>
            <dt class="cat_compta"><label for="f_id_categorie_compta">Catégorie comptable</label></dt>
            <dd class="cat_compta">
                <select name="id_categorie_compta" id="f_id_categorie_compta">
                {foreach from=$categories item="cat"}
                    <option value="{$cat.id}" {form_field name="id_categorie_compta" selected=$cat.id}>{$cat.intitule}
                    {if !empty($cat.description)}
                        — <em>{$cat.description}</em>
                    {/if}
                    </option>
                {/foreach}
                </select>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="new_cotisation"}
        <input type="submit" name="save" value="Ajouter &rarr;" />
    </p>

</form>

<script type="text/javascript">
{literal}
(function () {
    var hide = [];

    if (!$('#f_categorie').checked)
        hide.push('.cat_compta');

    if (!$('#f_periodicite_jours').checked)
        hide.push('.periode_jours');

    if (!$('#f_periodicite_dates').checked)
        hide.push('.periode_dates');

    g.toggle(hide, false);

    $('#f_categorie').onchange = function() {
        g.toggle('.cat_compta', this.checked);
        return true;
    };

    function togglePeriode()
    {
        g.toggle(['.periode_jours', '.periode_dates'], false);

        if (this.checked && this.value == 'jours')
            g.toggle('.periode_jours', true);
        else if (this.checked && this.value == 'date')
            g.toggle('.periode_dates', true);
    }

    $('#f_periodicite_ponctuel').onchange = togglePeriode;
    $('#f_periodicite_dates').onchange = togglePeriode;
    $('#f_periodicite_jours').onchange = togglePeriode;
})();
{/literal}
</script>

{/if}

{include file="admin/_foot.tpl"}
|

|
|
|
|
|
<
<
<
<

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
|
<
>
|
|
|
|
|
|
|

|



|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<



1
2
3
4
5
6
7




8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46
47
48
49
50
51
52









































































































53
54
55
{include file="admin/_head.tpl" title="Activités et cotisations" current="membres/services"}

{include file="services/_nav.tpl" current="index" service=null fee=null}

{if isset($_GET['CREATE'])}
	<p class="block error">Vous devez déjà créer une activité pour pouvoir utiliser cette fonction.</p>
{/if}





{if count($list)}
	<table class="list">
		<thead>
			<th>Activité</th>
			<td>Période</td>
			<td>Membres à jour</td>
			<td>Membres expis</td>
			<td>Membres en attente de règlement</td>
			<td></td>
		</thead>
		<tbody>
			{foreach from=$list item="row"}
				<tr>
					<th><a href="fees/?id={$row.id}">{$row.label}</a></th>
					<td>
						{if $row.duration}
							{$row.duration} jours
						{elseif $row.start_date}
							du {$row.start_date|date_short} au {$row.end_date|date_short}
						{else}
							ponctuelle
						{/if}
					</td>
					<td class="num"><a href="details.php?id={$row.id}">{$row.nb_users_ok}</a></td>
					<td class="num"><a href="details.php?id={$row.id}&amp;type=expired">{$row.nb_users_expired}</a></td>
					<td class="num"><a href="details.php?id={$row.id}&amp;type=unpaid">{$row.nb_users_unpaid}</a></td>
					<td class="actions">
						{linkbutton shape="menu" label="Tarifs" href="!services/fees/?id=%d"|args:$row.id}
						{linkbutton shape="users" label="Liste des inscrits" href="!services/details.php?id=%d"|args:$row.id}
						{if $session->canAccess('membres', Membres::DROIT_ADMIN)}

							{linkbutton shape="edit" label="Modifier" href="!services/edit.php?id=%d"|args:$row.id}
							{linkbutton shape="delete" label="Supprimer" href="!services/delete.php?id=%d"|args:$row.id}
						{/if}
					</td>
				</tr>
			{/foreach}
		</tbody>
	</table>
{else}
	<p class="block alert">Il n'y a aucune activité enregistrée.</p>
{/if}

{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
	{include file="services/_service_form.tpl" legend="Ajouter une activité" service=null period=0}









































































































{/if}

{include file="admin/_foot.tpl"}

Added src/templates/services/payment.tpl version [e2a1794c13].

























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{include file="admin/_head.tpl" title="Enregistrer un règlement" current="membres/services"}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Enregistrer un règlement</legend>

		<dl>
			<dt>Membre sélectionné</dt>
			<dd><h3>{$user_name}</h3></dd>
			<dt><strong>Inscription</strong></dt>
			{input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"}
			{input type="money" name="amount" label="Montant réglé par le membre" required=1}
			{input type="list" target="acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=1}
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Added src/templates/services/reminders/_form.tpl version [eed395de27].



























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>{$legend}</legend>
		<dl>
			{input type="select" name="id_service" options=$services_list label="Activité associée au rappel" required=1 source=$reminder}
			{input type="text" name="subject" required=1 source=$reminder default=$default_subject label="Sujet du message envoyé"}

			<dt><label for="f_delay_type_0">Délai d'envoi</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
			{input type="radio" name="delay_type" value=0 default=$delay_type label="Le jour de l'expiration de l'activité"}
			<dd>
				{input type="radio" name="delay_type" value=1 default=$delay_type}
				{input type="number" name="delay_before" min=1 max=999 default=$delay_before size=4}
				<label for="f_delay_type_1">jours <strong>avant</strong> expiration</label>
			</dd>
			<dd>
				{input type="radio" name="delay_type" value=2 default=$delay_type}
				{input type="number" name="delay_after" min=1 max=999 size=4 default=$delay_after}
				<label for="f_delay_type_2">jours <strong>après</strong> expiration</label>
			</dd>
			{input type="textarea" name="body" required=1 source=$reminder default=$default_body label="Texte du message envoyé" help="Pour inclure dans le contenu du mail le nom du membre, utilisez #IDENTITE, pour inclure le délai de l'envoi utilisez #NB_JOURS." cols="90" rows="15"}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript">
{literal}
(function () {
	$('#f_delay_before').onfocus = function () {
		$('#f_delay_type_1').checked = true;
	};
	$('#f_delay_after').onfocus = function () {
		$('#f_delay_type_2').checked = true;
	};
})();
{/literal}
</script>

Modified src/templates/services/reminders/delete.tpl from [e3f2d395a4] to [93a3e36f8e].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{include file="admin/_head.tpl" title="Supprimer un rappel automatique" current="membres/cotisations"}

<ul class="actions">
    <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    <li class="current"><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
</ul>

{form_errors}

<form method="post" action="{$self_url}">

    <fieldset>
        <legend>Supprimer ce rappel automatique ?</legend>
        <h3 class="warning">
            Êtes-vous sûr de vouloir supprimer le rappel «&nbsp;{$rappel.sujet}&nbsp;» ?
        </h3>
        <dl>
            <dt><label for="f_delete_history">Effacer aussi l'historique des e-mails envoyés par le biais de ce rappel&nbsp;?</label></dt>
            <dd>
                <label>
                    <input type="radio" name="delete_history" value="0" checked="checked" />
                    Non, conserver l'historique
                </label> (toutefois il ne sera plus associé à ce rappel)
            </dd>
            <dd>
                <label>
                    <input type="radio" name="delete_history" value="1" />
                    Oui, effacer l'historique des e-mails envoyés
                </label>
            </dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="delete_rappel_"|cat:$rappel.id}
        <input type="submit" name="delete" value="Supprimer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|

<
<
<
<
<
|
<

<
|
<
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<

<
<

1
2





3

4

5

6

7

















8




9


10
{include file="admin/_head.tpl" title="Supprimer un rappel automatique" current="membres/services"}






{include file="services/_nav.tpl" current="reminders"}



{include file="common/delete_form.tpl"

	legend="Supprimer ce rappel automatique ?"

	warning="Êtes-vous sûr de vouloir supprimer le rappel « %s » ?"|args:$reminder.subject

















	alert="Attention, cela supprimera également l'historique des emails envoyés par ce rappel."}







{include file="admin/_foot.tpl"}

Added src/templates/services/reminders/details.tpl version [c63967de23].

































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{include file="admin/_head.tpl" title="Liste des rappels envoyés" current="membres/services"}

{include file="services/_nav.tpl" current="reminders"}

<dl class="cotisation">
	<dt>Rappel&nbsp;: <em>{$reminder.subject}</em></dt>
	<dd>Activité&nbsp;: {$service.label}</dd>
	<dd>Délai d'envoi&nbsp;: {if $reminder.delay > 0}{$reminder.delay} jours après l'expiration{elseif $reminder.delay < 0}{$reminder.delay|abs} jours avant l'expiration{else}le jour de l'expiration{/if}</dd>
	<dt>Nombre de rappels envoyés</dt>
	<dd>
		{$list->count()}
	</dd>
</dl>

{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>
			<th><a href="../../membres/fiche.php?id={$row.id_user}">{$row.identity}</a></th>
			<td>{$row.email}</td>
			<td>{$row.date|date_short}</td>
			<td></td>
		</tr>
	{/foreach}

	</tbody>
</table>

{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}

Modified src/templates/services/reminders/edit.tpl from [b2128e65d7] to [bc86712955].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{include file="admin/_head.tpl" title="Modifier un rappel automatique" current="membres/cotisations" js=1}

<ul class="actions">
    <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    <li class="current"><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
</ul>

{form_errors}

<form method="post" action="{$self_url}" id="f_add">

    <fieldset>
        <legend>Modifier un rappel automatique</legend>
        <dl>
            <dt><label for="f_id_cotisation">Cotisation associée</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="id_cotisation" id="f_id_cotisation" required="required">
                    <option value="">--</option>
                    {foreach from=$cotisations item="co"}
                    <option value="{$co.id}" {form_field name="id_cotisation" selected=$co.id data=$rappel}>
                        {$co.intitule}
                        — {$co.montant|escape|html_money} {$config.monnaie}
                        — {if $co.duree}pour {$co.duree} jours
                        {elseif $co.debut}
                            du {$co.debut|format_sqlite_date_to_french} au {$co.fin|format_sqlite_date_to_french}
                        {else}
                            ponctuelle
                        {/if}
                    </option>
                    {/foreach}
                </select>
            </dd>
            <dt><label for="f_sujet">Sujet du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet data=$rappel}" required="required" size="50" /></dd>
            <dt><label for="f_delai">Délai d'envoi</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><label><input type="radio" name="delai_choix" value="0" {form_field name="delai_choix" checked=0 default=0 data=$rappel} /> Le jour de l'expiration de la cotisation</label></dd>
            <dd>
                <input type="radio" name="delai_choix" id="f_delai_pre" value="-1" {form_field name="delai_choix" checked=-1 data=$rappel} />
                <input type="number" name="delai_pre" id="f_delai_pre_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_pre data=$rappel default=30}" />
                <label for="f_delai_pre">jours avant expiration</label>
            </dd>
            <dd>
                <input type="radio" name="delai_choix" id="f_delai_post" value="1" {form_field name="delai_choix" checked=1 data=$rappel} /> 
                <input type="number" name="delai_post" id="f_delai_post_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_post default=30 data=$rappel}" />
                <label for="f_delai_post">jours après expiration</label>
            </dd>
            <dt><label for="f_texte">Texte du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><textarea name="texte" id="f_texte" cols="70" rows="15" required="required">{form_field name=texte data=$rappel}</textarea></dd>
            <dd class="help">Astuce : pour inclure dans le contenu du mail le nom du membre, utilisez #IDENTITE, pour inclure le délai de l'envoi utilisez #NB_JOURS.</dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_rappel_%s"|args:$rappel.id}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

{include file="admin/_foot.tpl"}
|

<
<
<
<
<
|
<

<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

<
<
<
<
<
<
<

1
2





3

4



5






































6







7
{include file="admin/_head.tpl" title="Modifier un rappel automatique" current="membres/services"}






{include file="services/_nav.tpl" current="reminders"}





{include file="services/reminders/_form.tpl" legend="Modifier un rappel automatique" default_subject=null default_body=null}














































{include file="admin/_foot.tpl"}

Modified src/templates/services/reminders/index.tpl from [2f62692021] to [68e5f541ec].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

47
48
49



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
{include file="admin/_head.tpl" title="Gestion des rappels automatiques" current="membres/cotisations" js=1}

<ul class="actions">
    <li><a href="{$admin_url}membres/cotisations/">Cotisations</a></li>
    <li><a href="{$admin_url}membres/cotisations/ajout.php">Saisie d'une cotisation</a></li>
    <li class="current"><a href="{$admin_url}membres/cotisations/gestion/rappels.php">Gestion des rappels automatiques</a></li>
</ul>

<p class="help">
    Les rappels automatiques sont envoyés aux membres disposant d'une adresse e-mail
    selon le délai défini. Il est possible de définir plusieurs rappels pour une même cotisation.
</p>

{if empty($liste)}
    <p class="alert">Aucun rappel automatique n'est enregistré.</p>
{else}
    <table class="list">
        <thead>
            <td>Cotisation</td>
            <td>Délai de rappel</td>
            <th>Sujet</th>
            <td></td>
        </thead>
        <tbody>
            {foreach from=$liste item="rappel"}
                <tr>
                    <td>
                        {$rappel.intitule}
                        — {$rappel.montant|escape|html_money} {$config.monnaie}
                        — {if $rappel.duree}pour {$rappel.duree} jours
                        {elseif $rappel.debut}
                            du {$rappel.debut|format_sqlite_date_to_french} au {$rappel.fin|format_sqlite_date_to_french}
                        {else}
                            ponctuelle
                        {/if}
                    </td>
                    <td>
                        {if $rappel.delai == 0}le jour de l'expiration
                        {else}
                            {$rappel.delai|abs}
                            {if abs($rappel.delai) > 1}jours{else}jour{/if}
                            {if $rappel.delai > 0}après{else}avant{/if}
                            expiration
                        {/if}
                    </td>
                    <th>{* FIXME liste des personnes ayant reçu ce rappel<a href="{$admin_url}membres/cotisations/rappel.php?id={$rappel.id}">{$rappel.sujet}</a>*}{$rappel.sujet}</th>

                    <td class="actions">
                        <a class="icn" href="{$admin_url}membres/cotisations/gestion/rappel_modifier.php?id={$rappel.id}" title="Modifier">✎</a>
                        <a class="icn" href="{$admin_url}membres/cotisations/gestion/rappel_supprimer.php?id={$rappel.id}" title="Supprimer">✘</a>



                    </td>
                </tr>
            {/foreach}
        </tbody>
    </table>
{/if}

{form_errors}

<form method="post" action="{$self_url}" id="f_add">

    <fieldset>
        <legend>Ajouter un rappel automatique</legend>
        <dl>
            <dt><label for="f_id_cotisation">Cotisation associée</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd>
                <select name="id_cotisation" id="f_id_cotisation" required="required">
                    <option value="">--</option>
                    {foreach from=$cotisations item="co"}
                    <option value="{$co.id}" {form_field name="id_cotisation" selected=$co.id}>
                        {$co.intitule}
                        — {$co.montant|escape|html_money} {$config.monnaie}
                        — {if $co.duree}pour {$co.duree} jours
                        {elseif $co.debut}
                            du {$co.debut|format_sqlite_date_to_french} au {$co.fin|format_sqlite_date_to_french}
                        {else}
                            ponctuelle
                        {/if}
                    </option>
                    {/foreach}
                </select>
            </dd>
            <dt><label for="f_sujet">Sujet du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><input type="text" name="sujet" id="f_sujet" value="{form_field name=sujet default=$default_subject}" required="required" size="50" /></dd>
            <dt><label for="f_delai">Délai d'envoi</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><label><input type="radio" name="delai_choix" value="0" {form_field name="delai_choix" checked=0 default=0} /> Le jour de l'expiration de la cotisation</label></dd>
            <dd>
                <input type="radio" name="delai_choix" id="f_delai_pre" value="-1" {form_field name="delai_choix" checked=-1} />
                <input type="number" name="delai_pre" id="f_delai_pre_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_pre default=30}" />
                <label for="f_delai_pre">jours avant expiration</label>
            </dd>
            <dd>
                <input type="radio" name="delai_choix" id="f_delai_post" value="1" {form_field name="delai_choix" checked=1} /> 
                <input type="number" name="delai_post" id="f_delai_post_nb" step="1" min="1" max="900" size="4" id="f_delai" value="{form_field name=delai_post default=30}" />
                <label for="f_delai_post">jours après expiration</label>
            </dd>
            <dt><label for="f_texte">Texte du mail</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
            <dd><textarea name="texte" id="f_texte" cols="70" rows="15" required="required">{form_field name=texte default=$default_text}</textarea></dd>
            <dd class="help">Astuce : pour inclure dans le contenu du mail le nom du membre, utilisez #IDENTITE, pour inclure le délai de l'envoi utilisez #NB_JOURS.</dd>
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="new_rappel"}
        <input type="submit" name="save" value="Ajouter &rarr;" />
    </p>

</form>

<script type="text/javascript">
{literal}
(function () {
    $('#f_delai_pre_nb').onclick = function () {
        $('#f_delai_pre').checked = true;
    };
    $('#f_delai_post_nb').onclick = function () {
        $('#f_delai_post').checked = true;
    };
})();
{/literal}
</script>

{include file="admin/_foot.tpl"}
|

|
<
<
<
<


|
<


|
|

|
|
|
|
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<
|
|
|
|
|
|
|
|
|
|
<
>
|
<
<
>
>
>
|
|
|
|
|


<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


1
2
3




4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23







24
25
26
27
28
29
30
31
32
33

34
35


36
37
38
39
40
41
42
43
44
45





46






































47



















48
49
{include file="admin/_head.tpl" title="Gestion des rappels automatiques" current="membres/services"}

{include file="services/_nav.tpl" current="reminders"}





<p class="help">
	Les rappels automatiques sont envoyés aux membres disposant d'une adresse e-mail selon le délai défini. Il est possible de définir plusieurs rappels pour une même activité.

</p>

{if empty($list)}
	<p class="alert block">Aucun rappel automatique n'est configuré.</p>
{else}
	<table class="list">
		<thead>
			<td>Activité</td>
			<td>Délai de rappel</td>
			<th>Sujet</th>
			<td></td>
		</thead>
		<tbody>
			{foreach from=$list item="reminder"}
				<tr>
					<td>
						{$reminder.service_label}







					</td>
					<td>
						{if $reminder.delay == 0}le jour de l'expiration
						{else}
							{$reminder.delay|abs}
							{if abs($reminder.delay) > 1}jours{else}jour{/if}
							{if $reminder.delay > 0}après{else}avant{/if}
							expiration
						{/if}
					</td>

					<th><a href="details.php?id={$reminder.id}">{$reminder.subject}</a></th>
					<td class="actions">


						{linkbutton shape="mail" label="Liste des rappels envoyés" href="!services/reminders/details.php?id=%d"|args:$reminder.id}
						{linkbutton shape="edit" label="Modifier" href="!services/reminders/edit.php?id=%d"|args:$reminder.id}
						{linkbutton shape="delete" label="Supprimer" href="!services/reminders/delete.php?id=%d"|args:$reminder.id}
					</td>
				</tr>
			{/foreach}
		</tbody>
	</table>
{/if}






{include file="services/reminders/_form.tpl" legend="Ajouter un rappel automatique"






































	reminder=null delay_type=0 delay_before=15 delay_after=5}




















{include file="admin/_foot.tpl"}

Added src/templates/services/save.tpl version [69201d6af6].































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
{include file="admin/_head.tpl" title="Enregistrer un règlement" current="membres/services"}

{include file="services/_nav.tpl" current="save" fee=null service=null}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Inscrire un membre à une activité</legend>

{if !$user_id}
		<dl>
			{input type="list" name="user" required=1 label="Sélectionner un membre" default=$selected_user target="membres/selector.php"}
		</dl>
{else}
		<dl>
			<dt>Membre sélectionné</dt>
			<dd><h3>{$user_name}</h3><input type="hidden" name="id_user" value="{$user_id}" /></dd>
			<dt><label for="f_service_ID">Activité</label> <b>(obligatoire)</b></dt>

		{foreach from=$grouped_services item="service"}
			<dd class="radio-btn">
				{input type="radio" name="id_service" value=$service.id data-expiry=$service.expiry_date|date_short label=null}
				<label for="f_id_service_{$service.id}">
					<div>
						<h3>{$service.label}</h3>
						<p>
							{if $service.duration}
								{$service.duration} jours
							{elseif $service.start_date}
								du {$service.start_date|date_short} au {$service.end_date|date_short}
							{else}
								ponctuelle
							{/if}
						</p>
						{if $service.description}
						<p class="help">
							{$service.description|escape|nl2br}
						</p>
						{/if}
					</div>
				</label>
			</dd>
		{/foreach}
		</dl>

		{foreach from=$grouped_services item="service"}
		<?php if (!count($service->fees)) { continue; } ?>
		<dl data-service="s{$service.id}">
			<dt><label for="f_fee">Tarif</label> <b>(obligatoire)</b></dt>
			{foreach from=$service.fees key="service_id" item="fee"}
			<dd class="radio-btn">
				{input type="radio" name="id_fee" value=$fee.id data-user-amount=$fee.user_amount data-account=$fee.id_account label=null}
				<label for="f_id_fee_{$fee.id}">
					<div>
						<h3>{$fee.label}</h3>
						<p>
							{if !$fee.user_amount}
								prix libre ou gratuit
							{elseif $fee.user_amount && $fee.formula}
								<strong>{$fee.user_amount|raw|money_currency}</strong> (montant calculé)
							{elseif $fee.user_amount}
								<strong>{$fee.user_amount|raw|money_currency}</strong>
							{/if}
						</p>
						{if $fee.description}
						<p class="help">
							{$fee.description|escape|nl2br}
						</p>
						{/if}
					</div>
				</label>
			</dd>
			{/foreach}
		</dl>
		{/foreach}
	</fieldset>

	<fieldset>
		<legend>Détails</legend>
		<dl>
			{input type="date" name="date" required=1 default=$today label="Date d'inscription"}
			{input type="date" name="expiry_date" default=$today label="Date d'expiration de l'inscription"}
			{input type="checkbox" name="paid" value="1" default="1" label="Marquer cette inscription comme payée"}
			<dd class="help">Décocher cette case pour pouvoir suivre les règlements de personnes qui payent en plusieurs fois. Il sera possible de cocher cette case lorsque le solde aura été réglé.</dd>
		</dl>
	</fieldset>

	<fieldset class="accounting">
		<legend>Enregistrement en comptabilité</legend>

		<dl>
			{input type="money" name="amount" label="Montant réglé par le membre" fake_required=1 help="En cas de règlement en plusieurs fois il sera possible d'ajouter des règlements via la page de suivi des activités de ce membre."}
			{input type="list" target="acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=1}
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."}
		</dl>
{/if}
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{if $user_id}
			{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
		{else}
			{button type="submit" name="next" label="Continuer" shape="right" class="main"}
		{/if}
	</p>

</form>

{literal}
<script type="text/javascript">
function selectService(elm) {
	$('[data-service]').forEach((e) => {
		e.style.display = ('s' + elm.value == e.getAttribute('data-service')) ? 'block' : 'none';
	});

	$('#f_expiry_date').value = elm.dataset.expiry;

	var first = document.querySelector('[data-service="s' + elm.value + '"] input[name=id_fee]');

	if (first) {
		first.checked = true;
		selectFee(first);
	}
}

function selectFee(elm) {
	var amount = parseInt(elm.getAttribute('data-user-amount'), 10);

	// Toggle accounting part of the form
	var accounting = elm.getAttribute('data-account') ? true : false;
	g.toggle('.accounting', accounting);
	$('#f_amount').required = accounting;

	// Fill the amount paid by the user
	if (amount) {
		$('#f_amount').value = g.formatMoney(amount);
	}
}

$('input[name=id_service]').forEach((e) => {
	e.onchange = () => { selectService(e); };
});

$('input[name=id_fee]').forEach((e) => {
	e.onchange = () => { selectFee(e); };
});

var selected = document.querySelector('input[name="id_service"]:checked, input[name="id_service"]');
selected.checked = true;

g.toggle('.accounting', false);
selectService(selected);
</script>
{/literal}

{include file="admin/_foot.tpl"}

Added src/templates/services/user.tpl version [dbd53507fd].

















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{include file="admin/_head.tpl" title="%s — Inscriptions aux activités et cotisations"|args:$user.identite current="membres/services"}

<p>
	{linkbutton href="!membres/fiche.php?id=%d"|args:$user.id label="Retour à la fiche membre" shape="user"}
	{linkbutton href="!services/save.php?user=%d"|args:$user.id label="Inscrire à une activité" shape="plus"}
</p>

{form_errors}

<dl class="cotisation">
	<dt>Statut des inscriptions</dt>
	{foreach from=$services item="service"}
	<dd>
		{$service.label}
		{if $service.status == -1 && $service.end_date} — terminée
		{elseif $service.status == -1} — <b class="error">en retard</b>
		{elseif $service.status == 1 && $service.end_date} — <b class="confirm">en cours</b>
		{elseif $service.status == 1} — <b class="confirm">à jour</b>{/if}
		{if $service.status.expiry_date} — expire le {$service.expiry_date|date_short}{/if}
		{if !$service.paid} — <b class="error">À payer&nbsp;!</b>{/if}
	</dd>
	{foreachelse}
	<dd>
		Aucune inscription.
	</dd>
	{/foreach}
	<dt>Nombre d'inscriptions pour ce membre</dt>
	<dd>
		{$list->count()}
		{if $session->canAccess('membres', Membres::DROIT_ADMIN)}
			{linkbutton href="?export=csv" shape="export" label="Export CSV"}
			{linkbutton href="?export=ods" shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>

{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>
			<th>{$row.label}</th>
			<td>{$row.date|date_short}</td>
			<td>{$row.expiry|date_short}</td>
			<td>{$row.fee}</td>
			<td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td>
			<td>{$row.amount|raw|money_currency}</td>
			<td class="actions">
				{if $session->canAccess('membres', Membres::DROIT_ECRITURE)}
					{if $row.paid}
						{linkbutton shape="reset" label="Marquer comme non payé" href="?id=%d&su_id=%d&paid=0"|args:$user.id,$row.id}
					{else}
						{linkbutton shape="check" label="Marquer comme payé" href="?id=%d&su_id=%d&paid=1"|args:$user.id,$row.id}
					{/if}
					{linkbutton shape="delete" label="Supprimer" href="user_delete.php?id=%d"|args:$row.id}
				{/if}
				{if $session->canAccess('compta', Membres::DROIT_ACCES) && $row.id_account}
					{linkbutton shape="menu" label="Liste des écritures" href="!acc/transactions/service_user.php?id=%d&user=%d"|args:$row.id,$user.id}
				{/if}
				{if $session->canAccess('compta', Membres::DROIT_ECRITURE) && $row.id_account}
					{linkbutton shape="plus" label="Nouveau règlement" href="payment.php?id=%d"|args:$row.id}
				{/if}
			</td>
		</tr>
	{/foreach}

	</tbody>
</table>

{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}


{include file="admin/_foot.tpl"}

Added src/templates/services/user_delete.tpl version [e256118085].





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
{include file="admin/_head.tpl" title="Supprimer une inscription" current="membres/services"}

{include file="services/_nav.tpl" current="index"}

{include file="common/delete_form.tpl"
	legend="Supprimer cette inscription ?"
	warning="Êtes-vous sûr de vouloir supprimer cette inscription ?"
	info="Les écritures comptables liées à cette inscription ne seront pas supprimées, la comptabilité demeurera inchangée."}

{include file="admin/_foot.tpl"}

Modified src/www/_route.php from [c720a9392c] to [d7347d2841].

1
2
3
4
5
6
7
8
9
10
11
12
13




14
15
16
17
18
19
20
<?php

namespace Garradin;

if (empty($_SERVER['REQUEST_URI'])) {
	die('Appel non supporté');
}

$uri = $_SERVER['REQUEST_URI'];

if ('_route.php' === basename($uri)) {
	die('Appel interdit');
}





if (($pos = strpos($uri, '?')) !== false)
{
	$uri = substr($uri, 0, $pos);
}

if (file_exists(__DIR__ . $uri))













>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace Garradin;

if (empty($_SERVER['REQUEST_URI'])) {
	die('Appel non supporté');
}

$uri = $_SERVER['REQUEST_URI'];

if ('_route.php' === basename($uri)) {
	die('Appel interdit');
}

if ('favicon.ico' === basename($uri)) {
	die('');
}

if (($pos = strpos($uri, '?')) !== false)
{
	$uri = substr($uri, 0, $pos);
}

if (file_exists(__DIR__ . $uri))
39
40
41
42
43
44
45






46
47
48
49
}
elseif (preg_match('!/f/([\d\w]+)/(.+)!', $uri, $match))
{
	$_GET['id'] = $match[1];
	$_GET['file'] = $match[2];
	require __DIR__ . '/file.php';
}






else
{
	require __DIR__ . '/index.php';
}







>
>
>
>
>
>




43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
}
elseif (preg_match('!/f/([\d\w]+)/(.+)!', $uri, $match))
{
	$_GET['id'] = $match[1];
	$_GET['file'] = $match[2];
	require __DIR__ . '/file.php';
}
elseif (preg_match('!/admin/!', $uri, $match))
{
	require __DIR__ . '/_inc.php';
	http_response_code(404);
	throw new UserException('Cette page n\'existe pas.');
}
else
{
	require __DIR__ . '/index.php';
}

Added src/www/admin/acc/_inc.php version [734ee883db].





































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$current_year_id = $session->get('acc_year');

if ($current_year_id) {
	// Check that the year is still valid
	$current_year = Years::get($current_year_id);

	if (!$current_year || $current_year->closed) {
		$current_year_id = null;
		$session->set('acc_year', null);
	}
}

if (!$current_year_id) {
	$current_year = Years::getCurrentOpenYear();

	if ($current_year) {
		$current_year_id = $current_year->id();
		$session->set('acc_year', $current_year_id);
	}
}

define('Garradin\CURRENT_YEAR_ID', $current_year_id);

$tpl->assign('current_year', $current_year);

Added src/www/admin/acc/accounts/deposit.php version [aab2604a33].





























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Transactions;
use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$account = Accounts::get((int)qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$checked = f('deposit') ?: [];

$journal = $account->getDepositJournal(CURRENT_YEAR_ID, $checked);
$transaction = new Transaction;
$transaction->id_year = CURRENT_YEAR_ID;

$rules = [
	'deposit' => 'array|required',
];

// Enregistrement des cases cochées
if (f('save') && $form->check('acc_deposit_' . $account->id, $rules))
{
	try {
		$transaction->importFromDepositForm();
		Transactions::saveDeposit($transaction, $journal, f('deposit'));
		Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id());
	}
	catch (UserException $e) {
		$journal = $account->getDepositJournal(CURRENT_YEAR_ID);
		$form->addError($e->getMessage());
	}
}

$date = new \DateTime;

if ($date > $current_year->end_date) {
	$date = $current_year->end_date;
}

$target = $account::TYPE_BANK;

$tpl->assign(compact(
	'account',
	'journal',
	'date',
	'target',
	'checked'
));

$tpl->display('acc/accounts/deposit.tpl');

Added src/www/admin/acc/accounts/index.php version [c826784eaa].































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace Garradin;

use Garradin\Accounting\Reports;

require_once __DIR__ . '/../_inc.php';

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$tpl->assign('chart_id', $current_year->id_chart);
$tpl->assign('grouped_accounts', Reports::getClosingSumsFavoriteAccounts(['year' => CURRENT_YEAR_ID]));

$tpl->display('acc/accounts/index.tpl');

Modified src/www/admin/acc/accounts/journal.php from [768483fe88] to [73088600e8].

1
2
3



4
5
6
7
8
9
10
11
12
13

14
15
16

17
18


19


20
21
22
23
24
25
26


27
28
29

30
31
32
33


34
35
36
37
38
39
40
<?php
namespace Garradin;




require_once __DIR__ . '/../_inc.php';

$compte = $comptes->get(qg('id'));

if (!$compte)
{
    throw new UserException("Le compte demandé n'existe pas.");
}

$journal = new Compta\Journal;


// Récupération de l'exercice courant et sélectionné
$exercices = new Compta\Exercices;

$exercice = (int) qg('exercice') ?: $exercices->getCurrent()->id;



$solde = $journal->getSolde($compte->id, false, $exercice);



if (($compte->position & Compta\Comptes::ACTIF) || ($compte->position & Compta\Comptes::CHARGE))
{
    $tpl->assign('credit', '-');
    $tpl->assign('debit', '+');
}
else


{
    $tpl->assign('credit', '+');
    $tpl->assign('debit', '-');

}

$tpl->assign('exercices', $exercices->getList());
$tpl->assign('exercice_selectionne', $exercice);



$tpl->assign('compte', $compte);
$tpl->assign('solde', $solde);
$tpl->assign('journal', $journal->getJournalCompte($compte->id, false, $exercice));
$tpl->assign('suivi', qg('suivi'));

$tpl->display('admin/compta/comptes/journal.tpl');



>
>
>


|

|
<
|


<
>

<
<
>
|
|
>
>
|
>
>
|
<
|
<
|

|
>
>
|
|
|
>


<
|
>
>

<
|
<
|

|
1
2
3
4
5
6
7
8
9
10
11

12
13
14

15
16


17
18
19
20
21
22
23
24
25

26

27
28
29
30
31
32
33
34
35
36
37

38
39
40
41

42

43
44
45
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$account = Accounts::get((int) qg('id'));

if (!$account) {

	throw new UserException("Le compte demandé n'existe pas.");
}


$year_id = (int) qg('year') ?: CURRENT_YEAR_ID;



if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);

	if (!$year) {
		throw new UserException("L'exercice demandé n'existe pas.");
	}



	$tpl->assign('year', $year);
}

$can_edit = $session->canAccess('compta', Membres::DROIT_ADMIN) && !$year->closed;
$simple = qg('simple');

// Use simplified view for favourite accounts
if (null === $simple) {
	$simple = (bool) $account->type;
}


$list = $account->listJournal($year_id, $simple);
$list->setTitle(sprintf('Journal - %s - %s', $account->code, $account->label));
$list->loadFromQueryString();


$sum = $account->getSum($year_id, $simple);

$tpl->assign(compact('simple', 'year', 'account', 'list', 'sum', 'can_edit'));

$tpl->display('acc/accounts/journal.tpl');

Modified src/www/admin/acc/accounts/reconcile.php from [141e71f932] to [23e1bbad96].

1
2
3



4
5
6
7
8
9
10
11
12
13

14

15
16
17
18
19
20
21
22
23
24

25
26

27
28
29
30
31
32
33

34
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51







52



53
54
55


56
57
58
59



60

61





62
63
64
65

66



67
68
69
70
71
72



73
74
75
76

77
78
<?php
namespace Garradin;




require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ECRITURE);

$banques = new Compta\Comptes_Bancaires;
$rapprochement = new Compta\Rapprochement;

$compte = $banques->get(qg('id'));

if (!$compte)

{

    throw new UserException("Le compte demandé n'existe pas.");
}

$solde_initial = $solde_final = 0;

$debut = qg('debut');
$fin = qg('fin');

if ($debut && $fin)
{

    if (!Utils::checkDate($debut) || !Utils::checkDate($fin))
    {

        $form->addError('La date donnée est invalide.');
        $debut = $fin = false;
    }
}

if (!$debut || !$fin)
{

    $debut = date('Y-m-01');
    $fin = date('Y-m-t');

}

$journal = $rapprochement->getJournal($compte->id, $debut, $fin, $solde_initial, $solde_final, (bool) qg('sauf'));

// Enregistrement des cases cochées
if ((f('save') || f('save_next')) && $form->check('compta_rapprocher_' . $compte->id))
{
    try
    {
        $rapprochement->record($journal, f('rapprocher'), $user->id);

        if (f('save'))
        {
            Utils::redirect(Utils::getSelfURL());
        }
        else







        {



            $next = Utils::modifyDate($debut, '+1 month', true);
            Utils::redirect(sprintf('%scompta/banques/rapprocher.php?id=%s&debut=%s&fin=%s&sauf=%s', ADMIN_URL, $compte->id, date('Y-m-01', $next), date('Y-m-t', $next), (int) qg('sauf')));
        }


    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());



    }

}






if (substr($debut, 0, 7) == substr($fin, 0, 7))
{
    $tpl->assign('prev', Utils::modifyDate($debut, '-1 month', true));

    $tpl->assign('next', Utils::modifyDate($debut, '+1 month', true));



}

$tpl->assign('compte', $compte);
$tpl->assign('debut', $debut);
$tpl->assign('fin', $fin);




$tpl->assign('journal', $journal);

$tpl->assign('solde_initial', $solde_initial);
$tpl->assign('solde_final', $solde_final);


$tpl->display('admin/compta/banques/rapprocher.tpl');



>
>
>


|

|
|
|
<

<
>
|
>
|


|
|
<
|

|

>
|
|
>
|
<
|


|
<
>
|
|
>


|


|
<
<
<
|

|
<
|
|
|
>
>
>
>
>
>
>
|
>
>
>
|
<
|
>
>
|
<
|
<
>
>
>
|
>
|
>
>
>
>
>
|
<
|
<
>
|
>
>
>


|
|
<
|
>
>
>
|
|
<
<
>

|
1
2
3
4
5
6
7
8
9
10
11
12
13

14

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31

32
33
34
35

36
37
38
39
40
41
42
43
44
45



46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

64
65
66
67

68

69
70
71
72
73
74
75
76
77
78
79
80

81

82
83
84
85
86
87
88
89
90

91
92
93
94
95
96


97
98
99
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Transactions;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}



$account = Accounts::get((int)qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$start = new \DateTime('first day of this month');
$end = new \DateTime('last day of this month');

$only = (bool) qg('only');

if (null !== qg('start') && null !== qg('end'))
{
	$start = \DateTime::createFromFormat('!d/m/Y', qg('start'));
	$end = \DateTime::createFromFormat('!d/m/Y', qg('end'));

	if (!$start || !$end) {
		$form->addError('La date donnée est invalide.');

	}
}

if ($start < $current_year->start_date || $start > $current_year->end_date

	|| $end < $current_year->start_date || $end > $current_year->end_date) {
	$start = clone $current_year->start_date;
	$end = clone $start;
	$end->modify('last day of this month');
}

$journal = $account->getReconcileJournal(CURRENT_YEAR_ID, $start, $end, $only);

// Enregistrement des cases cochées
$form->runIf(f('save') || f('save_next'), function () use ($journal, $start, $end, $account, $only) {



	Transactions::saveReconciled($journal, f('reconcile'));

	if (f('save')) {

		Utils::redirect(Utils::getSelfURL());
	}
	else {
		$start->modify('+1 month');
		$end->modify('+1 month');
		$url = sprintf('%sacc/accounts/reconcile.php?id=%s&start=%s&end=%s&only=%d',
			ADMIN_URL, $account->id(), $start->format('d/m/Y'), $end->format('d/m/Y'), $only);
		Utils::redirect($url);
	}
}, 'acc_reconcile_' . $account->id());

$prev = clone $start;
$next = clone $start;
$prev->modify('-1 month');
$next->modify('+1 month');


if ($next > $current_year->end_date) {
	$next = null;
}



if ($prev < $current_year->start_date) {
	$prev = null;
}

$self_uri = Utils::getSelfURI(false);

if (null !== $prev) {
	$prev = [
		'date' => $prev,
		'url' => sprintf($self_uri . '?id=%d&start=%s&end=%s&only=%d', $account->id, $prev->format('01/m/Y'), $prev->format('t/m/Y'), $only),
	];
}



if (null !== $next) {
	$next = [
		'date' => $next,
		'url' => sprintf($self_uri . '?id=%d&start=%s&end=%s&only=%d', $account->id, $next->format('01/m/Y'), $next->format('t/m/Y'), $only),
	];
}

$tpl->assign(compact(
	'account',

	'start',
	'end',
	'prev',
	'next',
	'journal',
	'only'


));

$tpl->display('acc/accounts/reconcile.tpl');

Added src/www/admin/acc/accounts/reconcile_assist.php version [a96b3c063b].

























































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Transactions;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$account = Accounts::get((int)qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$csrf_key = 'acc_reconcile_assist_' . $account->id();
$csv = new CSV_Custom($session, 'acc_reconcile_csv');

$csv->setColumns([
	'label'          => 'Libellé',
	'date'           => 'Date',
	'notes'          => 'Notes',
	'reference'      => 'Numéro pièce comptable',
	'p_reference'    => 'Référence paiement',
	'amount'         => 'Montant',
]);

$csv->setMandatoryColumns(['label', 'date', 'amount']);

$form->runIf('cancel', function () use ($csv) {
	$csv->clear();
}, $csrf_key, Utils::getSelfURL());

$form->runIf(f('upload') && isset($_FILES['file']['name']), function () use ($csv) {
	$csv->load($_FILES['file']);
}, $csrf_key, Utils::getSelfURL());

$form->runIf('assign', function () use ($csv) {
	$csv->setTranslationTable(f('translation_table'));
	$csv->skip((int)f('skip_first_line'));
}, $csrf_key, Utils::getSelfURL());

$start = null;
$end = null;
$journal = null;

if ($csv->ready()) {
	foreach ($csv->iterate() as $line => $row) {
		$date = \DateTime::createFromFormat('!d/m/Y', $row->date);
		if (!$date) {
			$form->addError(sprintf('Ligne %d : format de date invalide (%s)', $line, $row->date));
			continue;
		}

		if ($date < $start) {
			$start = $date;
		}

		if ($date > $end) {
			$end = $date;
		}
	}

	if ($start < $current_year->start_date || $start > $current_year->end_date) {
		$start = clone $current_year->start_date;
	}

	if ($end < $current_year->start_date || $end > $current_year->end_date) {
		$end = clone $current_year->end_date;
	}
}

if ($start && $end) {
	$journal = $account->getReconcileJournal(CURRENT_YEAR_ID, $start, $end);
}

// Enregistrement des cases cochées
$form->runIf('save', function () use ($journal, $csv) {
	Transactions::saveReconciled($journal, f('reconcile'));
	$csv->clear();
}, $csrf_key, Utils::getSelfURL());

$lines = null;

if ($journal && $csv->ready()) {
	try {
		$lines = $account->mergeReconcileJournalAndCSV($journal, $csv);
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact(
	'account',
	'start',
	'end',
	'lines',
	'csv',
	'csrf_key'
));

$tpl->display('acc/accounts/reconcile_assist.tpl');

Added src/www/admin/acc/accounts/simple.php version [50a686e3e9].













































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/../_inc.php';

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$year = $current_year;

$types = [
	Transaction::TYPE_REVENUE => 'Recettes',
	Transaction::TYPE_EXPENSE => 'Dépenses',
	Transaction::TYPE_TRANSFER => 'Virements',
	Transaction::TYPE_DEBT => 'Dettes',
	Transaction::TYPE_CREDIT => 'Créances',
	Transaction::TYPE_ADVANCED => 'Saisies avancées',
];

$type = qg('type') ?? Transaction::TYPE_REVENUE;

if (!array_key_exists($type, $types)) {
	$type = key($types);
}

$list = Transactions::listByType(CURRENT_YEAR_ID, $type);
$list->setTitle(sprintf('Suivi - %s', $types[$type]));
$list->loadFromQueryString();

$can_edit = $session->canAccess('compta', Membres::DROIT_ADMIN) && !$year->closed;

$tpl->assign(compact('type', 'list', 'types', 'can_edit', 'year'));

$tpl->display('acc/accounts/simple.tpl');

Added src/www/admin/acc/charts/accounts/all.php version [f7050bf620].





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace Garradin;

use Garradin\Accounting\Charts;

require_once __DIR__ . '/../../_inc.php';

$chart = null;

if ($id = (int)qg('id')) {
	$chart = Charts::get($id);
}
elseif (CURRENT_YEAR_ID) {
	$year = $current_year;
	$chart = $year->chart();
}

if (!$chart) {
	throw new UserException('Aucun plan comptable spécifié');
}

$accounts = $chart->accounts();

$tpl->assign('chart', $chart);
$tpl->assign('accounts', $accounts->listAll());
$tpl->display('acc/charts/accounts/all.tpl');

Added src/www/admin/acc/charts/accounts/delete.php version [6c837a3adc].

































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$account = Accounts::get((int) qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$chart = $account->chart();

if ($chart->archived) {
	throw new UserException("Il n'est pas possible de modifier un compte d'un plan comptable archivé.");
}

if (($chart->code && !$account->user) || !$account->canDelete()) {
	throw new UserException("Ce compte ne peut être supprimé car des écritures y sont liées (peut-être sur l'exercice courant ou sur un exercice clôt).\nSi vous souhaitez faire du ménage dans la liste des comptes il est recommandé de créer un nouveau comptable.");
}

if (f('delete') && $form->check('acc_accounts_delete_' . $account->id()))
{
	try
	{
		$page = '';

		if (!$account->type) {
			$page = 'all.php';
		}

		$account->delete();

		Utils::redirect(sprintf('%sacc/charts/accounts/%s?id=%d', ADMIN_URL, $page, $account->id_chart));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact('account'));

$tpl->display('acc/charts/accounts/delete.tpl');

Added src/www/admin/acc/charts/accounts/edit.php version [cd87e2dec4].















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$account = Accounts::get((int) qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$chart = $account->chart();

if ($chart->archived) {
	throw new UserException("Il n'est pas possible de modifier un compte d'un plan comptable archivé.");
}

$edit_disabled = !$account->canEdit();

if (f('edit') && $form->check('acc_accounts_edit_' . $account->id()))
{
	try {
		if ($edit_disabled) {
			$account->importLimitedForm();
		}
		else {
			$account->importForm();
		}

		$account->save();

		$page = '';

		if (!$account->type) {
			$page = 'all.php';
		}

		Utils::redirect(sprintf('%sacc/charts/accounts/%s?id=%d', ADMIN_URL, $page, $account->id_chart));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$types = $account::TYPES_NAMES;
$types[0] = '-- Pas un compte favori';

$tpl->assign(compact('types', 'account', 'edit_disabled'));

$tpl->display('acc/charts/accounts/edit.tpl');

Added src/www/admin/acc/charts/accounts/index.php version [5e00b51e32].





















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace Garradin;

use Garradin\Accounting\Charts;

require_once __DIR__ . '/../../_inc.php';

$chart = null;

if ($id = (int)qg('id')) {
	$chart = Charts::get($id);
}
elseif (CURRENT_YEAR_ID) {
	$year = $current_year;
	$chart = $year->chart();
}

if (!$chart) {
	throw new UserException('Aucun plan comptable spécifié');
}

$accounts = $chart->accounts();

$tpl->assign('chart', $chart);
$tpl->assign('accounts_grouped', $accounts->listCommonGrouped());
$tpl->display('acc/charts/accounts/index.tpl');

Added src/www/admin/acc/charts/accounts/new.php version [9b98b97dd0].





































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Accounting\Accounts;
use Garradin\Accounting\Charts;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$chart = Charts::get((int)qg('id'));

if (!$chart) {
	throw new UserException('Ce plan comptable n\'existe pas');
}

if ($chart->archived) {
	throw new UserException("Il n'est pas possible de modifier un plan comptable archivé.");
}

$account = new Account;
$account->position = Account::ASSET_OR_LIABILITY;

$types = $account::TYPES_NAMES;
$types[0] = '-- Pas un compte favori';

$translate_type_position = [
	Account::TYPE_REVENUE => Account::REVENUE,
	Account::TYPE_EXPENSE => Account::EXPENSE,
];

$translate_type_codes = $chart->accounts()->getNextCodesForTypes();

$simple = false;

// Simple creation with pre-determined account type
if ($type = (int)qg('type')) {
	$account->type = $type;

	$simple = true;

	$types = array_slice($types, 1, null, true);

	if (isset($translate_type_codes[$type])) {
		$account->code = $translate_type_codes[$type];
	}
}


if (f('save') && $form->check('acc_accounts_new'))
{
	try
	{
		if ($simple) {
			$account->importSimpleForm($translate_type_position, $translate_type_codes);
		}
		else {
			$account->importForm();
		}

		$account->id_chart = $chart->id();
		$account->user = 1;
		$account->save();

		$page = '';

		if (!$account->type) {
			$page = 'all.php';
		}

		Utils::redirect(sprintf('%sacc/charts/accounts/%s?id=%d', ADMIN_URL, $page, $account->id_chart));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact('simple', 'types', 'account', 'translate_type_position', 'translate_type_codes', 'chart'));

$tpl->display('acc/charts/accounts/new.tpl');

Added src/www/admin/acc/charts/accounts/selector.php version [17dde2f21a].















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php

namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Accounting\Charts;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../../_inc.php';

header('X-Frame-Options: SAMEORIGIN', true);

$targets = qg('targets');
$chart = qg('chart');

// Cache the page until the charts have changed
$hash = sha1($targets . $chart);
$expiry = Config::getInstance()->get('last_chart_change') ?: time();

Utils::HTTPCache($hash, $expiry);

if ($chart) {
	$chart = Charts::get((int)qg('chart'));
}
elseif (qg('year')) {
	$year = Years::get((int)qg('year'));

	if ($year) {
		$chart = $year->chart();
	}
}
elseif ($current_year) {
	$chart = $current_year->chart();
}

if (!$chart) {
	throw new UserException('Aucun exercice ouvert disponible');
}

$accounts = $chart->accounts();

$tpl->assign(compact('chart', 'targets'));

$all = (bool) qg('all');

if (!$targets) {
	$tpl->assign('accounts', !$all ? $accounts->listCommonTypes() : $accounts->listAll());
}
else {
	$tpl->assign('grouped_accounts', $accounts->listCommonGrouped(explode(':', $targets)));
}

$tpl->assign('all', $all);

$tpl->display('acc/charts/accounts/selector.tpl');

Added src/www/admin/acc/charts/delete.php version [35b792e76d].







































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace Garradin;

use Garradin\Accounting\Charts;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$chart = Charts::get((int) qg('id'));

if (!$chart) {
	throw new UserException("Le plan comptable demandé n'existe pas.");
}

if (!$chart->canDelete()) {
	throw new UserException("Ce plan comptable ne peut être supprimé car il est lié à des exercices");
}

if (f('delete') && $form->check('acc_charts_delete_' . $chart->id()))
{
	try
	{
		$chart->delete();
		Utils::redirect(sprintf('%sacc/charts/', ADMIN_URL));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact('chart'));

$tpl->display('acc/charts/delete.tpl');

Added src/www/admin/acc/charts/edit.php version [e0ba778302].





































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
namespace Garradin;

use Garradin\Accounting\Charts;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$chart = Charts::get((int) qg('id'));

if (!$chart) {
	throw new UserException("Le plan comptable demandé n'existe pas.");
}

if (f('save') && $form->check('acc_charts_edit_' . $chart->id()))
{
	try
	{
		$chart->importForm();
		$chart->save();

		Utils::redirect(sprintf('%sacc/charts/', ADMIN_URL));
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact('chart'));
$tpl->assign('country_list', Utils::getCountryList());

$tpl->display('acc/charts/edit.tpl');

Added src/www/admin/acc/charts/export.php version [34dc474779].









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace Garradin;

use Garradin\Accounting\Charts;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$chart = Charts::get((int) qg('id'));

if (!$chart) {
	throw new UserException("Le plan comptable demandé n'existe pas.");
}

CSV::export(
	null !== qg('ods') ? 'ods' : 'csv',
	sprintf('Plan comptable - %s - %s', Config::getInstance()->get('nom_asso'), $chart->label),
	$chart->accounts()->export()
);

Added src/www/admin/acc/charts/import.php version [db18f89e13].

























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Charts;
use Garradin\Entities\Accounting\Chart;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

if (f('import') && $form->check('acc_charts_import', ['file' => 'file|required'])) {
	try {
		$chart = new Chart;
		$chart->importForm();
		$chart->save();
		$chart->accounts()->importUpload($_FILES['file']); // This will save everything
		Utils::redirect(ADMIN_URL . 'acc/charts/');
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$tpl->assign('columns', implode(', ', Accounts::EXPECTED_CSV_COLUMNS));
$tpl->assign('country_list', Utils::getCountryList());

$tpl->display('acc/charts/import.tpl');

Added src/www/admin/acc/charts/index.php version [78edbd5354].









































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Chart;
use Garradin\Accounting\Charts;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$tpl->assign('list', Charts::list());

if ($session->canAccess('compta', Membres::DROIT_ADMIN)) {
	if (f('new') && $form->check('acc_charts_new')) {
		try {
			$chart = new Chart;
			$chart->importForm();
			$chart->save();

			if (f('copy')) {
				$chart->accounts()->copyFrom((int) f('copy'));
			}

			Utils::redirect(Utils::getSelfURI(false));
		}
		catch (UserException $e) {
			$form->addError($e->getMessage());
		}
	}

	$tpl->assign('from', (int)qg('from'));
	$tpl->assign('charts_groupped', Charts::listByCountry());
	$tpl->assign('country_list', Utils::getCountryList());
}

$tpl->display('acc/charts/index.tpl');

Added src/www/admin/acc/index.php version [f1d40ee231].































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace Garradin;

use Garradin\Accounting\Years;
use Garradin\Accounting\Graph;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$tpl->assign('graphs', Graph::URL_LIST);

$tpl->assign('years', Years::listOpen());

$tpl->display('acc/index.tpl');

Modified src/www/admin/acc/reports/_inc.php from [73c58d8a9d] to [461ecc0cc6].

1
2
3
4



5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
41


<?php

namespace Garradin;




require_once __DIR__ . '/../_inc.php';


$rapports = new Compta\Rapports;
$criterias = [];

if (qg('projet'))
{
	$projets = new Compta\Projets;
	$projet = $projets->get((int) qg('projet'));

	if (!$projet)
	{
		throw new UserException('Projet inconnu.');
	}

	$criterias['id_projet'] = $projet->id;
	$tpl->assign('projet', $projet);
}
elseif (qg('exercice'))
{
	$exercices = new Compta\Exercices;


	$exercice = $exercices->get((int)qg('exercice'));

	if (!$exercice)
	{
		throw new UserException('Exercice inconnu.');
	}

	$criterias['id_exercice'] = $exercice->id;
	$tpl->assign('cloture', $exercice->cloture ? $exercice->fin : time());
	$tpl->assign('exercice', $exercice);
}
else

{
	throw new UserException('Critère de rapport inconnu.');
}






>
>
>
|

>
|


|

<
|

|
<
|


|
|

<
|
<
>
|
|

|
<



|
|
|

|
>



>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

16
17
18

19
20
21
22
23
24

25

26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

namespace Garradin;

use Garradin\Accounting\Years;
use Garradin\Accounting\Accounts;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$criterias = [];

if (qg('analytical'))
{

	$account = Accounts::get((int) qg('analytical'));

	if (!$account) {

		throw new UserException('Numéro de compte analytique inconnu.');
	}

	$criterias['analytical'] = $account->id();
	$tpl->assign('analytical', $account);
}



if (qg('year'))
{
	$year = Years::get((int) qg('year'));

	if (!$year) {

		throw new UserException('Exercice inconnu.');
	}

	$criterias['year'] = $year->id();
	$tpl->assign('year', $year);
	$tpl->assign('close_date', $year->closed ? $year->end_date : time());
}

if (!count($criterias))
{
	throw new UserException('Critère de rapport inconnu.');
}

$tpl->assign('criterias_query', http_build_query($criterias));

Modified src/www/admin/acc/reports/balance_sheet.php from [a817281776] to [8ddee02813].

1
2
3
4



5
6

7




8

9

<?php

namespace Garradin;




require_once __DIR__ . '/_inc.php';


$tpl->assign('bilan', $rapports->bilan($criterias));






$tpl->display('admin/compta/rapports/bilan.tpl');





>
>
>


>
|
>
>
>
>

>
|
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Accounting\Reports;
use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/_inc.php';

$balance = Reports::getBalanceSheet($criterias);

$liability = $balance[Account::LIABILITY];
$asset = $balance[Account::ASSET];
$liability_sum = $balance['sums'][Account::LIABILITY];
$asset_sum = $balance['sums'][Account::ASSET];

$tpl->assign(compact('liability', 'asset', 'liability_sum', 'asset_sum'));

$tpl->display('acc/reports/balance_sheet.tpl');

Added src/www/admin/acc/reports/graph_pie.php version [ca1e3741d3].





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
<?php
namespace Garradin;

use Garradin\Accounting\Graph;

require_once __DIR__ . '/_inc.php';

header('Content-Type: image/svg+xml');

echo Graph::pie(qg('type'), $criterias);

Added src/www/admin/acc/reports/graph_plot.php version [041f1bcddf].





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
<?php
namespace Garradin;

use Garradin\Accounting\Graph;

require_once __DIR__ . '/_inc.php';

header('Content-Type: image/svg+xml');

echo Graph::plot(qg('type'), $criterias);

Added src/www/admin/acc/reports/graph_plot_all.php version [d2282ca501].

























>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace Garradin;

use Garradin\Accounting\Graph;

require_once __DIR__ . '/../_inc.php';

qv(['type' => 'string|required']);

header('Content-Type: image/svg+xml');

echo Graph::plot(qg('type'), [], Graph::MONTHLY_INTERVAL, 600);

Added src/www/admin/acc/reports/graphs.php version [fc9df953bc].

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace Garradin;

use Garradin\Accounting\Years;
use Garradin\Accounting\Graph;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$year = Years::get((int)qg('year'));

$tpl->assign('graphs', Graph::URL_LIST);
$tpl->assign('year', $year);

$tpl->display('acc/reports/graphs.tpl');

Modified src/www/admin/acc/reports/journal.php from [d73a2c213c] to [a5c36b7f16].

1
2
3


4
5
6
7
8
9
<?php

namespace Garradin;



require_once __DIR__ . '/_inc.php';

$tpl->assign('journal', $rapports->journal($criterias));

$tpl->display('admin/compta/rapports/journal.tpl');



>
>



|

|
1
2
3
4
5
6
7
8
9
10
11
<?php

namespace Garradin;

use Garradin\Accounting\Reports;

require_once __DIR__ . '/_inc.php';

$tpl->assign('journal', Reports::getJournal($criterias));

$tpl->display('acc/reports/journal.tpl');

Modified src/www/admin/acc/reports/ledger.php from [f6b332c56d] to [ea925577f6].

1
2
3


4
5
6
7
8
9
<?php

namespace Garradin;



require_once __DIR__ . '/_inc.php';

$tpl->assign('livre', $rapports->grandLivre($criterias));

$tpl->display('admin/compta/rapports/grand_livre.tpl');



>
>



|

|
1
2
3
4
5
6
7
8
9
10
11
<?php

namespace Garradin;

use Garradin\Accounting\Reports;

require_once __DIR__ . '/_inc.php';

$tpl->assign('ledger', Reports::getGeneralLedger($criterias));

$tpl->display('acc/reports/ledger.tpl');

Added src/www/admin/acc/reports/projects.php version [ebc0bf8009].









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Reports;
use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$by_year = (bool)qg('by_year');

$tpl->assign(compact('by_year'));
$tpl->assign('list', Reports::getAnalyticalSums($by_year));

$tpl->assign('analytical_type', Account::TYPE_ANALYTICAL);
$tpl->assign('analytical_accounts_count', CURRENT_YEAR_ID ? $current_year->accounts()->countByType(Account::TYPE_ANALYTICAL) : null);

$tpl->display('acc/reports/projects.tpl');

Modified src/www/admin/acc/reports/statement.php from [4bb5da1833] to [a66219b6ce].

1
2
3
4



5
6


7


8


9
10










<?php

namespace Garradin;




require_once __DIR__ . '/_inc.php';



$tpl->assign('compte_resultat', $rapports->compteResultat($criterias, [6, 7]));


$tpl->assign('compte_nature', $rapports->compteResultat($criterias, [86, 87]));



$tpl->display('admin/compta/rapports/compte_resultat.tpl');














>
>
>


>
>
|
>
>
|
>
>
|
|
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace Garradin;

use Garradin\Accounting\Reports;
use Garradin\Entities\Accounting\Account;

require_once __DIR__ . '/_inc.php';

$revenue = Reports::getClosingSumsWithAccounts($criterias + ['position' => Account::REVENUE]);
$expense = Reports::getClosingSumsWithAccounts($criterias + ['position' => Account::EXPENSE], null, true);

$get_sum = function (array $in): int {
	$sum = 0;

	foreach ($in as $row) {
		$sum += $row->sum;
	}

	return abs($sum);
};

$revenue_sum = $get_sum($revenue);
$expense_sum = $get_sum($expense);
$result = $revenue_sum - $expense_sum;

$tpl->assign(compact('revenue', 'expense', 'revenue_sum', 'expense_sum', 'result'));

$tpl->display('acc/reports/statement.tpl');

Added src/www/admin/acc/reports/trial_balance.php version [a483e23c57].























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
<?php

namespace Garradin;

use Garradin\Accounting\Reports;

require_once __DIR__ . '/_inc.php';

$tpl->assign('balance', Reports::getClosingSumsWithAccounts($criterias));

$tpl->display('acc/reports/trial_balance.tpl');

Added src/www/admin/acc/saved_searches.php version [76dcbfceee].



















>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$target = 'compta';
$search_url = ADMIN_URL . 'acc/search.php';

require __DIR__ . '/../common/saved_searches.php';

Added src/www/admin/acc/search.php version [1fddd71e5c].

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$target = 'compta';

require __DIR__ . '/../common/search.php';

Added src/www/admin/acc/transactions/actions.php version [0ea83417c0].

































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php

namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$check = f('check');

if (!$check || !is_array($check)) {
	throw new UserException('Aucune écriture n\'a été sélectionnée.');
}

$transactions = array_unique(array_values($check));
$lines = array_keys($check);

$csrf_key = 'acc_actions';

// Delete transactions
$form->runIf('delete', function () use ($transactions) {
	foreach ($transactions as $id) {
		$transaction = Transactions::get((int) $id);

		if (!$transaction) {
			throw new UserException('Cette écriture n\'existe pas');
		}

		$transaction->delete();
	}
}, $csrf_key, f('from') ?: ADMIN_URL);

// Add/remove lines to analytical
$form->runIf('change_analytical', function () use ($lines) {
	$id = f('id_analytical') ?: null;
	Transactions::setAnalytical($id, $lines);
}, $csrf_key, f('from') ?: ADMIN_URL);

$from = f('from');
$count = count($check);
$extra = compact('check', 'from');
$tpl->assign(compact('csrf_key', 'check', 'count', 'extra'));

if (f('action') == 'delete')
{
	$tpl->display('acc/transactions/actions_delete.tpl');
}
else
{
	// Get year to get analytical accounts
	$year = Years::get((int) f('year'));

	if (!$year) {
		throw new UserException("Aucun exercice sélectionné.");
	}

	$analytical = $year->chart()->accounts()->listAnalytical();
	$tpl->assign('analytical_accounts', ['' => '-- Aucun projet'] + $analytical);

	$tpl->display('acc/transactions/actions_analytical.tpl');
}

Added src/www/admin/acc/transactions/creator.php version [5ea9d63a0d].











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
namespace Garradin;

use Garradin\Accounting\Reports;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$u = (new Membres)->get((int)qg('id'));

if (!$u) {
	throw new UserException('Ce membre n\'existe pas');
}

$criterias = ['creator' => $u->id];

$tpl->assign('journal', Reports::getJournal($criterias));
$tpl->assign('transaction_creator', $u);

$tpl->display('acc/transactions/creator.tpl');

Modified src/www/admin/acc/transactions/delete.php from [0f0bd1e03f] to [f22c7ed0fd].

1
2
3
4


5
6
7
8
9

10
11


12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace Garradin;



require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$journal = new Compta\Journal;


$operation = $journal->get(qg('id'));



if (!$operation)
{

    throw new UserException("L'opération demandée n'existe pas.");
}

if (f('delete'))
{
    if ($form->check('compta_supprimer_' . $operation->id))
    {
        try
        {
            $journal->delete($operation->id);
            Utils::redirect(ADMIN_URL . 'compta/operations/');
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('operation', $operation);

$tpl->display('admin/compta/operations/supprimer.tpl');




>
>




<
>

<
>
>
|
<
|
>
|


|

<
<
|
|
|
|
|
|
|
|
|
|
|
<
|

|
1
2
3
4
5
6
7
8
9
10

11
12

13
14
15

16
17
18
19
20
21
22


23
24
25
26
27
28
29
30
31
32
33

34
35
36
<?php

namespace Garradin;

use Garradin\Accounting\Transactions;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);


$transaction = Transactions::get((int) qg('id'));


if (!$transaction) {
	throw new UserException('Cette écriture n\'existe pas');
}


if ($transaction->validated) {
	throw new UserException('Cette écriture est validée et ne peut être modifiée');
}

if (f('delete') && $form->check('acc_delete_' . $transaction->id))
{


	try
	{
		$transaction->delete();
		Utils::redirect(ADMIN_URL . 'acc/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}


$tpl->assign('transaction', $transaction);

$tpl->display('acc/transactions/delete.tpl');

Added src/www/admin/acc/transactions/delete_file.php version [19275e53c5].





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace Garradin;

use Garradin\Accounting\Transactions;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$csrf_key = sprintf('acc_delete_file_%d', qg('id'));
$redirect = sprintf(ADMIN_URL . 'acc/transactions/details.php?id=%d', qg('from'));

require __DIR__ . '/../../common/delete_file.php';

Added src/www/admin/acc/transactions/details.php version [6884309cde].











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;

require_once __DIR__ . '/../_inc.php';

$transaction = Transactions::get((int) qg('id'));

if (!$transaction) {
	throw new UserException('Cette écriture n\'existe pas');
}

$tpl->assign('files', $transaction->listFiles());
$tpl->assign('transaction', $transaction);
$tpl->assign('tr_year', $transaction->year());
$tpl->assign('creator_name', $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null);

$tpl->assign('related_users', $transaction->listLinkedUsers());

$tpl->display('acc/transactions/details.tpl');

Added src/www/admin/acc/transactions/edit.php version [4ea18fe075].





































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

namespace Garradin;

use Garradin\Entities\Accounting\Transaction;
use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$transaction = Transactions::get((int) qg('id'));

if (!$transaction) {
	throw new UserException('Cette écriture n\'existe pas');
}

if ($transaction->validated) {
	throw new UserException('Cette écriture est validée et ne peut être modifiée');
}

$year = Years::get($transaction->id_year);

if ($year->closed) {
	throw new UserException('Cette écriture ne peut être modifiée car elle appartient à un exercice clôturé');
}

$chart = $year->chart();
$accounts = $chart->accounts();

$tpl->assign('chart', $chart);

$rules = [
	'lines' => 'array|required',
];

if (f('save') && $form->check('acc_edit_' . $transaction->id(), $rules)) {
	try {
		$transaction->importFromEditForm();
		$transaction->save();

		// Append file
		if (!empty($_FILES['file']['name'])) {
			$file = Fichiers::upload($_FILES['file']);
			$file->linkTo(Fichiers::LIEN_COMPTA, $transaction->id());
		}

		// Link members
		if (null !== f('users') && is_array(f('users'))) {
			$transaction->updateLinkedUsers(array_keys(f('users')));
		}
		else {
			// Remove all
			$transaction->updateLinkedUsers([]);
		}

		Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id());
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$types_accounts = [];
$lines = [];

if (!empty($_POST['lines']) && is_array($_POST['lines'])) {
	$lines = Utils::array_transpose($_POST['lines']);

	foreach ($lines as &$line) {
		$line = (object) $line;
		$line->credit = Utils::moneyToInteger($line->credit);
		$line->debit = Utils::moneyToInteger($line->debit);
	}
}
else {
	$lines = $transaction->getLinesWithAccounts();

	foreach ($lines as $k => &$line) {
		$line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_name)];
	}
}

if ($transaction->type != Transaction::TYPE_ADVANCED) {
	$types_accounts = $transaction->getTypesAccounts();
}

$amount = $transaction->getLinesCreditSum();

$tpl->assign(compact('transaction', 'lines', 'types_accounts', 'amount'));

$tpl->assign('types_details', Transaction::getTypesDetails());
$tpl->assign('chart_id', $chart->id());
$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->assign('linked_users', $transaction->listLinkedUsersAssoc());

$tpl->display('acc/transactions/edit.tpl');

Modified src/www/admin/acc/transactions/new.php from [20d760aa75] to [7173ab8670].

1
2




3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


19
20
21
22
23
24
25
26
27
28

29

30
31
32
33
34
35
36
37
38
39
40
41


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84


85
86
87
88
89

90
91
92
93
94
95
96
97
98

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118



119

120
121
122
123

124
125
126
127

128

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188

189
<?php
namespace Garradin;





require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ECRITURE);

$journal = new Compta\Journal;

$journal->checkExercice();

$cats = new Compta\Categories;
$banques = new Compta\Comptes_Bancaires;

$type = f('type') ?: 'recette';

if (f('save'))
{


    $form->check('compta_saisie', [
        'libelle' => 'required',
        'date'    => 'date_format:Y-m-d|required',
        'montant' => 'money|required',
    ]);

    if (!$form->hasErrors())
    {
        try
        {

            if ($type == 'avance')

            {
                $id = $journal->add([
                    'libelle'       =>  f('libelle'),
                    'montant'       =>  f('montant'),
                    'date'          =>  f('date'),
                    'compte_credit' =>  f('compte_credit'),
                    'compte_debit'  =>  f('compte_debit'),
                    'numero_piece'  =>  f('numero_piece'),
                    'remarques'     =>  f('remarques'),
                    'id_auteur'     =>  $user->id,
                    'id_projet'     =>  f('projet') ?: null,
                ]);


            }
            elseif ($type == 'virement')
            {
                $id = $journal->add([
                    'libelle'       =>  f('libelle'),
                    'montant'       =>  f('montant'),
                    'date'          =>  f('date'),
                    'compte_debit'  =>  f('compte1'),
                    'compte_credit' =>  f('compte2'),
                    'numero_piece'  =>  f('numero_piece'),
                    'remarques'     =>  f('remarques'),
                    'id_auteur'     =>  $user->id,
                    'id_projet'     =>  f('projet') ?: null,
                ]);
            }
            else
            {
                if ($type == 'recette')
                {
                    $cat = 'categorie_recette';
                }
                else
                {
                    // Dette ou dépense
                    $cat = 'categorie_depense';
                }

                $cat = $cats->get(f($cat));

                if (!$cat)
                {
                    throw new UserException('Il faut choisir une catégorie.');
                }

                if ($type == 'dette')
                {
                    if (!trim(f('compte')) ||
                        (f('compte') != 4010 && f('compte') != 4110))
                    {
                        throw new UserException('Type de dette invalide.');
                    }
                }
                else


                {
                    if (f('moyen_paiement') == 'ES')
                    {
                        $a = Compta\Comptes::CAISSE;
                        $b = $cat->compte;

                    }
                    elseif (in_array(f('moyen_paiement'), ['CH', 'CB']) && f('a_encaisser'))
                    {
                        $a = f('moyen_paiement') == 'CH' 
                            ? Compta\Comptes::CHEQUE_A_ENCAISSER
                            : Compta\Comptes::CARTE_A_ENCAISSER;
                        $b = $cat->compte;
                    }
                    else

                    {
                        if (!trim(f('banque')))
                        {
                            throw new UserException('Le compte bancaire choisi est invalide.');
                        }

                        if (!array_key_exists(f('banque'), $banques->getList()))
                        {
                            throw new UserException('Le compte bancaire choisi n\'existe pas.');
                        }

                        $a = f('banque');
                        $b = $cat->compte;
                    }
                }

                if ($type == 'depense')
                {
                    $debit = $b;
                    $credit = $a;



                }

                elseif ($type == 'recette')
                {
                    $debit = $a;
                    $credit = $b;

                }
                elseif ($type == 'dette')
                {
                    $debit = $cat->compte;

                    $credit = f('compte');

                }

                $id = $journal->add([
                    'libelle'       =>  f('libelle'),
                    'montant'       =>  f('montant'),
                    'date'          =>  f('date'),
                    'moyen_paiement'=>  ($type == 'dette') ? null : f('moyen_paiement'),
                    'numero_cheque' =>  ($type == 'dette') ? null : f('numero_cheque'),
                    'compte_credit' =>  $credit,
                    'compte_debit'  =>  $debit,
                    'numero_piece'  =>  f('numero_piece'),
                    'remarques'     =>  f('remarques'),
                    'id_categorie'  =>  ($type === 'dette') ? null : (int)$cat->id,
                    'id_auteur'     =>  $user->id,
                    'id_projet'     =>  f('projet') ?: null,
                ]);
            }

            $session->set('context_compta_date', f('date'));

            Utils::redirect(ADMIN_URL . 'compta/operations/saisir.php?ok='.(int)$id);
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('type', $type);

$tpl->assign('comptes', $comptes->listTree());
$tpl->assign('moyens_paiement', $cats->listMoyensPaiement());
$tpl->assign('moyen_paiement', f('moyen_paiement') ?: 'ES');
$tpl->assign('categories_depenses', $cats->getList(Compta\Categories::DEPENSES));
$tpl->assign('categories_recettes', $cats->getList(Compta\Categories::RECETTES));
$tpl->assign('comptes_bancaires', $banques->getList());
$tpl->assign('banque', f('banque'));
$tpl->assign('compte_cheque_e_encaisser', Compta\Comptes::CHEQUE_A_ENCAISSER);
$tpl->assign('compte_carte_e_encaisser', Compta\Comptes::CARTE_A_ENCAISSER);
$tpl->assign('projets', (new Compta\Projets)->getAssocList());

if (!$session->get('context_compta_date'))
{
    $exercices = new Compta\Exercices;
    $exercice = $exercices->getCurrent();

    if ($exercice->debut > time() || $exercice->fin < time())
    {
        $session->set('context_compta_date', date('Y-m-d', $exercice->debut));
    }
    else
    {
        $session->get('context_compta_date', date('Y-m-d'));
    }
}

$tpl->assign('date', $session->get('context_compta_date') ?: false);
$tpl->assign('ok', (int) qg('ok'));


$tpl->display('admin/compta/operations/saisir.tpl');


>
>
>
>





|
|
<
|
<
<

|
|
<
|
>
>
|
|
<
<
<
|
<
|
<
<
>
|
>
|
<
<
<
<
<
<
<
<
<
<
<
>
>
|
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
|
|
<
|
<
<
<
|
|
|
<
|
<
<
|
<
<
<
<
<
|
|
|
<
>
>
|
|
<
<
<
>
|
<
<
<
<
<
<
|
|
>
|
|
<
<
<
|
<
<
<
<
|
<
<
|
|

|
<
|
|
>
>
>
|
>
|
<
|
<
>
|
<
|
<
>
|
>
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
|

|
|
|
<
|
|
|
|
|
|
|
<
<
<
<
<
<
|
<
<
<

<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
|

>
|
1
2
3
4
5
6
7
8
9
10
11
12
13

14


15
16
17

18
19
20
21
22



23

24


25
26
27
28











29
30
31













32




33
34

35



36
37
38

39


40





41
42
43

44
45
46
47



48
49






50
51
52
53
54



55




56


57
58
59
60

61
62
63
64
65
66
67
68

69

70
71

72

73
74
75
76















77

78
79
80
81
82

83
84
85
86
87
88
89






90



91




92











93
94
95
96
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ECRITURE);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');

}



$chart = $current_year->chart();
$accounts = $chart->accounts();


$transaction = new Transaction;
$transaction->type = -1;
$lines = [[], []];
$amount = 0;



$payoff_for = qg('payoff_for') ?: f('payoff_for');




// Quick pay-off for debts and credits, directly from a debt/credit details page
if ($id = $payoff_for) {
	$payoff_for = $transaction->payOffFrom($id);












	if (!$payoff_for) {
		throw new UserException('Écriture inconnue');
	}


















	$amount = $payoff_for->sum;
}





// Quick transaction from an account journal page
if ($id = qg('account')) {
	$account = $accounts::get($id);




	if (!$account || $account->id_chart != $current_year->id_chart) {





		throw new UserException('Ce compte ne correspond pas à l\'exercice comptable ou n\'existe pas');
	}


	$transaction->type = Transaction::getTypeFromAccountType($account->type);
	$key = sprintf('account_%d_%d', $transaction->type, 0);

	if (!isset($_POST[$key])) {



		$_POST[$key] = [$account->id => sprintf('%s — %s', $account->code, $account->label)];
	}






}
elseif (!empty($_POST['lines']) && is_array($_POST['lines'])) {
	$lines = Utils::array_transpose($_POST['lines']);

	foreach ($lines as &$line) {



		$line['credit'] = Utils::moneyToInteger($line['credit']);




		$line['debit'] = Utils::moneyToInteger($line['debit']);


	}
}

if (f('save') && $form->check('acc_transaction_new')) {

	try {
		$transaction->id_year = $current_year->id();
		$transaction->importFromNewForm();
		$transaction->id_creator = $session->getUser()->id;
		$transaction->save();

		// Append file
		if (!empty($_FILES['file']['name'])) {

			$file = Fichiers::upload($_FILES['file']);

			$file->linkTo(Fichiers::LIEN_COMPTA, $transaction->id());
		}



		 // Link members
		if (null !== f('users') && is_array(f('users'))) {
			$transaction->updateLinkedUsers(array_keys(f('users')));
		}

















		$session->set('acc_last_date', f('date'));

		Utils::redirect(Utils::getSelfURL(false) . '?ok=' . $transaction->id());
	}
	catch (UserException $e) {

		$form->addError($e->getMessage());
	}
}

$tpl->assign('date', $session->get('acc_last_date') ?: $current_year->start_date->format('d/m/Y'));
$tpl->assign(compact('transaction', 'payoff_for', 'amount', 'lines'));
$tpl->assign('payoff_targets', implode(':', [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]));






$tpl->assign('ok', (int) qg('ok'));








$tpl->assign('types_details', Transaction::getTypesDetails());











$tpl->assign('chart_id', $chart->id());

$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->display('acc/transactions/new.tpl');

Added src/www/admin/acc/transactions/service_user.php version [0473be53b7].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace Garradin;

use Garradin\Accounting\Reports;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$criterias = ['service_user' => (int)qg('id')];

$tpl->assign('balance', Reports::getClosingSumsWithAccounts($criterias));
$tpl->assign('journal', Reports::getJournal($criterias));
$tpl->assign('user_id', qg('user'));

$tpl->display('acc/transactions/service_user.tpl');

Added src/www/admin/acc/transactions/user.php version [698de7ba96].

























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace Garradin;

use Garradin\Accounting\Reports;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$u = (new Membres)->get((int)qg('id'));

if (!$u) {
	throw new UserException('Ce membre n\'existe pas');
}

$years = Years::listAssoc();
end($years);
$year = (int)qg('year') ?: key($years);

$criterias = ['user' => $u->id];

$tpl->assign('balance', Reports::getClosingSumsWithAccounts($criterias + ['year' => $year]));
$tpl->assign('journal', Reports::getJournal($criterias));
$tpl->assign(compact('years', 'year'));
$tpl->assign('transaction_user', $u);

$tpl->display('acc/transactions/user.tpl');

Added src/www/admin/acc/years/balance.php version [1c5fd61cf1].























































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Accounting\Reports;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$year = Years::get((int)qg('id'));

if (!$year) {
	throw new UserException('Exercice inconnu.');
}

if ($year->closed) {
	throw new UserException('Impossible de modifier un exercice clôturé.');
}

if (f('next') && !f('from_year')) {
	Utils::redirect('/admin/acc/years/');
}

if (f('save') && $form->check('acc_years_balance_' . $year->id()))
{
	try {
		$transaction = new Transaction;
		$transaction->importFromBalanceForm($year);
		$transaction->save();

		Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id());
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$previous_year = null;
$chart_change = false;
$lines = [[]];
$lines_accounts = [[]];
$years = Years::listClosed();

if (!count($years)) {
	$previous_year = 0;
}
elseif (null !== f('from_year')) {
	$previous_year = (int)f('from_year');
	$previous_year = Years::get($previous_year);

	if (!$previous_year) {
		throw new UserException('Année précédente invalide');
	}
}


if ($previous_year) {
	$lines = Reports::getClosingSumsWithAccounts(['year' => $previous_year->id(), 'exclude_position' => [Account::EXPENSE, Account::REVENUE]]);

	if ($previous_year->id_chart != $year->id_chart) {
		$chart_change = true;
		$codes = [];

		foreach ($lines as $line) {
			$codes[] = $line->code;
		}

		$matching_accounts = $year->accounts()->listForCodes($codes);
	}

	// Append result
	$result = Reports::getResult(['year' => $previous_year->id()]);

	if ($result > 0) {
		$account = $year->accounts()->getSingleAccountForType(Account::TYPE_POSITIVE_RESULT);
	}
	else {
		$account = $year->accounts()->getSingleAccountForType(Account::TYPE_NEGATIVE_RESULT);
	}

	if (!$account) {
		$account = (object) [
			'id' => null,
			'code' => null,
			'label' => 'Résultat de l\'exercice',
		];
	}

	$lines[] = (object) [
		'sum' => $result,
		'id' => $account->id,
		'code' => $account->code,
		'label' => $account->label,
	];

	foreach ($lines as $k => &$line) {
		$line->credit = $line->sum > 0 ? $line->sum : 0;
		$line->debit = $line->sum < 0 ? abs($line->sum) : 0;

		if ($chart_change) {
			if (array_key_exists($line->code, $matching_accounts)) {
				$acc = $matching_accounts[$line->code];
				$line->account_selected = [$acc->id => sprintf('%s — %s', $acc->code, $acc->label)];
			}
			else {
				$line->account_selected = null;
			}
		}
		else {
			$line->account_selected = [$line->id => sprintf('%s — %s', $line->code, $line->label)];
		}
	}

	unset($line);
}

$tpl->assign(compact('lines', 'years', 'chart_change', 'previous_year', 'year'));

$tpl->display('acc/years/balance.tpl');

Modified src/www/admin/acc/years/close.php from [27ad552537] to [c2cd93158e].

1
2


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


18
19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Garradin;



require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$e = new Compta\Exercices;

$exercice = $e->get((int)qg('id'));

if (!$exercice)
{
	throw new UserException('Exercice inconnu.');
}

if (f('close'))


{
    $form->check('compta_cloturer_exercice_' . $exercice->id, [

        'fin'     => 'date|required',
        'reports' => 'boolean',
    ]);

    if (!$form->hasErrors())
    {
        try
        {
            $id = $e->close($exercice->id, f('fin'));
        
            if ($id && f('reports'))
            {
                $e->doReports($exercice->id, Utils::modifyDate(f('fin'), '+1 day'));
            }

            Utils::redirect(ADMIN_URL . 'compta/exercices/');
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('exercice', $exercice);

$tpl->display('admin/compta/exercices/cloturer.tpl');


>
>





<
<
|

|
<



|
>
>
|
<
>
|
<
|

|
|
|
<
|
|
|
|
<
<
<
|
|
|
|
|
|
|
|
<
|

|
1
2
3
4
5
6
7
8
9


10
11
12

13
14
15
16
17
18
19

20
21

22
23
24
25
26

27
28
29
30



31
32
33
34
35
36
37
38

39
40
41
<?php
namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);



$year = Years::get((int)qg('id'));

if (!$year) {

	throw new UserException('Exercice inconnu.');
}

if ($year->closed) {
	throw new UserException('Impossible de modifier un exercice clôturé.');
}


$rules = [
	'end_date' => 'date_format:d/m/Y|required',

];

if (f('close') && $form->check('acc_years_close_' . $year->id()))
{
	try {

		$year->close($user->id);
		$year->save();
		$session->set('acc_year', null);




		Utils::redirect(ADMIN_URL . 'acc/years/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}


$tpl->assign('year', $year);

$tpl->display('acc/years/close.tpl');

Modified src/www/admin/acc/years/delete.php from [47c6a4fb3d] to [1f309cbc2f].

1
2


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
namespace Garradin;



require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$e = new Compta\Exercices;

$exercice = $e->get((int)qg('id'), true);

if (!$exercice)
{
	throw new UserException('Exercice inconnu.');
}

if ($exercice->cloture && $exercice->nb_operations > 0)
{
    throw new UserException('Impossible de supprimer un exercice clôturé.');
}

if (f('delete'))
{
    if ($form->check('compta_supprimer_exercice_'.$exercice->id))
    {
        try
        {
            $id = $e->delete($exercice->id);

            Utils::redirect(ADMIN_URL . 'compta/exercices/');
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('exercice', $exercice);

$tpl->display('admin/compta/exercices/supprimer.tpl');


>
>





<
<
|

|
<



|
<
|


|
<
<
<
<
<
|
|
<
|
<
<
<
<
<
<
|
|

|
1
2
3
4
5
6
7
8
9


10
11
12

13
14
15
16

17
18
19
20





21
22

23






24
25
26
27
<?php
namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);



$year = Years::get((int)qg('id'));

if (!$year) {

	throw new UserException('Exercice inconnu.');
}

if ($year->closed) {

	throw new UserException('Impossible de supprimer un exercice clôturé.');
}

$form->runIf(f('delete') && f('confirm_delete'), function () use ($year) {





	$year->delete();
}, 'acc_years_delete_' . $year->id(), ADMIN_URL . 'acc/years/');








$tpl->assign('nb_transactions', $year->countTransactions());
$tpl->assign('year', $year);

$tpl->display('acc/years/delete.tpl');

Modified src/www/admin/acc/years/edit.php from [566d206828] to [43b8da34f3].

1
2


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
namespace Garradin;



require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$e = new Compta\Exercices;

$exercice = $e->get((int)qg('id'));

if (!$exercice)
{
	throw new UserException('Exercice inconnu.');
}

if ($exercice->cloture)
{
    throw new UserException('Impossible de modifier un exercice clôturé.');
}

if ($form('edit'))
{
    $form->check('compta_modif_exercice_' . $exercice->id, [
        'libelle' => 'required',
        'fin'     => 'required|date',
        'debut'   => 'required|date',
    ]);

    if (!$form->hasErrors())
    {
        try
        {
            $id = $e->edit($exercice->id, [
                'libelle'   =>  f('libelle'),
                'debut'     =>  f('debut'),
                'fin'       =>  f('fin'),
            ]);

            Utils::redirect(ADMIN_URL . 'compta/exercices/');
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('exercice', $exercice);

$tpl->display('admin/compta/exercices/modifier.tpl');


>
>





<
<
|

|
<



|
<
|


<
<
<
<
<
<
<
|
<
|
|
<
|
<
<
<
|

|
|
|
|
|
|
|
|
<
|

|
1
2
3
4
5
6
7
8
9


10
11
12

13
14
15
16

17
18
19







20

21
22

23



24
25
26
27
28
29
30
31
32
33

34
35
36
<?php
namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);



$year = Years::get((int)qg('id'));

if (!$year) {

	throw new UserException('Exercice inconnu.');
}

if ($year->closed) {

	throw new UserException('Impossible de modifier un exercice clôturé.');
}








if (f('edit') && $form->check('acc_years_edit_' . $year->id()))

{
	try {

		$year->importForm();



		$year->save();

		Utils::redirect(ADMIN_URL . 'acc/years/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}


$tpl->assign('year', $year);

$tpl->display('acc/years/edit.tpl');

Added src/www/admin/acc/years/import.php version [3baf406550].

















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$year_id = (int) qg('id') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);

	if (!$year) {
		throw new UserException("L'exercice demandé n'existe pas.");
	}
}

if (qg('export')) {
	CSV::export(
		qg('export'),
		sprintf('Export comptable - %s - %s', Config::getInstance()->get('nom_asso'), $year->label),
		Transactions::export($year->id())
	);
	exit;
}

if ($year->closed) {
	throw new UserException('Impossible de modifier un exercice clôturé.');
}

$csv = new CSV_Custom($session, 'acc_import_year');
$csv->setColumns(Transactions::POSSIBLE_CSV_COLUMNS);
$csv->setMandatoryColumns(Transactions::MANDATORY_CSV_COLUMNS);

if (f('cancel')) {
	$csv->clear();
	Utils::redirect(Utils::getSelfURL());
}

$csrf_key = 'acc_years_import_' . $year->id();

$form->runIf(f('assign') && $csv->loaded(), function () use ($csv, $year, $user) {
	$csv->skip(f('skip_first_line'));
	$csv->setTranslationTable(f('translation_table'));

	Transactions::importCustom($year, $csv, $user->id);
	$csv->clear();
}, $csrf_key, ADMIN_URL . 'acc/years/');

$form->runIf('load', function () use ($csv, $year, $user) {
	if (f('type') == 'garradin') {
		Transactions::importCSV($year, $_FILES['file'], $user->id);
		Utils::redirect(ADMIN_URL . 'acc/years/');
	}
	elseif (isset($_FILES['file']['tmp_name'])) {
		$csv->load($_FILES['file']);
		Utils::redirect(Utils::getSelfURI());
	}
	else {
		throw new UserException('Fichier invalide');
	}
}, $csrf_key, Utils::getSelfURI());

$tpl->assign(compact('csv', 'year', 'csrf_key'));

$tpl->display('acc/years/import.tpl');

Modified src/www/admin/acc/years/index.php from [631399af3f] to [7a17ddf96c].

1
2
3


4
5

6

7
8
9
10
11
<?php
namespace Garradin;



require_once __DIR__ . '/../_inc.php';


$e = new Compta\Exercices;


$tpl->assign('liste', $e->getList());
$tpl->assign('current_exercice', $e->getCurrent());

$tpl->display('admin/compta/exercices/index.tpl');



>
>
|

>
|
>

|
<

|
1
2
3
4
5
6
7
8
9
10
11
12

13
14
<?php
namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$years = new Years;

$tpl->assign('list', $years->list(true));


$tpl->display('acc/years/index.tpl');

Modified src/www/admin/acc/years/new.php from [6ab96d643e] to [52e738c176].

1
2
3




4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20



21
22
23
24
25
26

27

28
29

30
31
32
33
34
35




36
37
<?php
namespace Garradin;





require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$e = new Compta\Exercices;

if (f('add'))
{
    $form->check('compta_ajout_exercice', [
        'libelle' => 'required',
        'fin'     => 'required|date',
        'debut'   => 'required|date',
    ]);

    if (!$form->hasErrors())
    {
        try



        {
            $id = $e->add([
                'libelle' =>  f('libelle'),
                'debut'   =>  f('debut'),
                'fin'     =>  f('fin'),
            ]);



            Utils::redirect(ADMIN_URL . 'compta/exercices/');
        }

        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}





$tpl->display('admin/compta/exercices/ajouter.tpl');



>
>
>
>
|



<
|
<
<
<
<
<
<
<
<
<
<
|
>
>
>
|
|
<
<
<
<
>
|
>
|
|
>
|
|
|
|
|
|
>
>
>
>

|
1
2
3
4
5
6
7
8
9
10
11

12










13
14
15
16
17
18




19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace Garradin;

use Garradin\Accounting\Years;
use Garradin\Accounting\Charts;
use Garradin\Entities\Accounting\Year;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);


if (f('new') && $form->check('acc_years_new')) {










	try {
		$year = new Year;
		$year->importForm();
		$year->save();

		if (Years::countClosed()) {




			Utils::redirect(ADMIN_URL . 'acc/years/balance.php?id=' . $year->id());
		}
		else {
			Utils::redirect(ADMIN_URL . 'acc/years/');
		}
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$new_dates = Years::getNewYearDates();
$tpl->assign('start_date', $new_dates[0]);
$tpl->assign('end_date', $new_dates[1]);
$tpl->assign('charts', Charts::listByCountry());

$tpl->display('acc/years/new.tpl');

Added src/www/admin/acc/years/select.php version [5863923d36].

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../../_inc.php';

$years = new Years;

if (f('change')) {
	$year = Years::get(f('year'));

	if (!$year) {
		throw new UserException('Exercice inconnu');
	}

	$session->set('acc_year', $year->id());
	Utils::redirect(f('from') ?: ADMIN_URL . 'acc/years/');
}

$tpl->assign('list', $years->listOpen());
$tpl->assign('from', qg('from'));

$tpl->display('acc/years/select.tpl');

Added src/www/admin/common/delete_file.php version [f9d6581182].























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace Garradin;

use Garradin\Services\Services;

if (!defined('Garradin\ROOT')) {
	die('Access denied.');
}

if (!isset($csrf_key, $redirect)) {
	throw new \InvalidArgumentException('Missing params');
}

$file = new Fichiers(qg('id'));

if (!$file->checkAccess($session))
{
    throw new UserException('Vous n\'avez pas accès à ce fichier.');
}

$form->runIf('delete', function () use ($file) {
	$file->remove();
}, $csrf_key, $redirect);

$tpl->assign(compact('file', 'csrf_key'));

$tpl->display('common/delete_file.tpl');

Modified src/www/admin/common/saved_searches.php from [7efc82999d] to [e1f8499fe1].

1
2
3
4




5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21





22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

55

<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';





$recherche = new Recherche;
$mode = null;

if (qg('edit') || qg('delete'))
{
	$r = $recherche->get(qg('edit') ?: qg('delete'));

	if (!$r)
	{
		throw new UserException('Recherche non trouvée');
	}

	if ($r->id_membre !== null && $r->id_membre != $user->id)
	{
		throw new UserException('Recherche privée appartenant à un autre membre.');
	}






	$tpl->assign('recherche', $r);

	$mode = qg('edit') ? 'edit' : 'delete';
}

if ($mode == 'edit' && f('save') && $form->check('edit_recherche_' . $r->id))
{
	try {
		$recherche->edit($r->id, [
			'intitule'  => f('intitule'),
			'id_membre' => f('prive') ? $user->id : null,
		]);

		Utils::redirect('/admin/membres/recherches.php');
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}
elseif ($mode == 'delete' && f('delete') && $form->check('del_recherche_' . $r->id))
{
	$recherche->remove($r->id);
	Utils::redirect('/admin/membres/recherches.php');
}

$tpl->assign('mode', $mode);

if (!$mode)
{
	$tpl->assign('liste', $recherche->getList($user->id, 'membres'));
}


$tpl->display('admin/membres/recherches.tpl');




|
>
>
>
>




|

|










>
>
>
>
>














|








|


<
<


|


>
|
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56


57
58
59
60
61
62
63
64
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

if (empty($target) || !in_array($target, Recherche::TARGETS)) {
    throw new UserException('Cible inconnue');
}

$recherche = new Recherche;
$mode = null;

if (qg('edit') || qg('delete') || qg('duplicate'))
{
	$r = $recherche->get(qg('edit') ?: (qg('delete') ?: qg('duplicate')));

	if (!$r)
	{
		throw new UserException('Recherche non trouvée');
	}

	if ($r->id_membre !== null && $r->id_membre != $user->id)
	{
		throw new UserException('Recherche privée appartenant à un autre membre.');
	}

	if (qg('duplicate')) {
		$recherche->duplicate($r->id);
		Utils::redirect(Utils::getSelfURI(false));
	}

	$tpl->assign('recherche', $r);

	$mode = qg('edit') ? 'edit' : 'delete';
}

if ($mode == 'edit' && f('save') && $form->check('edit_recherche_' . $r->id))
{
	try {
		$recherche->edit($r->id, [
			'intitule'  => f('intitule'),
			'id_membre' => f('prive') ? $user->id : null,
		]);

		Utils::redirect(Utils::getSelfURI(false));
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}
elseif ($mode == 'delete' && f('delete') && $form->check('del_recherche_' . $r->id))
{
	$recherche->remove($r->id);
	Utils::redirect(Utils::getSelfURI(false));
}



if (!$mode)
{
	$tpl->assign('liste', $recherche->getList($user->id, $target));
}

$tpl->assign(compact('mode', 'target', 'search_url'));

$tpl->display('common/search/saved_searches.tpl');

Added src/www/admin/common/search.php version [19bc74c76e].





















































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?php
namespace Garradin;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

if (empty($target) || !in_array($target, Recherche::TARGETS)) {
	throw new UserException('Cible inconnue');
}

$recherche = new Recherche;

$query = (object) [
	'query' => f('q') ? json_decode(f('q'), true) : null,
	'order' => f('order'),
	'limit' => f('limit') ?: 100,
	'desc'  => (bool) f('desc'),
];

$text_query = trim(qg('qt'));
$result = null;
$sql_query = null;
$search = null;
$id = f('id') ?: qg('id');

// Recherche simple
if ($text_query !== '' && $target === 'membres' && empty($query->query))
{
	$query = $recherche->buildSimpleMemberQuery($text_query);
}
// Recherche existante
elseif ($id && empty($query->query))
{
	$search = $recherche->get($id);

	if (!$search) {
		throw new UserException('Recherche inconnue ou invalide');
	}

	if ($search->type === Recherche::TYPE_SQL) {
		$sql_query = $search->contenu;
	}
	else {
		$query = $search->query;
		$query->limit = (int) f('limit') ?: $query->limit;
	}
}

// Recherche SQL
if (f('sql_query')) {
	// Only admins can run custom queries, others can only run saved queries
	$session->requireAccess($target, Membres::DROIT_ADMIN);
	$sql_query = f('sql_query');
}

// Execute search
if ($query->query || $sql_query) {
	try {
		if ($sql_query) {
			$sql = $sql_query;
		}
		else {
			$sql = $recherche->buildQuery($target, $query->query, $query->order, $query->desc, $query->limit);
		}

	   $result = $recherche->searchSQL($target, $sql);
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}

	if (f('to_sql')) {
		$sql_query = $sql;
	}
}

if (null !== $result)
{
	if (count($result) == 1 && $text_query !== '' && $target === 'membres') {
		Utils::redirect(ADMIN_URL . 'membres/fiche.php?id=' . (int)$result[0]->id);
	}

	if (f('save') && !$form->hasErrors())
	{
		$type = $sql_query ? Recherche::TYPE_SQL : Recherche::TYPE_JSON;

		if ($id) {
			$recherche->edit($id, [
				'type'    => $type,
				'contenu' => $sql_query ?: $query,
			]);
		}
		else
		{
			$label = $sql_query ? 'Recherche SQL du ' : 'Recherche avancée du ';
			$label .= date('d/m/Y à H:i:s');
			$id = $recherche->add($label, $user->id, $type, $target, $sql_query ?: $query);
		}

		$url = $target == 'compta' ? '/admin/acc/saved_searches.php?id=' : '/admin/membres/recherches.php?id=';
		Utils::redirect($url . $id);
	}

	$tpl->assign('result_header', $recherche->getResultHeader($target, $result));
}
elseif ($target === 'membres')
{
	$query->query = [[
		'operator' => 'AND',
		'conditions' => [
			[
				'column'   => $config->get('champ_identite'),
				'operator' => '= ?',
				'values'   => [''],
			],
		],
	]];
	$result = null;
}
elseif ($target === 'compta')
{
	$years = Years::list();
	$query->query = [[
		'operator' => 'AND',
		'conditions' => [
			[
				'column'   => 't.id_year',
				'operator' => '= ?',
				'values'   => [qg('year')],
			],
			[
				'column'   => 't.reference',
				'operator' => 'LIKE %?%',
				'values'   => '',
			],
		],
	]];
	$query->desc = true;
	$result = null;
}

$columns = $recherche->getColumns($target);
$is_admin = $session->canAccess($target, Membres::DROIT_ADMIN);
$schema = $recherche->schema($target);

$tpl->assign(compact('query', 'sql_query', 'result', 'columns', 'is_admin', 'schema', 'search'));

if ($target == 'compta') {
	$tpl->display('acc/search.tpl');
}
else {
	$tpl->display('admin/membres/recherche.tpl');
}

Deleted src/www/admin/compta/_inc.php version [da7b17d045].

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ACCES);

$comptes = new Compta\Comptes;

$tpl->assign('id_caisse', Compta\Comptes::CAISSE);
$tpl->assign('id_cheque_a_encaisser', Compta\Comptes::CHEQUE_A_ENCAISSER);
$tpl->assign('id_carte_a_encaisser', Compta\Comptes::CARTE_A_ENCAISSER);
<
<
<
<
<
<
<
<
<
<
<
<
<


























Deleted src/www/admin/compta/banques/index.php version [9e1fc41443].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$banques = new Compta\Comptes_Bancaires;
$journal = new Compta\Journal;

if (f('add') && $form->check('compta_ajout_banque'))
{
	$session->requireAccess('compta', Membres::DROIT_ADMIN);

    try
    {
        $id = $banques->add([
            'libelle' => f('libelle'),
            'banque'  => f('banque'),
            'iban'    => f('iban'),
            'bic'     => f('bic'),
        ]);

        if (f('solde') > 0)
        {
        	$exercices = new Compta\Exercices;
        	$exercice = $exercices->getCurrent();
        	$solde = f('solde');

        	$journal->add([
                'libelle'       =>  'Solde initial',
                'montant'       =>  abs($solde),
                'date'          =>  gmdate('Y-m-d', $exercice->debut),
                'compte_credit' =>  $solde > 0 ? '110' : $id,
                'compte_debit'  =>  $solde < 0 ? '119' : $id,
                'numero_piece'  =>  null,
                'remarques'     =>  'Opération automatique à l\'ajout du compte dans la liste des comptes bancaires',
                'id_auteur'     =>  $user->id,
            ]);
        }

        Utils::redirect(ADMIN_URL . 'compta/banques/');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$liste = $banques->getList();

foreach ($liste as &$banque)
{
    $banque->solde = $journal->getSolde($banque->id);
}

$tpl->assign('liste', $liste);

$tpl->register_modifier('format_iban', function ($iban) {
    return implode(' ', str_split($iban, 4));
});

$tpl->display('admin/compta/banques/index.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


























































































































Deleted src/www/admin/compta/banques/modifier.php version [580b266350].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$banque = new Compta\Comptes_Bancaires;

$compte = $banque->get(qg('id'));

if (!$compte)
{
    throw new UserException('Le compte demandé n\'existe pas.');
}

if (f('save') && $form->check('compta_edit_banque_' . $compte->id))
{
    try
    {
        $id = $banque->edit($compte->id, [
            'libelle' => f('libelle'),
            'banque'  => f('banque'),
            'iban'    => f('iban'),
            'bic'     =>  f('bic'),
        ]);

        Utils::redirect(ADMIN_URL . 'compta/banques/');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('compte', $compte);

$tpl->display('admin/compta/banques/modifier.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































Deleted src/www/admin/compta/banques/supprimer.php version [a80679a404].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$banque = new Compta\Comptes_Bancaires;

$compte = $banque->get(qg('id'));

if (!$compte)
{
	throw new UserException('Le compte demandé n\'existe pas.');
}

if (f('delete') && $form->check('compta_delete_banque_' . $compte->id))
{
	try
	{
		$banque->delete($compte->id);
		Utils::redirect(ADMIN_URL . 'compta/banques/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}

$tpl->assign('compte', $compte);

$tpl->display('admin/compta/banques/supprimer.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































Deleted src/www/admin/compta/categories/ajouter.php version [ccc6c6987b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace Garradin;

use Garradin\Compta\Categories;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$cats = new Categories;

if (f('add'))
{
    $form->check('compta_ajout_cat', [
        'intitule' => 'required|string',
        'compte'   => 'required|in_table:compta_comptes,id',
        'type'     => 'required|in:' . implode(',', [Categories::DEPENSES, Categories::RECETTES, Categories::AUTRES]),
    ]);

    if (!$form->hasErrors())
    {
        try
        {
            $id = $cats->add([
                'intitule'      =>  f('intitule'),
                'description'   =>  f('description'),
                'compte'        =>  f('compte'),
                'type'          =>  f('type'),
            ]);

            if (f('type') == Categories::DEPENSES)
                $type = 'depenses';
            elseif (f('type') == Categories::AUTRES)
                $type = 'autres';
            else
                $type = 'recettes';

            Utils::redirect(ADMIN_URL . 'compta/categories/?'.$type);
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('type', f('type') !== null ? f('type') : Categories::RECETTES);
$tpl->assign('comptes', $comptes->listTree());
$tpl->assign('categories', $cats);

$tpl->display('admin/compta/categories/ajouter.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































Deleted src/www/admin/compta/categories/index.php version [e2ecaf3c11].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$cats = new Compta\Categories;

if (null !== qg('depenses'))
    $type = Compta\Categories::DEPENSES;
else
    $type = Compta\Categories::RECETTES;

$tpl->assign('current_nav', $type == Compta\Categories::DEPENSES ? 'depenses' : 'recettes');
$tpl->assign('type', $type);
$tpl->assign('liste', $cats->getList($type));

$tpl->display('admin/compta/categories/index.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































Deleted src/www/admin/compta/categories/modifier.php version [12706d60d1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php

namespace Garradin;

use Garradin\Compta\Categories;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$cats = new Categories;

$id = (int)qg('id');
$cat = $cats->get($id);

if (!$cat)
{
    throw new UserException('Cette catégorie n\'existe pas.');
}

if (f('save'))
{
    $form->check('compta_edit_cat_' . $cat->id, [
        'intitule' => 'required|string',
    ]);

    if (!$form->hasErrors())
    {
        try
        {
            $id = $cats->edit($id, [
                'intitule'      =>  f('intitule'),
                'description'   =>  f('description'),
            ]);

            if ($cat->type == Compta\Categories::DEPENSES)
                $type = 'depenses';
            elseif ($cat->type == Compta\Categories::AUTRES)
                $type = 'autres';
            else
                $type = 'recettes';

            Utils::redirect(ADMIN_URL . 'compta/categories/?'.$type);
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('cat', $cat);

$tpl->display('admin/compta/categories/modifier.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































Deleted src/www/admin/compta/categories/supprimer.php version [db80f3760b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace Garradin;

use Garradin\Compta\Categories;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$cats = new Compta\Categories;

$id = (int)qg('id');
$cat = $cats->get($id);

if (!$cat)
{
    throw new UserException('Cette catégorie n\'existe pas.');
}

if (f('delete') && $form->check('delete_compta_cat_' . $cat->id))
{
    try
    {
        $cats->delete($id);
        Utils::redirect(ADMIN_URL . 'compta/categories/');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('cat', $cat);

$tpl->display('admin/compta/categories/supprimer.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































Deleted src/www/admin/compta/comptes/ajouter.php version [7be03dc1f5].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$classe = (int) qg('classe');

if (!$classe || $classe < 1 || $classe > 9)
{
    throw new UserException("Cette classe de compte n'existe pas.");
}

if (f('add'))
{
    if ($form->check('compta_ajout_compte'))
    {
        try
        {
            $id = $comptes->add([
                'id'       =>  f('numero'),
                'libelle'  =>  f('libelle'),
                'parent'   =>  f('parent'),
                'position' =>  f('position'),
            ]);

            Utils::redirect(ADMIN_URL . 'compta/comptes/?classe='.$classe);
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$parent = $comptes->get(f('parent') ?: $classe);

$tpl->assign('positions', $comptes->getPositions());
$tpl->assign('position', f('position') ?: $parent->position);
$tpl->assign('comptes', $comptes->listTree($classe));

$tpl->display('admin/compta/comptes/ajouter.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































Deleted src/www/admin/compta/comptes/index.php version [7140f6c1ad].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

if (qg('export') == 'plan')
{
	$comptes->exportPlan();
	exit;
}

$tpl->assign('confirm', qg('confirm'));

if (f('import') && $form->check('plan_import', ['upload' => 'file|required', 'format' => 'required|in:json']))
{
	try {
		$comptes->importPlan($_FILES['upload']['tmp_name'], true);
		Utils::redirect(ADMIN_URL . 'compta/comptes/?import&confirm=import');
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}
elseif (f('reset') && $form->check('plan_reset'))
{
	try {
		$comptes->importPlan(null, true);
		Utils::redirect(ADMIN_URL . 'compta/comptes/?import&confirm=reset');
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$classe = (int) qg('classe');

$tpl->assign('classe', $classe);

if (!$classe)
{
	$tpl->assign('classes', $comptes->listTree(0, false));
}
else
{
	$positions = $comptes->getPositions();

	$tpl->assign('classe_compte', $comptes->get($classe));
	$tpl->assign('liste', $comptes->listTree($classe));
}

function tpl_get_position($pos)
{
	global $positions;
	return $positions[$pos];
}

$tpl->register_modifier('get_position', 'Garradin\tpl_get_position');

$template = 'index';

if ($classe) {
	$template = 'classe';
}
elseif (qg('import') !== null) {
	$template = 'import';
	$tpl->assign('confirm', qg('confirm'));
}

$tpl->display(sprintf('admin/compta/comptes/%s.tpl', $template));
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































Deleted src/www/admin/compta/comptes/modifier.php version [61a3177e1f].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$id = qg('id');
$compte = $comptes->get($id);

if (!$compte)
{
    throw new UserException('Le compte demandé n\'existe pas.');
}

if (f('save'))
{
    if ($form->check('compta_edit_compte_' . $compte->id))
    {
        try
        {
            $id = $comptes->edit($compte->id, [
                'libelle'  =>  f('libelle'),
                'position' =>  f('position'),
            ]);

            Utils::redirect(ADMIN_URL . 'compta/comptes/?classe='.substr($compte->id, 0, 1));
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('positions', $comptes->getPositions());
$tpl->assign('position', f('position') ?: $compte->position);
$tpl->assign('compte', $compte);

$tpl->display('admin/compta/comptes/modifier.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































Deleted src/www/admin/compta/comptes/supprimer.php version [6d0ff3b7ef].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$id = qg('id');
$compte = $comptes->get($id);

if (!$compte)
{
    throw new UserException('Le compte demandé n\'existe pas.');
}

if (f('delete') && $form->check('compta_delete_compte_' . $compte->id))
{
    try
    {
        $comptes->delete($compte->id);
        Utils::redirect(ADMIN_URL . 'compta/comptes/?classe=' . substr($compte->id, 0, 1));
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}
elseif (f('disable') && $form->check('compta_disable_compte_' . $compte->id))
{
    try
    {
        $comptes->disable($compte->id);
        Utils::redirect(ADMIN_URL . 'compta/comptes/?classe='.substr($compte->id, 0, 1));
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('can_delete', $comptes->canDelete($compte->id));
$tpl->assign('can_disable', $comptes->canDisable($compte->id));

$tpl->assign('compte', $compte);

$tpl->display('admin/compta/comptes/supprimer.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































Deleted src/www/admin/compta/graph.php version [1c8c6790d6].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

qv(['g' => 'required|in:recettes_depenses,banques_caisses']);

$graph = qg('g');

if (Static_Cache::expired('graph_' . $graph))
{
	$stats = new Compta\Stats;

	$plot = new \KD2\SVGPlot(400, 300);

	if ($graph == 'recettes_depenses')
	{
		$r = new \KD2\SVGPlot_Data($stats->recettes());
		$r->title = 'Recettes';

		$d = new \KD2\SVGPlot_Data($stats->depenses());
		$d->title = 'Dépenses';

		$data = [$d, $r];

		$plot->setTitle('Recettes et dépenses de l\'exercice courant');
	}
	elseif ($graph == 'banques_caisses')
	{
		$banques = new Compta\Comptes_Bancaires;

		$data = [];

		$r = new \KD2\SVGPlot_Data($stats->soldeCompte(Compta\Comptes::CAISSE));
		$r->title = 'Caisse';

		$data[] = $r;

		foreach ($banques->getList() as $banque)
		{
			$r = new \KD2\SVGPlot_Data($stats->soldeCompte($banque->id));
			$r->title = $banque->libelle;
			$data[] = $r;
		}

		$plot->setTitle('Solde des comptes et caisses');
	}

	if (!empty($data))
	{
		$labels = [];

		foreach ($data[0]->get() as $k=>$v)
		{
			$labels[] = Utils::date_fr('M y', strtotime(substr($k, 0, 4) . '-' . substr($k, 4, 2) .'-01'));
		}

		$plot->setLabels($labels);

		$i = 0;
		$colors = ['#c71', '#941', '#fa4', '#fd9', '#ffc', '#cc9'];

		foreach ($data as $line)
		{
			$line->color = $colors[$i++];
			$line->width = 2;
			$plot->add($line);

			if ($i >= count($colors))
				$i = 0;
		}
	}

	Static_Cache::store('graph_' . $graph, $plot->output());
}

header('Content-Type: image/svg+xml');
Static_Cache::display('graph_' . $graph);
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































Deleted src/www/admin/compta/import.php version [2c5e1fbc3f].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$e = new Compta\Exercices;
$import = new Compta\Import;

if (qg('export') == 'csv')
{
    $import->toCSV($e->getCurrentId());
    exit;
}
elseif (qg('export') == 'ods')
{
    $import->toODS($e->getCurrentId());
    exit;
}

if (f('import'))
{
    $form->check('compta_import', [
        'upload' => 'file|required',
        'type'   => 'required|in:garradin',
    ]);

    if (!$form->hasErrors())
    {
        try
        {
            if (f('type') == 'garradin')
            {
                $import->fromCSV($_FILES['upload']['tmp_name']);
            }
            else
            {
                throw new UserException('Import inconnu.');
            }

            Utils::redirect(ADMIN_URL . 'compta/import.php?ok');
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('ok', qg('ok') !== null);

$tpl->display('admin/compta/import.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































































Deleted src/www/admin/compta/index.php version [bb0c467a1d].

1
2
3
4
5
6
7
8
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$journal = new Compta\Journal;

$tpl->display('admin/compta/index.tpl');
<
<
<
<
<
<
<
<
















Deleted src/www/admin/compta/operations/cotisation.php version [98e2b3f517].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$journal = new Compta\Journal;

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$m_cotisations = new Membres\Cotisations;
$cotisations = new Cotisations;

$mco = $m_cotisations->get($id);

if (!$mco)
{
	throw new UserException("La cotisation demandée n'existe pas.");
}

$co = $cotisations->get($mco->id_cotisation);
$membre = (new Membres)->get($mco->id_membre);

if (!$membre)
{
    throw new UserException("Le membre demandé n'existe pas.");
}

$liste_comptes = $comptes->getListAll();

function get_nom_compte($compte)
{
	if (is_null($compte))
		return '';

	global $liste_comptes;
	return $liste_comptes[$compte];
}

$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte');

$tpl->assign('journal', $m_cotisations->listOperationsCompta($mco->id));

$tpl->assign('cotisation_membre', $mco);
$tpl->assign('cotisation', $co);
$tpl->assign('membre', $membre);

$tpl->display('admin/compta/operations/cotisation.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































Deleted src/www/admin/compta/operations/index.php version [b00c4bb240].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$cats = new Compta\Categories;
$cat = $type = false;

if (qg('cat'))
{
	$cat = $cats->get(qg('cat'));

	if (!$cat)
	{
		throw new UserException("La catégorie demandée n'existe pas.");
	}

	$type = $cat->type;
}
else
{
	if (null !== qg('autres'))
		$type = Compta\Categories::AUTRES;
	elseif (null !== qg('depenses'))
		$type = Compta\Categories::DEPENSES;
	else
		$type = Compta\Categories::RECETTES;
}

$journal = new Compta\Journal;

$list = $journal->getListForCategory($type === Compta\Categories::AUTRES ? null : $type, $cat ? $cat->id : null);

$tpl->assign('categorie', $cat);
$tpl->assign('journal', $list);
$tpl->assign('type', $type);

if ($type !== Compta\Categories::AUTRES)
{
	$tpl->assign('liste_cats', $cats->getList($type));
}

$total = 0.0;

foreach ($list as $row)
{
	$total += (float) $row->montant;
}

$tpl->assign('total', $total);

$tpl->display('admin/compta/operations/index.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































Deleted src/www/admin/compta/operations/membre.php version [2f74dbc0da].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$exercices = new Compta\Exercices;
$journal = new Compta\Journal;

$exercice = qg('exercice') ?: $exercices->getCurrentId();

if (!$exercice)
{
	throw new UserException('Exercice inconnu.');
}

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$membre = (new Membres)->get($id);

if (!$membre)
{
    throw new UserException("Le membre demandé n'existe pas.");
}

$liste_comptes = $comptes->getListAll();

function get_nom_compte($compte)
{
	if (is_null($compte))
		return '';

	global $liste_comptes;
	return $liste_comptes[$compte];
}

$tpl->register_modifier('get_nom_compte', 'Garradin\get_nom_compte');

$tpl->assign('journal', $journal->listForMember($membre->id, $exercice));

$tpl->assign('exercices', $exercices->getList());
$tpl->assign('exercice', $exercice);
$tpl->assign('membre', $membre);

$tpl->display('admin/compta/operations/membre.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































Deleted src/www/admin/compta/operations/modifier.php version [6e0e877ba5].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$journal = new Compta\Journal;
$cats = new Compta\Categories;
$banques = new Compta\Comptes_Bancaires;

$operation = $journal->get(qg('id'));

if (!$operation)
{
    throw new UserException("L'opération demandée n'existe pas.");
}

if ($operation->id_categorie)
{
    $categorie = $cats->get($operation->id_categorie);
}
else
{
    $categorie = false;
}

if ($categorie && $categorie->type != Compta\Categories::AUTRES)
{
    $type = $categorie->type;
}
else
{
    $type = null;
}

if (f('save'))
{
    $form->check('compta_modifier_' . $operation->id, [
        'libelle' => 'required|string',
        'montant' => 'required|money',
        'date'    => 'required|date_format:Y-m-d',
    ]);

    if (!$form->hasErrors())
    {
        try
        {
            if (is_null($type))
            {
                $journal->edit($operation->id, [
                    'libelle'       =>  f('libelle'),
                    'montant'       =>  f('montant'),
                    'date'          =>  f('date'),
                    'compte_credit' =>  f('compte_credit'),
                    'compte_debit'  =>  f('compte_debit'),
                    'numero_piece'  =>  f('numero_piece'),
                    'remarques'     =>  f('remarques'),
                    'id_projet'     =>  f('id_projet'),
                ]);
            }
            else
            {
                $cat = $cats->get(f('id_categorie'));

                if (!$cat)
                {
                    throw new UserException('Il faut choisir une catégorie.');
                }

                if (!array_key_exists(f('moyen_paiement'), $cats->listMoyensPaiement()))
                {
                    throw new UserException('Moyen de paiement invalide.');
                }

                if (f('moyen_paiement') == 'ES')
                {
                    $a = Compta\Comptes::CAISSE;
                    $b = $cat->compte;
                }
                else
                {
                    if (!trim(f('banque')))
                    {
                        throw new UserException('Le compte bancaire choisi est invalide.');
                    }

                    if (!array_key_exists(f('banque'), $banques->getList())
                        && f('banque') != Compta\Comptes::CHEQUE_A_ENCAISSER
                        && f('banque') != Compta\Comptes::CARTE_A_ENCAISSER)
                    {
                        throw new UserException('Le compte bancaire choisi n\'existe pas.');
                    }

                    $a = f('banque');
                    $b = $cat->compte;
                }

                if ($type == Compta\Categories::DEPENSES)
                {
                    $debit = $b;
                    $credit = $a;
                }
                elseif ($type == Compta\Categories::RECETTES)
                {
                    $debit = $a;
                    $credit = $b;
                }

                $journal->edit($operation->id, [
                    'libelle'       =>  f('libelle'),
                    'montant'       =>  f('montant'),
                    'date'          =>  f('date'),
                    'moyen_paiement'=>  f('moyen_paiement'),
                    'numero_cheque' =>  f('numero_cheque'),
                    'compte_credit' =>  $credit,
                    'compte_debit'  =>  $debit,
                    'numero_piece'  =>  f('numero_piece'),
                    'remarques'     =>  f('remarques'),
                    'id_categorie'  =>  (int)$cat->id,
                    'id_projet'     =>  f('id_projet'),
                ]);
            }

            Utils::redirect(ADMIN_URL . 'compta/operations/voir.php?id='.(int)$operation->id);
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('type', $type);

if ($type === null)
{
    $tpl->assign('comptes', $comptes->listTree());
}
else
{
    $tpl->assign('moyens_paiement', $cats->listMoyensPaiement());
    $tpl->assign('categories', $cats->getList($type));
    $tpl->assign('comptes_bancaires', $banques->getList());
}

$tpl->assign('projets', (new Compta\Projets)->getAssocList());

$tpl->assign('operation', $operation);

$tpl->display('admin/compta/operations/modifier.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































































































































Deleted src/www/admin/compta/operations/recherche_sql.php version [bece8f5704].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ADMIN);

$journal = new Compta\Journal;

$query = trim(qg('query'));

$tpl->assign('schema', $journal->schemaSQL());
$tpl->assign('query', $query);

if ($query != '')
{
    try {
        $tpl->assign('result', $journal->searchSQL($query));
    }
    catch (\Exception $e)
    {
        $tpl->assign('result', null);
        $tpl->assign('error', $e->getMessage());
    }
}
else
{
    $tpl->assign('result', null);
}

$tpl->display('admin/compta/operations/recherche_sql.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































Deleted src/www/admin/compta/operations/voir.php version [c847e11f88].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$journal = new Compta\Journal;

$operation = $journal->get(qg('id'));

if (!$operation)
{
    throw new UserException("L'opération demandée n'existe pas.");
}
$exercices = new Compta\Exercices;

$tpl->assign('operation', $operation);

$credit = $comptes->get($operation->compte_credit);
$tpl->assign('nom_compte_credit', $credit ? $credit->libelle : null);

$debit = $comptes->get($operation->compte_debit);
$tpl->assign('nom_compte_debit', $debit ? $debit->libelle : null);

$tpl->assign('exercice', $exercices->get($operation->id_exercice));

if ($operation->id_categorie)
{
    $cats = new Compta\Categories;

    $categorie = $cats->get($operation->id_categorie);
    $tpl->assign('categorie', $categorie);

    if ($categorie->type == Compta\Categories::RECETTES)
    {
        $tpl->assign('compte', $debit ? $debit->libelle : null);
    }
    else
    {
        $tpl->assign('compte', $credit ? $credit->libelle : null);
    }

    $tpl->assign('moyen_paiement', $cats->getMoyenPaiement($operation->moyen_paiement));
}

if ($operation->id_projet)
{
    $tpl->assign('projet', (new Compta\Projets)->get($operation->id_projet));
}

if ($operation->id_auteur)
{
    $auteur = (new Membres)->get($operation->id_auteur);
    $tpl->assign('nom_auteur', $auteur->identite);
}

$tpl->assign('related_members', $journal->listRelatedMembers($operation->id));

$tpl->display('admin/compta/operations/voir.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































































































Deleted src/www/admin/compta/pie.php version [502f5f0839].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

qv(['g' => 'required|in:recettes,depenses']);

$graph = qg('g');

if (Static_Cache::expired('pie_' . $graph))
{
	$stats = new Compta\Stats;
	$categories = new Compta\Categories;

	$pie = new \KD2\SVGPie(400, 250);

	if ($graph == 'recettes')
	{
		$data = $stats->repartitionRecettes();
		$categories = $categories->getList(Compta\Categories::RECETTES);
		$pie->setTitle('Répartition des recettes');
	}
	else
	{
		$data = $stats->repartitionDepenses();
		$categories = $categories->getList(Compta\Categories::DEPENSES);
		$pie->setTitle('Répartition des dépenses');
	}

	$others = 0;
	$colors = ['#c71', '#941', '#fa4', '#fd9', '#ffc', '#cc9'];
	$max = count($colors);
	$i = 0;

	foreach ($data as $row)
	{
		if ($i++ >= $max)
		{
			$others += $row->somme;
		}
		else
		{
			$cat = $categories[$row->id_categorie];
			$pie->add(new \KD2\SVGPie_Data($row->somme, substr($cat->intitule, 0, 50), $colors[$i-1]));
		}
	}

	if ($others > 0)
	{
		$pie->add(new \KD2\SVGPie_Data($others, 'Autres', '#ccc'));
	}

	Static_Cache::store('pie_' . $graph, $pie->output());
}

header('Content-Type: image/svg+xml');
Static_Cache::display('pie_' . $graph);
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































Deleted src/www/admin/compta/projets/index.php version [439392048d].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php

namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$projets = new Compta\Projets;

$action = null;
$id = null;

if (qg('supprimer'))
{
	$session->requireAccess('compta', Membres::DROIT_ADMIN);

	$action = 'supprimer';
	$id = (int) qg('supprimer');
}
elseif (qg('modifier'))
{
	$session->requireAccess('compta', Membres::DROIT_ADMIN);

	$action = 'modifier';
	$id = (int) qg('modifier');
}

if ($id)
{
	if (!($projet = $projets->get($id)))
	{
		throw new UserException('Ce projet n\'existe pas.');
	}
	
	$tpl->assign('projet', $projet);
}


if (f('ajouter') && $form->check('ajout_projet'))
{
	$session->requireAccess('compta', Membres::DROIT_ADMIN);

	try {
		$projets->add(f('libelle'));
		Utils::redirect(ADMIN_URL . 'compta/projets/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}
elseif (f('modifier') && $form->check('modifier_projet_' . $id))
{
	try {
		$projets->edit($id, f('libelle'));
		Utils::redirect(ADMIN_URL . 'compta/projets/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}
elseif (f('supprimer') && $form->check('supprimer_projet_' . $id))
{
	try {
		$projets->remove($id);
		Utils::redirect(ADMIN_URL . 'compta/projets/');
	}
	catch (UserException $e)
	{
		$form->addError($e->getMessage());
	}
}


$tpl->assign('action', $action);
$tpl->assign('liste', $projets->getList());

$tpl->display('admin/compta/projets/index.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































Modified src/www/admin/config/categories/modifier.php from [6a75b928b9] to [79f85734c9].

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
        'droit_wiki'                => 'in:' . $droits,
        'droit_compta'              => 'in:' . $droits,
        'droit_membres'             => 'in:' . $droits,
        'droit_config'              => sprintf('in:%s,%s', Membres::DROIT_ADMIN, Membres::DROIT_AUCUN),
        'droit_connexion'           => sprintf('in:%s,%s', Membres::DROIT_ACCES, Membres::DROIT_AUCUN),
        'droit_inscription'         => sprintf('in:%s,%s', Membres::DROIT_ACCES, Membres::DROIT_AUCUN),
        'cacher'                    => 'boolean',
        'id_cotisation_obligatoire' => 'numeric',
    ]);

    if (!$form->hasErrors())
    {
        $data = [
            'nom'           =>  f('nom'),
            'droit_wiki'    =>  (int) f('droit_wiki'),
            'droit_compta'  =>  (int) f('droit_compta'),
            'droit_config'  =>  (int) f('droit_config'),
            'droit_membres' =>  (int) f('droit_membres'),
            'droit_connexion' => (int) f('droit_connexion'),
            'droit_inscription' => (int) f('droit_inscription'),
            'cacher'        =>  (int) f('cacher'),
            'id_cotisation_obligatoire' => (int) f('id_cotisation_obligatoire'),
        ];

        // Ne pas permettre de modifier la connexion, l'accès à la config et à la gestion des membres
        // pour la catégorie du membre qui édite les catégories, sinon il pourrait s'empêcher
        // de se connecter ou n'avoir aucune catégorie avec le droit de modifier les catégories !
        if ($cat->id == $user->id_categorie)
        {







<













<







30
31
32
33
34
35
36

37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
        'droit_wiki'                => 'in:' . $droits,
        'droit_compta'              => 'in:' . $droits,
        'droit_membres'             => 'in:' . $droits,
        'droit_config'              => sprintf('in:%s,%s', Membres::DROIT_ADMIN, Membres::DROIT_AUCUN),
        'droit_connexion'           => sprintf('in:%s,%s', Membres::DROIT_ACCES, Membres::DROIT_AUCUN),
        'droit_inscription'         => sprintf('in:%s,%s', Membres::DROIT_ACCES, Membres::DROIT_AUCUN),
        'cacher'                    => 'boolean',

    ]);

    if (!$form->hasErrors())
    {
        $data = [
            'nom'           =>  f('nom'),
            'droit_wiki'    =>  (int) f('droit_wiki'),
            'droit_compta'  =>  (int) f('droit_compta'),
            'droit_config'  =>  (int) f('droit_config'),
            'droit_membres' =>  (int) f('droit_membres'),
            'droit_connexion' => (int) f('droit_connexion'),
            'droit_inscription' => (int) f('droit_inscription'),
            'cacher'        =>  (int) f('cacher'),

        ];

        // Ne pas permettre de modifier la connexion, l'accès à la config et à la gestion des membres
        // pour la catégorie du membre qui édite les catégories, sinon il pourrait s'empêcher
        // de se connecter ou n'avoir aucune catégorie avec le droit de modifier les catégories !
        if ($cat->id == $user->id_categorie)
        {
78
79
80
81
82
83
84
85
86
87
88
89
90
    }
}

$tpl->assign('cat', $cat);

$tpl->assign('readonly', $cat->id == $user->id_categorie ? 'disabled="disabled"' : '');

$cotisations = new Cotisations;
$tpl->assign('cotisations', $cotisations->listCurrent());

$tpl->assign('membres', new Membres);

$tpl->display('admin/config/categories/modifier.tpl');







<
<
<



76
77
78
79
80
81
82



83
84
85
    }
}

$tpl->assign('cat', $cat);

$tpl->assign('readonly', $cat->id == $user->id_categorie ? 'disabled="disabled"' : '');




$tpl->assign('membres', new Membres);

$tpl->display('admin/config/categories/modifier.tpl');

Modified src/www/admin/config/index.php from [149f2778eb] to [d2afe0f99d].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37






38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71















72

73
74
75
76
        $config->set('nom_asso', f('nom_asso'));
        $config->set('email_asso', f('email_asso'));
        $config->set('adresse_asso', f('adresse_asso'));
        $config->set('site_asso', f('site_asso'));
        $config->set('accueil_wiki', f('accueil_wiki'));
        $config->set('accueil_connexion', f('accueil_connexion'));
        $config->set('categorie_membres', f('categorie_membres'));
        
        $config->set('champ_identite', f('champ_identite'));
        $config->set('champ_identifiant', f('champ_identifiant'));

        $config->set('pays', f('pays'));
        $config->set('monnaie', f('monnaie'));

        // N'enregistrer les couleurs que si ce ne sont pas les couleurs par défaut
        if (f('couleur1') != ADMIN_COLOR1 || f('couleur2') != ADMIN_COLOR2)
        {
            $config->set('couleur1', f('couleur1'));
            $config->set('couleur2', f('couleur2'));

            if (f('image_fond'))
            {
                $config->set('image_fond', f('image_fond'));
            }
        }
        else
        {
            $config->set('couleur1', null);
            $config->set('couleur2', null);






            $config->set('image_fond', null);
        }

        $config->save();

        Utils::redirect(ADMIN_URL . 'config/?ok');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('ok', qg('ok') !== null);

$server_time = time();

$tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']');


$tpl->assign('php_version', phpversion());
$tpl->assign('has_gpg_support', \KD2\Security::canUseEncryption());
$tpl->assign('server_time', $server_time);

$v = \SQLite3::version();
$tpl->assign('sqlite_version', $v['versionString']);

$tpl->assign('pays', Utils::getCountryList());

$cats = new Membres\Categories;
$tpl->assign('membres_cats', $cats->listSimple());

$tpl->assign('champs', $config->get('champs_membres')->getList());

$tpl->assign('couleur1', $config->get('couleur1') ?: ADMIN_COLOR1);
$tpl->assign('couleur2', $config->get('couleur2') ?: ADMIN_COLOR2);















$tpl->assign('background_image_source', ADMIN_BACKGROUND_IMAGE);

$tpl->assign('couleurs_defaut', [ADMIN_COLOR1, ADMIN_COLOR2]);

$tpl->assign('custom_js', ['color_helper.js']);
$tpl->display('admin/config/index.tpl');







|











<
<
<
<
<





>
>
>
>
>
>


















>
>
















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>




9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27





28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
        $config->set('nom_asso', f('nom_asso'));
        $config->set('email_asso', f('email_asso'));
        $config->set('adresse_asso', f('adresse_asso'));
        $config->set('site_asso', f('site_asso'));
        $config->set('accueil_wiki', f('accueil_wiki'));
        $config->set('accueil_connexion', f('accueil_connexion'));
        $config->set('categorie_membres', f('categorie_membres'));

        $config->set('champ_identite', f('champ_identite'));
        $config->set('champ_identifiant', f('champ_identifiant'));

        $config->set('pays', f('pays'));
        $config->set('monnaie', f('monnaie'));

        // N'enregistrer les couleurs que si ce ne sont pas les couleurs par défaut
        if (f('couleur1') != ADMIN_COLOR1 || f('couleur2') != ADMIN_COLOR2)
        {
            $config->set('couleur1', f('couleur1'));
            $config->set('couleur2', f('couleur2'));





        }
        else
        {
            $config->set('couleur1', null);
            $config->set('couleur2', null);
        }

        if (trim(f('image_fond')) != '') {
            $config->set('image_fond', f('image_fond'));
        }
        elseif (!f('image_fond') && !$config->get('couleur1') && !$config->get('couleur2')) {
            $config->set('image_fond', null);
        }

        $config->save();

        Utils::redirect(ADMIN_URL . 'config/?ok');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('ok', qg('ok') !== null);

$server_time = time();

$tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']');

$tpl->assign('new_version', ENABLE_TECH_DETAILS ? Utils::getLatestVersion() : null);
$tpl->assign('php_version', phpversion());
$tpl->assign('has_gpg_support', \KD2\Security::canUseEncryption());
$tpl->assign('server_time', $server_time);

$v = \SQLite3::version();
$tpl->assign('sqlite_version', $v['versionString']);

$tpl->assign('pays', Utils::getCountryList());

$cats = new Membres\Categories;
$tpl->assign('membres_cats', $cats->listSimple());

$tpl->assign('champs', $config->get('champs_membres')->getList());

$tpl->assign('couleur1', $config->get('couleur1') ?: ADMIN_COLOR1);
$tpl->assign('couleur2', $config->get('couleur2') ?: ADMIN_COLOR2);

$image_fond = ADMIN_BACKGROUND_IMAGE;

if ($config->get('image_fond'))
{
    try {
        $f = new Fichiers($config->get('image_fond'));
        $image_fond = $f->getURL();
    }
    catch (\InvalidArgumentException $e)
    {
        // Fichier qui n'existe pas/plus
    }
}

$tpl->assign('background_image_source', $image_fond);
$tpl->assign('background_image_default', ADMIN_BACKGROUND_IMAGE);
$tpl->assign('couleurs_defaut', [ADMIN_COLOR1, ADMIN_COLOR2]);

$tpl->assign('custom_js', ['color_helper.js']);
$tpl->display('admin/config/index.tpl');

Modified src/www/admin/config/logs.php from [3641ab6b53] to [3f8055d3aa].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23




24
25
26
27
28
29
30
31
32
<?php
namespace Garradin;

use KD2\ErrorManager;

require_once __DIR__ . '/_inc.php';

if (qg('type') == 'errors' && ERRORS_ENABLE_LOG_VIEW)
{
    $reports = ErrorManager::getReportsFromLog(null, qg('id'));

    $reports = array_reverse($reports, true);

    foreach ($reports as &$report)
    {
        $report->context->date = strtotime($report->context->date);
    }

    unset($report);

    $errors = [];

    if (qg('id'))
    {




        $tpl->assign('id', qg('id'));
        $tpl->assign('main', $reports[0]);
        $tpl->assign('reports', $reports);
    }
    else
    {
        foreach ($reports as $report)
        {
            if (!isset($errors[$report->context->id]))







|


>













>
>
>
>

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
namespace Garradin;

use KD2\ErrorManager;

require_once __DIR__ . '/_inc.php';

if (qg('type') == 'errors' && ENABLE_TECH_DETAILS)
{
    $reports = ErrorManager::getReportsFromLog(null, qg('id'));

    $reports = array_reverse($reports, true);

    foreach ($reports as &$report)
    {
        $report->context->date = strtotime($report->context->date);
    }

    unset($report);

    $errors = [];

    if (qg('id'))
    {
        if (!count($reports)) {
            throw new UserException('Erreur inconnue');
        }

        $tpl->assign('id', qg('id'));
        $tpl->assign('main', reset($reports));
        $tpl->assign('reports', $reports);
    }
    else
    {
        foreach ($reports as $report)
        {
            if (!isset($errors[$report->context->id]))

Modified src/www/admin/index.php from [5988e390c9] to [7e32fa08cc].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33

34
35
36
37
38
39
40
41
42
43

$tpl->assign('categorie', $categorie);

$wiki = new Wiki;
$page = $wiki->getByURI($config->get('accueil_connexion'));
$tpl->assign('page', $page);

$cats = new Membres\Categories;

$categorie = $cats->get($user->id_categorie);

$cotisations = new Membres\Cotisations;

if (!empty($categorie->id_cotisation_obligatoire))
{
	$tpl->assign('cotisation', $cotisations->isMemberUpToDate($user->id, $categorie->id_cotisation_obligatoire));
}
else
{
	$tpl->assign('cotisation', false);
}

$tpl->assign('custom_css', ['wiki.css']);


$tpl->assign('banniere', Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session]));


$tpl->display('admin/index.tpl');
flush();

// Si pas de cron on réalise les tâches automatisées à ce moment-là
// c'est pas idéal mais mieux que rien
if (!USE_CRON)
{
	require_once ROOT . '/cron.php';
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


>
|
>








|

9
10
11
12
13
14
15















16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

$tpl->assign('categorie', $categorie);

$wiki = new Wiki;
$page = $wiki->getByURI($config->get('accueil_connexion'));
$tpl->assign('page', $page);
















$tpl->assign('custom_css', ['wiki.css']);

$banner = null;
Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner);
$tpl->assign('banniere', $banner);

$tpl->display('admin/index.tpl');
flush();

// Si pas de cron on réalise les tâches automatisées à ce moment-là
// c'est pas idéal mais mieux que rien
if (!USE_CRON)
{
	require_once ROOT . '/scripts/cron.php';
}

Modified src/www/admin/install.php from [29602daaba] to [d0c2a835bd].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
namespace Garradin;

const INSTALL_PROCESS = true;

require_once __DIR__ . '/../../include/test_required.php';
require_once __DIR__ . '/../../include/init.php';

Install::checkAndCreateDirectories();

if (!file_exists(DB_FILE))
{
    // Renommage du fichier sqlite à la version 0.5.0
    $old_file = str_replace('.sqlite', '.db', DB_FILE);

    if (file_exists($old_file))
    {
        rename($old_file, DB_FILE);
        Utils::redirect(ADMIN_URL . 'upgrade.php');
    }
}

function f($key)
{
    return \KD2\Form::get($key);
}

$tpl = Template::getInstance();
$tpl->assign('admin_url', ADMIN_URL);










<
<
<
<
<
<
<
<
<
<
<
<







1
2
3
4
5
6
7
8
9
10












11
12
13
14
15
16
17
<?php
namespace Garradin;

const INSTALL_PROCESS = true;

require_once __DIR__ . '/../../include/test_required.php';
require_once __DIR__ . '/../../include/init.php';

Install::checkAndCreateDirectories();













function f($key)
{
    return \KD2\Form::get($key);
}

$tpl = Template::getInstance();
$tpl->assign('admin_url', ADMIN_URL);
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
else
{
    $tpl->assign('disabled', false);

    if (f('save'))
    {
        $form->check('install', [
            'nom_asso'     => 'required',
            'email_asso'   => 'required|email',
            'nom_membre'   => 'required',
            'email_membre' => 'required|email',
            'passe'        => 'confirmed|required',
            'cat_membre'   => 'required',
        ]);

        if (!$form->hasErrors())
        {
            try {
            	Install::install(f('nom_asso'), f('adresse_asso'), f('email_asso'),
            		f('cat_membre'), f('nom_membre'), f('email_membre'), f('passe'),
            		WWW_URL);

            	Utils::redirect(ADMIN_URL . 'login.php');
            }
            catch (UserException $e)
            {
                @unlink(DB_FILE);








<
<



<





|
<
<







26
27
28
29
30
31
32


33
34
35

36
37
38
39
40
41


42
43
44
45
46
47
48
else
{
    $tpl->assign('disabled', false);

    if (f('save'))
    {
        $form->check('install', [


            'nom_membre'   => 'required',
            'email_membre' => 'required|email',
            'passe'        => 'confirmed|required',

        ]);

        if (!$form->hasErrors())
        {
            try {
            	Install::install(f('nom_asso'), f('nom_membre'), f('email_membre'), f('passe'));



            	Utils::redirect(ADMIN_URL . 'login.php');
            }
            catch (UserException $e)
            {
                @unlink(DB_FILE);

Modified src/www/admin/login.php from [3ef470efd1] to [939e98ed56].

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

38
39
40
41


42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
59

    header('Content-Type: image/gif');
    echo base64_decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");

    exit;
}

$login = null;

// Soumission du formulaire
if (f('login'))
{
    $form->check('login', [
        '_id'       => 'required|string',
        'passe'     => 'required|string',
        'permanent' => 'boolean',
    ]);


    if (!$form->hasErrors() && ($login = $session->login(f('_id'), f('passe'), (bool) f('permanent'))))
    {
        Utils::redirect(ADMIN_URL);


    }
}

$session->cleanOldCookies();

$champs = $config->get('champs_membres');

$champ = $champs->get($config->get('champ_identifiant'));


$tpl->assign('ssl_enabled', empty($_SERVER['HTTPS']) ? false : true);
$tpl->assign('prefer_ssl', (bool)PREFER_HTTPS);
$tpl->assign('own_https_url', str_replace('http://', 'https://', utils::getSelfURL()));

$tpl->assign('champ', $champ);
$tpl->assign('fail', $login === false);
$tpl->assign('changed', qg('changed') !== null);

$tpl->display('admin/login.tpl');







|
|
|
<
|
|
|
<
<
<
>
|
<
|
<
>
>

|
|
<
|
<
|
<
>





|
<



21
22
23
24
25
26
27
28
29
30

31
32
33



34
35

36

37
38
39
40
41

42

43

44
45
46
47
48
49
50

51
52
53

    header('Content-Type: image/gif');
    echo base64_decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");

    exit;
}

$champs = $config->get('champs_membres');
$id_field = (object) $champs->get($config->get('champ_identifiant'));
$id_field_name = $id_field->title;


$form->runIf('login', function () use ($id_field_name, $session) {
    if (!trim(f('_id'))) {



        throw new UserException(sprintf('L\'identifiant (%s) n\'a pas été renseigné.', $id_field_name));
    }



    if (!trim(f('password'))) {
        throw new UserException('Le mot de passe n\'a pas été renseigné.');
    }

    if (!$session->login(f('_id'), f('password'), (bool) f('permanent'))) {

        throw new UserException(sprintf("Connexion impossible.\nVérifiez votre identifiant (%s) et votre mot de passe.", $id_field_name));

    }

}, 'login', ADMIN_URL);

$tpl->assign('ssl_enabled', empty($_SERVER['HTTPS']) ? false : true);
$tpl->assign('prefer_ssl', (bool)PREFER_HTTPS);
$tpl->assign('own_https_url', str_replace('http://', 'https://', utils::getSelfURL()));

$tpl->assign(compact('id_field_name'));

$tpl->assign('changed', qg('changed') !== null);

$tpl->display('admin/login.tpl');

Modified src/www/admin/membres/ajouter.php from [855d0663f7] to [9c63eece10].

41
42
43
44
45
46
47


48
49
50
51
52
53
54
55
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}



$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $champs->getAll());

$tpl->assign('membres_cats', $cats->listSimple());
$tpl->assign('current_cat', f('id_categorie') ?: $config->get('categorie_membres'));

$tpl->display('admin/membres/ajouter.tpl');







>
>








41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('id_field_name', $config->get('champ_identifiant'));

$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $champs->getAll());

$tpl->assign('membres_cats', $cats->listSimple());
$tpl->assign('current_cat', f('id_categorie') ?: $config->get('categorie_membres'));

$tpl->display('admin/membres/ajouter.tpl');

Deleted src/www/admin/membres/cotisations.php version [d156e1e5bb].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$membre = $membres->get($id);

if (!$membre)
{
    throw new UserException("Ce membre n'existe pas.");
}

$cats = new Membres\Categories;

$categorie = $cats->get($membre->id_categorie);
$tpl->assign('categorie', $categorie);

$cotisations = new Membres\Cotisations;

if (!empty($categorie->id_cotisation_obligatoire))
{
	$tpl->assign('cotisation', $cotisations->isMemberUpToDate($membre->id, $categorie->id_cotisation_obligatoire));
}
else
{
	$tpl->assign('cotisation', false);
}

$tpl->assign('nb_activites', $cotisations->countForMember($membre->id));
$tpl->assign('cotisations', $cotisations->listForMember($membre->id));
$tpl->assign('cotisations_membre', $cotisations->listSubscriptionsForMember($membre->id));

$tpl->assign('membre', $membre);

$tpl->display('admin/membres/cotisations.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































Deleted src/www/admin/membres/cotisations/ajout.php version [b2cc2cef01].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ECRITURE);

$membre = false;

if (($id = qg('id')) && is_numeric($id))
{
    $membre = $membres->get((int) $id);

    if (!$membre)
    {
        throw new UserException("Ce membre n'existe pas.");
    }

    $cats = new Membres\Categories;
    $categorie = $cats->get($membre->id_categorie);
}
else
{
    $categorie = ['id_cotisation_obligatoire' => false];
}

$cotisations = new Cotisations;
$m_cotisations = new Membres\Cotisations;

$cats = new Compta\Categories;
$banques = new Compta\Comptes_Bancaires;

if (f('add') && $form->check('add_cotisation'))
{
    try {
        $data = [
            'date'          =>  f('date'),
            'id_cotisation' =>  f('id_cotisation'),
            'id_membre'     =>  f('id_membre'),
            'numero_membre' =>  f('numero_membre'),
            'id_auteur'     =>  $user->id,
        ];

        $compta = [
            'montant'        =>  f('montant'),
            'moyen_paiement' =>  f('moyen_paiement'),
            'numero_cheque'  =>  f('numero_cheque'),
            'banque'         =>  f('banque'),
            'numero_piece'   =>  f('numero_piece'),
            'remarques'      =>  f('remarques'),
            'a_encaisser'    =>  f('a_encaisser'),
        ];

        $id_membre = $m_cotisations->add($data, $compta);

        Utils::redirect(ADMIN_URL . 'membres/cotisations.php?id=' . $id_membre);
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('membre', $membre);

$tpl->assign('cotisations', $cotisations->listCurrent());

$tpl->assign('default_co', null);
$tpl->assign('default_amount', 0.00);
$tpl->assign('default_date', date('Y-m-d'));
$tpl->assign('default_compta', null);

$tpl->assign('moyens_paiement', $cats->listMoyensPaiement());
$tpl->assign('moyen_paiement', f('moyen_paiement') ?: 'ES');
$tpl->assign('comptes_bancaires', $banques->getList());
$tpl->assign('banque', f('banque'));


if (qg('cotisation'))
{
    $co = $cotisations->get(qg('cotisation'));

    if (!$co)
    {
        throw new UserException("La cotisation indiquée en paramètre n'existe pas.");
    }

    $tpl->assign('default_co', $co->id);
    $tpl->assign('default_compta', $co->id_categorie_compta);
    $tpl->assign('default_amount', $co->montant);
}
elseif ($membre)
{
    if (!empty($categorie->id_cotisation_obligatoire))
    {
        $co = $cotisations->get($categorie->id_cotisation_obligatoire);

        $tpl->assign('default_co', $co->id);
        $tpl->assign('default_amount', $co->montant);
    }
}

$tpl->display('admin/membres/cotisations/ajout.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































Deleted src/www/admin/membres/cotisations/gestion/modifier.php version [c6ecbc69e3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
namespace Garradin;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

if (!qg('id') || !is_numeric(qg('id')))
{
    throw new UserException("Argument du numéro de cotisation manquant.");
}

$cotisations = new Cotisations;

$co = $cotisations->get(qg('id'));
$cats = new Compta\Categories;

if (!$co)
{
    throw new UserException("Cette cotisation n'existe pas.");
}

if (f('save') && $form->check('edit_co_' . $co->id))
{
    try {
        $duree = f('periodicite') == 'jours' ? (int) f('duree') : null;
        $debut = f('periodicite') == 'date' ? f('debut') : null;
        $fin = f('periodicite') == 'date' ? f('fin') : null;
        $id_cat = f('categorie') ? (int) f('id_categorie_compta') : null;

        $cotisations->edit($co->id, [
            'intitule'            => f('intitule'),
            'description'         => f('description'),
            'montant'             => (float) f('montant'),
            'duree'               => $duree,
            'debut'               => $debut,
            'fin'                 => $fin,
            'id_categorie_compta' => $id_cat,
        ]);

        Utils::redirect(ADMIN_URL . 'membres/cotisations/');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}


$co->periodicite = $co->duree ? 'jours' : ($co->debut ? 'date' : 'ponctuel');
$co->categorie = $co->id_categorie_compta ? 1 : 0;

$tpl->assign('cotisation', $co);
$tpl->assign('categories', $cats->getList(Compta\Categories::RECETTES));

$tpl->display('admin/membres/cotisations/gestion/modifier.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































Deleted src/www/admin/membres/cotisations/gestion/supprimer.php version [c3a9f9323c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace Garradin;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

if (!qg('id') || !is_numeric(qg('id')))
{
    throw new UserException("Argument du numéro de cotisation manquant.");
}

$cotisations = new Cotisations;

$co = $cotisations->get(qg('id'));

if (!$co)
{
    throw new UserException("Cette cotisation n'existe pas.");
}

if (f('delete') && $form->check('delete_co_' . $co->id))
{
    try {
        $cotisations->delete($co->id);
        Utils::redirect(ADMIN_URL . 'membres/cotisations/');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('cotisation', $co);

$tpl->display('admin/membres/cotisations/gestion/supprimer.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































Deleted src/www/admin/membres/cotisations/rappels.php version [c3e2281ab7].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$membre = $membres->get($id);

if (!$membre)
{
	throw new UserException("Ce membre n'existe pas.");
}

$re = new Rappels_Envoyes;
$cm = new Membres\Cotisations;

if (f('save'))
{
	$session->requireAccess('membres', Membres::DROIT_ECRITURE);

	$medias = implode(',', [$re::MEDIA_EMAIL, $re::MEDIA_COURRIER, $re::MEDIA_TELEPHONE, $re::MEDIA_AUTRE]);

	$form->check('add_rappel_' . $membre->id, [
		'id_cotisation' => 'numeric|required',
		'media'         => 'numeric|required|in:' . $medias,
		'date'          => 'required|date_format:Y-m-d'
	]);

	if (!$form->hasErrors())
	{
		try {
			$re->add([
				'id_rappel'     => NULL,
				'id_cotisation'	=> f('id_cotisation'),
				'id_membre'		=> $membre->id,
				'media'			=> f('media'),
				'date'			=> f('date'),
			]);

			Utils::redirect(ADMIN_URL . 'membres/cotisations/rappels.php?id=' . $membre->id . '&ok');
		}
		catch (UserException $e)
		{
			$form->addError($e->getMessage());
		}
	}
}

$tpl->assign('ok', null !== qg('ok'));
$tpl->assign('membre', $membre);
$tpl->assign('cotisations', $cm->listSubscriptionsForMember($membre->id));
$tpl->assign('default_date', date('Y-m-d'));
$tpl->assign('rappels', $re->listForMember($membre->id));
$tpl->assign('rappels_envoyes', $re);

$tpl->display('admin/membres/cotisations/rappels.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































Modified src/www/admin/membres/fiche.php from [890dac5864] to [1ec254fd3b].

1
2
3



4
5
6
7
8
9
10
<?php
namespace Garradin;




require_once __DIR__ . '/_inc.php';

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$membre = $membres->get($id);



>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\Services\Services_User;

require_once __DIR__ . '/_inc.php';

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$membre = $membres->get($id);
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

42
43
44
45
46
$tpl->assign('champs', $champs->getList());

$cats = new Membres\Categories;

$categorie = $cats->get($membre->id_categorie);
$tpl->assign('categorie', $categorie);

$cotisations = new Membres\Cotisations;

if (!empty($categorie->id_cotisation_obligatoire))
{
	$tpl->assign('cotisation', $cotisations->isMemberUpToDate($membre->id, $categorie->id_cotisation_obligatoire));
}
else
{
	$tpl->assign('cotisation', false);
}

$tpl->assign('nb_activites', $cotisations->countForMember($membre->id));

if ($session->canAccess('compta', Membres::DROIT_ACCES))
{
	$journal = new Compta\Journal;
	$tpl->assign('nb_operations', $journal->countForMember($membre->id));

}

$tpl->assign('membre', $membre);

$tpl->display('admin/membres/fiche.tpl');







<
|
<
|
<
<
<
<
<
<
<
<
<
|
<
<
|
>





21
22
23
24
25
26
27

28

29









30


31
32
33
34
35
36
37
$tpl->assign('champs', $champs->getList());

$cats = new Membres\Categories;

$categorie = $cats->get($membre->id_categorie);
$tpl->assign('categorie', $categorie);


$tpl->assign('services', Services_User::listDistinctForUser($membre->id));











if ($session->canAccess('compta', Membres::DROIT_ACCES)) {


	$tpl->assign('transactions_linked', Transactions::countForUser($membre->id));
	$tpl->assign('transactions_created', Transactions::countForCreator($membre->id));
}

$tpl->assign('membre', $membre);

$tpl->display('admin/membres/fiche.tpl');

Modified src/www/admin/membres/import.php from [bd11ab09aa] to [34b02fc73c].

16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

47
48
49
50
51

52
53
54
55

56
57
58




59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
}
elseif (qg('export') == 'ods')
{
    $import->toODS();
    exit;
}


$champs = $config->get('champs_membres')->getAll();

$csv_file = false;

if (f('csv_encoded'))
{
    $form->check('membres_import', [
        'csv_encoded'     => 'required|json',
        'csv_translate'   => 'required|array',
        'skip_first_line' => 'boolean',
    ]);

    $csv_file = json_decode(f('csv_encoded'), true);

    if (!$form->hasErrors())
    {
        try
        {
            $import->fromArray($csv_file, f('csv_translate'), $user->id, f('skip_first_line') ? 1 : 0);
            Utils::redirect(ADMIN_URL . 'membres/import.php?ok');
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());

        }
    }
}
elseif (f('import'))
{

    $form->check('membres_import', [
        'upload' => 'file|required',
        'type'   => 'required|in:csv,garradin',
    ]);


    if (!$form->hasErrors())
    {




        try

        {
            if (f('type') == 'garradin')
            {
                $import->fromGarradinCSV($_FILES['upload']['tmp_name'], $user->id);
                Utils::redirect(ADMIN_URL . 'membres/import.php?ok');
            }
            elseif (f('type') == 'csv')
            {
                $csv_file = $import->getCSVAsArray($_FILES['upload']['tmp_name']);
            }
            else
            {
                throw new UserException('Import inconnu.');
            }
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('ok', null !== qg('ok') ? true : false);

$tpl->assign('csv_file', $csv_file);
$tpl->assign('csv_first_line', $csv_file ? reset($csv_file) : null);

$tpl->assign('max_upload_size', Utils::getMaxUploadSize());

$tpl->assign('garradin_champs', $champs);

$tpl->display('admin/membres/import.tpl');







>

|
<

|
|
<
<
<
<
<
|
<
|
<
<
|
<
<
<
|
<
|
<
>
|
|
<
<
<
>
|
|
<
|
>
|
<
|
>
>
>
>
|
>
|
|
<
|
|
|
|
<
|
<
<
<
|
<
<
<
<
<
<
<
<



|
<



<
<

16
17
18
19
20
21
22
23
24
25

26
27
28





29

30


31



32

33

34
35
36



37
38
39

40
41
42

43
44
45
46
47
48
49
50
51

52
53
54
55

56



57








58
59
60
61

62
63
64


65
}
elseif (qg('export') == 'ods')
{
    $import->toODS();
    exit;
}

$csv = new CSV_Custom($session, 'users_import');
$champs = $config->get('champs_membres')->getAll();
$csrf_key = 'membres_import';


$columns = [];






foreach ($champs as $key => $config) {

    if (!isset($config->title)) {


        continue;



    }



    $columns[$key] = $config->title;
}




$csv->setColumns($columns);

if (f('cancel')) {

    $csv->clear();
    Utils::redirect(Utils::getSelfURL(false));
}


$form->runIf(f('import') && $csv->loaded(), function () use ($csv, $import, $user) {
    $csv->setTranslationTable(f('translation_table'));
    $csv->skip((int)f('skip_first_line'));
    $import->fromCustomCSV($csv, $user->id);
    $csv->clear();
}, $csrf_key, ADMIN_URL . 'membres/import.php?ok');

$form->runIf(f('import') && f('type') == 'garradin' && !empty($_FILES['upload']['tmp_name']), function () use ($import, $user) {

    $import->fromGarradinCSV($_FILES['upload']['tmp_name'], $user->id);
}, $csrf_key, ADMIN_URL . 'membres/import.php?ok');

$form->runIf(f('import') && f('type') == 'custom' && !empty($_FILES['upload']['tmp_name']), function () use ($csv) {

    $csv->load($_FILES['upload']);



}, $csrf_key, ADMIN_URL . 'membres/import.php');









$tpl->assign('ok', null !== qg('ok') ? true : false);

$tpl->assign(compact('csv', 'csrf_key'));


$tpl->assign('max_upload_size', Utils::getMaxUploadSize());



$tpl->display('admin/membres/import.tpl');

Modified src/www/admin/membres/message_collectif.php from [41976be460] to [b187670977].

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
            catch (UserException $e) {
                $form->addError($e->getMessage());
            }
        }

        if (isset($recipients) && !count($recipients))
        {
            $form->addError('Aucun membre dans la liste.');
        }
    }
    else
    {
        $form->addErrror('Destinataires invalides : ' . f('recipients'));
    }








|







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
            catch (UserException $e) {
                $form->addError($e->getMessage());
            }
        }

        if (isset($recipients) && !count($recipients))
        {
            $form->addError('La liste de destinataires sélectionnée ne comporte aucun membre, ou aucun avec une adresse e-mail renseignée.');
        }
    }
    else
    {
        $form->addErrror('Destinataires invalides : ' . f('recipients'));
    }

Modified src/www/admin/membres/modifier.php from [5c3655690e] to [6fe3b3ffab].

76
77
78
79
80
81
82

83
84
85
86
87
88
89
90
91
92
93
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}


$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $champs->getAll());

$tpl->assign('membres_cats', $cats->listSimple());
$tpl->assign('current_cat', f('id_categorie') ?: $membre->id_categorie);

$tpl->assign('can_change_id', $session->canAccess('membres', Membres::DROIT_ADMIN));

$tpl->assign('membre', $membre);

$tpl->display('admin/membres/modifier.tpl');







>











76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('id_field_name', $config->get('champ_identifiant'));
$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $champs->getAll());

$tpl->assign('membres_cats', $cats->listSimple());
$tpl->assign('current_cat', f('id_categorie') ?: $membre->id_categorie);

$tpl->assign('can_change_id', $session->canAccess('membres', Membres::DROIT_ADMIN));

$tpl->assign('membre', $membre);

$tpl->display('admin/membres/modifier.tpl');

Modified src/www/admin/membres/recherche.php from [ed8d36c24a] to [e65f5d600d].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$recherche = new Recherche;

$champs = $config->get('champs_membres');
$text_query = trim(qg('qt'));
$query = null;
$limit = f('limit') ?: 100;
$order = f('order');
$desc = (bool) f('desc');
$sql_query = null;
$id = f('id') ?: qg('id');

// Recherche simple
if ($text_query !== '')
{
    $operator = 'LIKE %?%';

    if (is_numeric(trim($text_query)))
    {
        $column = 'numero';
        $operator = '= ?';
    }
    elseif (strpos($text_query, '@') !== false)
    {
        $column = 'email';
    }
    else
    {
        $column = $config->get('champ_identite');
    }

    $query = [[
        'operator' => 'AND',
        'conditions' => [
            [
                'column'   => $column,
                'operator' => $operator,
                'values'   => [$text_query],
            ],
        ],
    ]];

    $order = $column;
}
elseif ($id)
{
    $r = $recherche->get($id);

    if (!$r || $r->type != Recherche::TYPE_JSON)
    {
        throw new UserException('Recherche inconnue ou invalide');
    }

    $query = $r->query;
    $order = $r->order;
    $desc = $r->desc;
    $limit = (int) f('limit') ?: $r->limit;

    $tpl->assign('recherche', $r);
}

if (f('q') !== null)
{
    $query = json_decode(f('q'), true);
}

if ($query)
{
    try {
        $sql_query = $recherche->buildQuery('membres', $query, $order, $desc, $limit);
        $result = $recherche->searchSQL('membres', $sql_query);
    }
    catch (UserException $e) {
        $form->addError($e->getMessage());
        $query = null;
    }
}

if ($query)
{
    if (count($result) == 1 && $text_query !== '')
    {
        Utils::redirect(ADMIN_URL . 'membres/fiche.php?id=' . (int)$result[0]->id);
    }

    if (f('save') && !$form->hasErrors())
    {
        $query = [
            'query' => $query,
            'order' => $order,
            'limit' => $limit,
            'desc'  => $desc,
        ];

        if ($id)
        {
            $recherche->edit($id, [
                'type'    => Recherche::TYPE_JSON,
                'contenu' => $query,
            ]);
        }
        else
        {
            $id = $recherche->add('Recherche avancée du ' . date('d/m/Y H:i:s'), $user->id, $recherche::TYPE_JSON, 'membres', $query);
        }

        Utils::redirect('/admin/membres/recherches.php?id=' . $id);
    }

    $tpl->assign('result_header', $membres->getSearchHeaderFields($result));
}
else
{
    $query = [[
        'operator' => 'AND',
        'conditions' => [
            [
                'column'   => $config->get('champ_identite'),
                'operator' => '= ?',
                'values'   => [''],
            ],
        ],
    ]];
    $result = null;
}

$tpl->assign('id', $id);
$tpl->assign('query', $query);
$tpl->assign('sql_query', $sql_query);
$tpl->assign('result', $result);
$tpl->assign('order', $order);
$tpl->assign('desc', $desc);
$tpl->assign('limit', $limit);
$tpl->assign('colonnes', $recherche->getColumns('membres'));

$tpl->display('admin/membres/recherche.tpl');





<
|
<
<
<
<
<
<
<
<

<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
1
2
3
4
5

6








7




8























































































































<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';


$target = 'membres';













require __DIR__ . '/../common/search.php';























































































































Deleted src/www/admin/membres/recherche_sql.php version [b02dad85af].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$recherche = new Recherche;

$query = trim(qg('query'));
$result = null;
$id = (int) qg('id');

if ($id)
{
	$r = $recherche->get($id);

	if (!$r || $r->type != Recherche::TYPE_SQL)
	{
		throw new UserException('Recherche inconnue');
	}

	if (!$session->canAccess('membres', Membres::DROIT_ADMIN) || !$query)
	{
		$query = $r->contenu;
	}

	$tpl->assign('recherche', $r);
}
else
{
	$session->requireAccess('membres', Membres::DROIT_ADMIN);
}

$tpl->assign('schema', $recherche->schema('membres'));
$tpl->assign('query', $query);

if ($query != '')
{
	try {
		$result = $recherche->searchSQL('membres', $query);
	}
	catch (\Exception $e)
	{
		$form->addError($e->getMessage());
	}

	if (!$form->hasErrors() && qg('save'))
	{
		if ($id)
		{
			$recherche->edit($id, [
				'type'    => Recherche::TYPE_SQL,
				'contenu' => $query,
			]);
		}
		else
		{
			$id = $recherche->add('Recherche SQL du ' . date('d/m/Y H:i:s'), $user->id, $recherche::TYPE_SQL, 'membres', $query);
		}

		Utils::redirect('/admin/membres/recherches.php?id=' . $id);
	}
}

$tpl->assign('result', $result);
$tpl->assign('id', $id);
$tpl->display('admin/membres/recherche_sql.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































































































Modified src/www/admin/membres/recherches.php from [7efc82999d] to [f67fc4e789].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

$recherche = new Recherche;
$mode = null;

if (qg('edit') || qg('delete'))
{
	$r = $recherche->get(qg('edit') ?: qg('delete'));

	if (!$r)
	{
		throw new UserException('Recherche non trouvée');
	}

	if ($r->id_membre !== null && $r->id_membre != $user->id)
	{
		throw new UserException('Recherche privée appartenant à un autre membre.');
	}

	$tpl->assign('recherche', $r);

	$mode = qg('edit') ? 'edit' : 'delete';
}

if ($mode == 'edit' && f('save') && $form->check('edit_recherche_' . $r->id))
{
	try {
		$recherche->edit($r->id, [
			'intitule'  => f('intitule'),
			'id_membre' => f('prive') ? $user->id : null,
		]);

		Utils::redirect('/admin/membres/recherches.php');
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}
elseif ($mode == 'delete' && f('delete') && $form->check('del_recherche_' . $r->id))
{
	$recherche->remove($r->id);
	Utils::redirect('/admin/membres/recherches.php');
}

$tpl->assign('mode', $mode);

if (!$mode)
{
	$tpl->assign('liste', $recherche->getList($user->id, 'membres'));
}

$tpl->display('admin/membres/recherches.tpl');





<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
1
2
3
4
5


6



























7
8









9








<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';



$target = 'membres';



























$search_url = ADMIN_URL . 'membres/recherche.php';










require __DIR__ . '/../common/saved_searches.php';








Added src/www/admin/membres/selector.php version [88f3f59197].









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace Garradin;

require_once __DIR__ . '/_inc.php';

header('X-Frame-Options: SAMEORIGIN', true);

$text_query = trim(qg('q'));

$tpl->assign('list', []);

// Recherche simple
if ($text_query !== '')
{
    $tpl->assign('list', (new Membres)->quickSearch($text_query));
}

$tpl->assign('query', $text_query);

$tpl->display('admin/membres/selector.tpl');

Modified src/www/admin/mes_infos_securite.php from [c7676c4b61] to [07c69221f1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace Garradin;

require_once __DIR__ . '/_inc.php';

$confirm = false;

if (f('confirm'))
{
    $form->check('edit_me_security', [
        'passe'       => 'confirmed|min:6',
        'passe_check' => 'required',
        'code'        => 'min:6|max:6',
    ]);

    if (f('passe_check') && !$session->checkPassword(f('passe_check'), $user->passe))
    {
        $form->addError('Le mot de passe fourni ne correspond pas au mot de passe actuel. Merci de bien vouloir renseigner votre mot de passe courant pour confirmer les changements.');
    }
    elseif (f('otp_secret') && f('otp_secret') != 'disable' && !f('code')) {













<







1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
<?php

namespace Garradin;

require_once __DIR__ . '/_inc.php';

$confirm = false;

if (f('confirm'))
{
    $form->check('edit_me_security', [
        'passe'       => 'confirmed|min:6',
        'passe_check' => 'required',

    ]);

    if (f('passe_check') && !$session->checkPassword(f('passe_check'), $user->passe))
    {
        $form->addError('Le mot de passe fourni ne correspond pas au mot de passe actuel. Merci de bien vouloir renseigner votre mot de passe courant pour confirmer les changements.');
    }
    elseif (f('otp_secret') && f('otp_secret') != 'disable' && !f('code')) {

Modified src/www/admin/my_services.php from [8944c5b1e8] to [395c086295].

1
2
3


4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace Garradin;



require_once __DIR__ . '/_inc.php';

$tpl->assign('membre', $user);

$cats = new Membres\Categories;

$categorie = $cats->get($user->id_categorie);
$tpl->assign('categorie', $categorie);

$cotisations = new Membres\Cotisations;

if (!empty($categorie->id_cotisation_obligatoire))
{
    $tpl->assign('cotisation', $cotisations->isMemberUpToDate($user->id, $categorie->id_cotisation_obligatoire));
}
else
{
    $tpl->assign('cotisation', false);
}

$tpl->assign('nb_activites', $cotisations->countForMember($user->id));
$tpl->assign('cotisations', $cotisations->listForMember($user->id));
$tpl->assign('cotisations_membre', $cotisations->listSubscriptionsForMember($user->id));

$tpl->display('admin/mes_cotisations.tpl');



>
>




<
|
<
<
|
<

<
<
<
<
<
<
|
|
|
<
<
<

|
1
2
3
4
5
6
7
8
9

10


11

12






13
14
15



16
17
<?php
namespace Garradin;

use Garradin\Services\Services_User;

require_once __DIR__ . '/_inc.php';

$tpl->assign('membre', $user);


$list = Services_User::perUserList($user->id);


$list->loadFromQueryString();








$tpl->assign(compact('list'));

$tpl->assign('services', Services_User::listDistinctForUser($user->id));




$tpl->display('my_services.tpl');

Added src/www/admin/services/_inc.php version [b575a08455].















>
>
>
>
>
>
>
1
2
3
4
5
6
7
<?php

namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ACCES);

Modified src/www/admin/services/delete.php from [298dee0445] to [d664c15fa3].

1
2
3


4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
namespace Garradin;



require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ECRITURE);

$membre = false;

$cotisations = new Cotisations;
$m_cotisations = new Membres\Cotisations;

qv(['id' => 'required|numeric']);

$id = (int) qg('id');

$co = $m_cotisations->get($id);

if (!$co)
{
    throw new UserException("Cette cotisation membre n'existe pas.");
}

$membre = $membres->get($co->id_membre);

if (!$membre)
{
    throw new UserException("Le membre lié à la cotisation n'existe pas ou plus.");
}

if (f('delete'))
{
    if ($form->check('del_cotisation_' . $co->id))
    {
        try {
            $m_cotisations->delete($co->id);
            Utils::redirect(ADMIN_URL . 'membres/cotisations.php?id=' . $membre->id);
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('membre', $membre);
$tpl->assign('cotisation', $co);
$tpl->assign('nb_operations', $m_cotisations->countOperationsCompta($co->id));

$tpl->display('admin/membres/cotisations/supprimer.tpl');



>
>
|

|

<
<
<
<
<
<
<
|

<
<
|
<
|


<
|
<
|
<
<
|
<
<
<
<
<
|
|
|
<
<
<
<
<
<
<
|
<
<

|
1
2
3
4
5
6
7
8
9







10
11


12

13
14
15

16

17


18





19
20
21







22


23
24
<?php
namespace Garradin;

use Garradin\Services\Services;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);








$service = Services::get((int) qg('id'));



if (!$service) {

	throw new UserException("Cette activité n'existe pas");
}


$csrf_key = 'service_delete_' . $service->id();




$form->runIf('delete', function () use ($service) {





	$service->delete();
}, $csrf_key, ADMIN_URL . 'services/');








$tpl->assign(compact('service', 'csrf_key'));



$tpl->display('services/delete.tpl');

Modified src/www/admin/services/details.php from [8935c9e002] to [c951b03a28].

1
2

3
4
5
6
7
8
9

10
11
12
13
14
15
16
17

18


19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
namespace Garradin;


require_once __DIR__ . '/../_inc.php';

qv(['id' => 'required|numeric']);

$id = (int) qg('id');


$cotisations = new Cotisations;
$m_cotisations = new Membres\Cotisations;

$co = $cotisations->get($id);

if (!$co)
{
    throw new UserException("Cette cotisation n'existe pas.");

}



$page = (int) qg('p') ?: 1;
$categories = (int) qg('cats');


$tpl->assign('cats', $categories);
$tpl->assign('page', $page);
$tpl->assign('bypage', Membres\Cotisations::ITEMS_PER_PAGE);
$tpl->assign('total', $m_cotisations->countMembersForCotisation($co->id));
$tpl->assign('pagination_url', Utils::getSelfUrl([
	'id' => $co->id,
	'o' => qg('o'),
	(qg('a') !== null ? 'a' : 'd') => '',
	'p'  => '[ID]',
	'cats' => $categories,
]));

$tpl->assign('cotisation', $co);
$tpl->assign('order', qg('o') ?: 'date');
$tpl->assign('desc', null === qg('a'));
$tpl->assign('liste', $m_cotisations->listMembersForCotisation(
	$co->id, $categories, $page, qg('o'), null !== qg('a') ? false : true));

$tpl->display('admin/membres/cotisations/voir.tpl');


>

|

|

|

>
|
<
|
<

|
|
|
>

>
>
|
|
|
>
|
|
<
<
<
<
|
<
<
<
<
<

|
<
<
<
<

|
1
2
3
4
5
6
7
8
9
10
11
12

13

14
15
16
17
18
19
20
21
22
23
24
25
26
27




28





29
30




31
32
<?php
namespace Garradin;
use Garradin\Services\Services;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ACCES);

$service = Services::get((int) qg('id'));

if (!$service) {
	throw new UserException("Cette activité n'existe pas");

}


$type = qg('type');

if ('unpaid' == $type) {
	$list = $service->unpaidUsersList();
}
elseif ('expired' == $type) {
	$list = $service->expiredUsersList();
}
else {
	$type = 'paid';
	$list = $service->paidUsersList();
}





$list->loadFromQueryString();






$tpl->assign(compact('list', 'service', 'type'));





$tpl->display('services/details.tpl');

Added src/www/admin/services/edit.php version [2bc2174d1a].







































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace Garradin;

use Garradin\Services\Services;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

$service = Services::get((int) qg('id'));

if (!$service) {
	throw new UserException("Cette activité n'existe pas");
}

$csrf_key = 'service_edit_' . $service->id();

$form->runIf('save', function () use ($service) {
	$service->importForm();
	$service->save();
}, $csrf_key, ADMIN_URL . 'services/');

if ($service->duration) {
	$period = 1;
}
elseif ($service->start_date) {
	$period = 2;
}
else {
	$period = 0;
}

$tpl->assign(compact('service', 'period', 'csrf_key'));

$tpl->display('services/edit.tpl');

Added src/www/admin/services/fees/delete.php version [810abad666].

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
namespace Garradin;

use Garradin\Services\Fees;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

$fee = Fees::get((int) qg('id'));

if (!$fee) {
	throw new UserException("Ce tarif n'existe pas");
}

$csrf_key = 'fee_delete_' . $fee->id();

$form->runIf('delete', function () use ($fee) {
	$fee->delete();
}, $csrf_key, ADMIN_URL . 'services/fees/?id=' . $fee->id_service);

$tpl->assign(compact('fee', 'csrf_key'));

$tpl->display('services/fees/delete.tpl');

Added src/www/admin/services/fees/details.php version [fb9b7b8df1].





































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
namespace Garradin;
use Garradin\Services\Fees;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ACCES);

$fee = Fees::get((int) qg('id'));

if (!$fee) {
	throw new UserException("Ce tarif n'existe pas");
}

$type = qg('type');

if ('unpaid' == $type) {
	$list = $fee->unpaidUsersList();
}
elseif ('expired' == $type) {
	$list = $fee->expiredUsersList();
}
else {
	$type = 'paid';
	$list = $fee->paidUsersList();
}

$list->loadFromQueryString();

$service = $fee->service();

$tpl->assign(compact('list', 'fee', 'type', 'service'));

$tpl->display('services/fees/details.tpl');

Added src/www/admin/services/fees/edit.php version [30c90681af].

























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
namespace Garradin;

use Garradin\Services\Fees;
use Garradin\Accounting\Accounts;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

$fee = Fees::get((int) qg('id'));

if (!$fee) {
	throw new UserException("Ce tarif n'existe pas");
}

$service = $fee->service();
$csrf_key = 'fee_edit_' . $fee->id();

$form->runIf('save', function () use ($fee) {
	$fee->importForm();
	$fee->save();
}, $csrf_key, ADMIN_URL . 'services/fees/?id=' . $service->id());

if ($fee->amount) {
	$amount_type = 1;
}
elseif ($fee->formula) {
	$amount_type = 2;
}
else {
	$amount_type = 0;
}

$accounting_enabled = (bool) $fee->id_account;

$years = Years::listOpen();

$account = $fee->id_account ? [$fee->id_account => Accounts::getSelectorLabel($fee->id_account)] : null;

$tpl->assign(compact('service', 'amount_type', 'fee', 'csrf_key', 'account', 'accounting_enabled', 'years'));

$tpl->display('services/fees/edit.tpl');

Added src/www/admin/services/fees/index.php version [85376d6574].





































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Accounting\Years;
use Garradin\Entities\Services\Fee;
use Garradin\Services\Services;

require_once __DIR__ . '/../_inc.php';

$service = Services::get((int)qg('id'));

if (!$service) {
	throw new UserException("Cette activité n'existe pas");
}

$fees = $service->fees();

$form->runIf($session->canAccess('membres', Membres::DROIT_ADMIN) && f('save'), function () use ($service) {
	$fee = new Fee;
	$fee->id_service = $service->id();
	$fee->importForm();
	$fee->save();
}, 'fee_add', ADMIN_URL . 'services/fees/?id=' . $service->id());

$targets = Account::TYPE_REVENUE;

$accounting_enabled = false;
$years = Years::listOpen();

$tpl->assign(compact('service', 'targets', 'accounting_enabled', 'years'));
$tpl->assign('list', $fees->listWithStats());

$tpl->display('services/fees/index.tpl');

Modified src/www/admin/services/index.php from [960a734ef2] to [8524cc3af6].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
namespace Garradin;

require_once __DIR__ . '/../_inc.php';

$cotisations = new Cotisations;

if ($session->canAccess('membres', Membres::DROIT_ADMIN))
{
	$cats = new Compta\Categories;

	if (f('save'))
	{
		$form->check('new_cotisation', [
			'intitule'            => 'required|string',
			'montant'             => 'required|money',
			'periodicite'         => 'required|in:jours,date,ponctuel',
			'duree'               => 'required_if:periodicite,jours|numeric|min:0',
			'debut'               => 'required_if:periodicite,date|date_format:Y-m-d',
			'fin'                 => 'required_if:periodicite,date|date_format:Y-m-d',
			'categorie'           => 'boolean',
			'id_categorie_compta' => 'required_if:categorie,1|numeric|in_table:compta_categories,id',
		]);

		if (!$form->hasErrors())
		{
			try {
				$duree = f('periodicite') == 'jours' ? (int) f('duree') : null;
				$debut = f('periodicite') == 'date' ? f('debut') : null;
				$fin = f('periodicite') == 'date' ? f('fin') : null;
				$id_cat = f('categorie') ? (int) f('id_categorie_compta') : null;

				$cotisations->add([
					'intitule'          =>  f('intitule'),
					'description'       =>  f('description'),
					'montant'           =>  (float) f('montant'),
					'duree'             =>  $duree,
					'debut'             =>  $debut,
					'fin'               =>  $fin,
					'id_categorie_compta'=> $id_cat,

				]);

				Utils::redirect(ADMIN_URL . 'membres/cotisations/');
			}
			catch (UserException $e)
			{
				$form->addError($e->getMessage());
			}
		}
	}

	$tpl->assign('categories', $cats->getList(Compta\Categories::RECETTES));
}

$tpl->assign('liste', $cotisations->listWithStats());

$tpl->display('admin/membres/cotisations/index.tpl');



<
|
<
|
<
|
<
|
<
|
<
<
<
<
<
<
<
<
<
<
|
<
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
>
|
|
|
<
<
<
<
<
<
<
|
<
|
|
|

|
1
2
3

4

5

6

7

8










9

10





11








12
13
14
15







16

17
18
19
20
21
<?php
namespace Garradin;


use Garradin\Entities\Services\Service;

use Garradin\Services\Services;



require_once __DIR__ . '/_inc.php';












$csrf_key = 'service_add';







$form->runIf($session->canAccess('membres', Membres::DROIT_ADMIN) && f('save'), function () {








	$service = new Service;
	$service->importForm();
	$service->save();
	Utils::redirect(ADMIN_URL . 'services/fees/?id=' . $service->id());







}, $csrf_key);


$tpl->assign(compact('csrf_key'));
$tpl->assign('list', Services::listWithStats());

$tpl->display('services/index.tpl');

Added src/www/admin/services/payment.php version [a8ad0d0fc0].













































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
namespace Garradin;

use Garradin\Services\Services_User;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ECRITURE);

$su = Services_User::get((int)qg('id'));

if (!$su) {
	throw new UserException("Cette inscription n'existe pas");
}

$user_name = (new Membres)->getNom($su->id_user);

$csrf_key = 'service_pay';

$form->runIf('save', function () use ($su, $session) {
	$su->addPayment($session->getUser()->id);

	if ($su->paid != (bool) f('paid')) {
		$su->paid = (bool) f('paid');
		$su->save();
	}

	Utils::redirect(ADMIN_URL . 'services/user.php?id=' . $su->id_user);
}, $csrf_key);

$types_details = Transaction::getTypesDetails();
$account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string;

$tpl->assign(compact('csrf_key', 'account_targets', 'user_name', 'su'));

$tpl->display('services/payment.tpl');

Modified src/www/admin/services/reminders/delete.php from [91fa0c34f6] to [6e29e0f367].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace Garradin;

require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

if (!qg('id') || !is_numeric(qg('id')))
{
    throw new UserException("Argument du numéro de rappel manquant.");

}

$rappels = new Rappels;

$rappel = $rappels->get(qg('id'));

if (!$rappel)
{
    throw new UserException("Ce rappel n'existe pas.");
}

if (f('delete') && $form->check('delete_rappel_' . $rappel->id))
{
    try {

        $rappels->delete($rappel->id, (bool) f('delete_history'));
        Utils::redirect(ADMIN_URL . 'membres/cotisations/gestion/rappels.php');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('rappel', $rappel);

$tpl->display('admin/membres/cotisations/gestion/rappel_supprimer.tpl');



|
|
<
|
<
|
<
>
|
|
<

|

|
<
|


|
|
<
>
|
|
|
<
<
<
<
<
|
<

|
1
2
3
4
5

6

7

8
9
10

11
12
13
14

15
16
17
18
19

20
21
22
23





24

25
26
<?php
namespace Garradin;

use Garradin\Entities\Services\Reminder;
use Garradin\Services\Reminders;

use Garradin\Services\Services;



require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);


$reminder = Reminders::get((int) qg('id'));

if (!$reminder) {

	throw new UserException("Ce rappel n'existe pas");
}

$csrf_key = 'reminder_delete_' . $reminder->id();


$form->runIf('delete', function () use ($reminder) {
	$reminder->delete();
}, $csrf_key, ADMIN_URL . 'services/reminders/');






$tpl->assign(compact('reminder', 'csrf_key'));


$tpl->display('services/reminders/delete.tpl');

Added src/www/admin/services/reminders/details.php version [160d90cac6].



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
namespace Garradin;

use Garradin\Entities\Services\Reminder;
use Garradin\Services\Reminders;
use Garradin\Services\Services;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

$reminder = Reminders::get((int) qg('id'));

if (!$reminder) {
	throw new UserException("Ce rappel n'existe pas");
}

$list = $reminder->sentList();
$list->loadFromQueryString();

$service = $reminder->service();

$tpl->assign(compact('list', 'reminder', 'service'));

$tpl->display('services/reminders/details.tpl');

Modified src/www/admin/services/reminders/edit.php from [dfb1636402] to [30e7c419dc].

1
2
3




4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41



42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
<?php
namespace Garradin;





require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

if (!qg('id') || !is_numeric(qg('id')))
{
    throw new UserException("Argument du numéro de rappel manquant.");
}

$rappels = new Rappels;

$rappel = $rappels->get(qg('id'));

if (!$rappel)
{
    throw new UserException("Ce rappel n'existe pas.");
}

$cotisations = new Cotisations;


if (f('save') && $form->check('edit_rappel_' . $rappel->id))



{
    try {
        if (f('delai_choix') == 0)
           $delai = 0;
        elseif (f('delai_choix') > 0)
            $delai = (int) f('delai_post');
        else
            $delai = -(int) f('delai_pre');

        $rappels->edit($rappel->id, [
            'sujet'         => f('sujet'),
            'texte'         => f('texte'),
            'delai'         => $delai,
            'id_cotisation' => f('id_cotisation'),
        ]);

        Utils::redirect(ADMIN_URL . 'membres/cotisations/gestion/rappels.php');



    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());

    }
}

$rappel->delai_pre = 
    $rappel->delai_post = (abs($rappel->delai) ?: 30);

$rappel->delai_choix = ($rappel->delai == 0) ? 0 : ($rappel->delai > 0 ? 1 : -1);

$tpl->assign('rappel', $rappel);
$tpl->assign('cotisations', $cotisations->listCurrent());

$tpl->display('admin/membres/cotisations/gestion/rappel_modifier.tpl');



>
>
>
>
|



<
<
<
<
|
<

<
<
|
<
|


<
>

|
>
>
>
|
<
<
|
<
<
<
<

|
<
<
|
<
|
|
<
>
>
>
|
|
<
<
>
|
|
|
<
<

<
|
<
<

|
1
2
3
4
5
6
7
8
9
10
11




12

13


14

15
16
17

18
19
20
21
22
23
24


25




26
27


28

29
30

31
32
33
34
35


36
37
38
39


40

41


42
43
<?php
namespace Garradin;

use Garradin\Entities\Services\Reminder;
use Garradin\Services\Reminders;
use Garradin\Services\Services;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);





$reminder = Reminders::get((int) qg('id'));




if (!$reminder) {

	throw new UserException("Ce rappel n'existe pas");
}


$csrf_key = 'reminder_edit_' . $reminder->id();

$form->runIf('save', function () use ($reminder) {
	$reminder->importForm();
	$reminder->save();
}, $csrf_key, ADMIN_URL . 'services/reminders/');



$delay_before = $delay_after = '';





if ($reminder->delay < 0) {


	$delay_type = 1;

	$delay_before = abs($reminder->delay);
}

elseif ($reminder->delay > 0) {
	$delay_type = 2;
	$delay_after = abs($reminder->delay);
}
else {


	$delay_type = 0;
}

$services_list = Services::listAssoc();




$tpl->assign(compact('delay_type', 'delay_before', 'delay_after', 'reminder', 'csrf_key', 'services_list'));



$tpl->display('services/reminders/edit.tpl');

Modified src/www/admin/services/reminders/index.php from [d8c9502290] to [98a8dbe0ad].

1
2
3




4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33





34
35
36
37

38
39
40
41
42
43

44

<?php
namespace Garradin;





require_once __DIR__ . '/../../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);

$rappels = new Rappels;
$cotisations = new Cotisations;

if (f('save') && $form->check('new_rappel'))
{
    try {
        if (f('delai_choix') == 0)
    	   $delai = 0;
        elseif (f('delai_choix') > 0)
            $delai = (int) f('delai_post');
        else
            $delai = -(int) f('delai_pre');

        $rappels->add([
            'sujet'         => f('sujet'),
            'texte'         => f('texte'),
            'delai'         => $delai,
            'id_cotisation' => f('id_cotisation'),
        ]);

        Utils::redirect(ADMIN_URL . 'membres/cotisations/gestion/rappels.php');
    }
    catch (UserException $e)
    {
        $form->addError($e->getMessage());
    }





}

$tpl->assign('liste', $rappels->listByCotisation());
$tpl->assign('cotisations', $cotisations->listCurrent());


$tpl->assign('default_subject', '[#NOM_ASSO] Échéance de cotisation');
$tpl->assign('default_text', "Bonjour #IDENTITE,\n\nVotre cotisation arrive à échéance dans #NB_JOURS jours.\n\n"
	.	"Merci de nous contacter pour renouveler votre cotisation.\n\nCordialement.\n\n"
    .   "--\n#NOM_ASSO\n#ADRESSE_ASSO\nE-Mail : #EMAIL_ASSO\nSite web : #SITE_ASSO");


$tpl->display('admin/membres/cotisations/gestion/rappels.tpl');




>
>
>
>
|



<
<
|
<
|
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
|
|
<
|
|
|
>
>
>
>
>
|
|
<
<
>

|
|
|
|

>
|
>
1
2
3
4
5
6
7
8
9
10
11


12

13







14







15
16

17
18
19
20
21
22
23
24
25
26


27
28
29
30
31
32
33
34
35
36
<?php
namespace Garradin;

use Garradin\Entities\Services\Reminder;
use Garradin\Services\Reminders;
use Garradin\Services\Services;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('membres', Membres::DROIT_ADMIN);



$services_list = Services::listAssoc();









if (!count($services_list)) {







	Utils::redirect(ADMIN_URL . 'services/?CREATE');
}


$csrf_key = 'reminder_add';

$form->runIf('save', function () {
	$reminder = new Reminder;
	$reminder->importForm();
	$reminder->save();
}, $csrf_key, Utils::getSelfURL());

$list = Reminders::list();


$services_list = Services::listAssoc();

$default_subject = '[#NOM_ASSO] Échéance de cotisation';
$default_body = "Bonjour #IDENTITE,\n\nVotre cotisation arrive à échéance dans #NB_JOURS jours.\n\n"
	.   "Merci de nous contacter pour renouveler votre cotisation.\n\nCordialement.\n\n"
	.   "--\n#NOM_ASSO\n#ADRESSE_ASSO\nE-Mail : #EMAIL_ASSO\nSite web : #SITE_ASSO";

$tpl->assign(compact('csrf_key', 'list', 'services_list', 'default_subject', 'default_body'));

$tpl->display('services/reminders/index.tpl');

Added src/www/admin/services/save.php version [9b5a2bdf52].











































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
namespace Garradin;

use Garradin\Services\Services;
use Garradin\Entities\Services\Service_User;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ECRITURE);

$user_id = qg('user');

if (!$user_id && ($user_id = f('user'))) {
	$user_id = @key($user_id);
}

if (!$user_id) {
	$user_id = f('id_user');
}

$user_id = (int) $user_id ?: null;
$user_name = $user_id ? (new Membres)->getNom($user_id) : null;

if (!$user_name) {
	$user_id = null;
}

$grouped_services = Services::listGroupedWithFees($user_id);

if (!count($grouped_services)) {
	Utils::redirect(ADMIN_URL . 'services/?CREATE');
}

$csrf_key = 'service_save';

$form->runIf('save', function () use ($session) {
	$su = Service_User::saveFromForm($session->getUser()->id);

	Utils::redirect(ADMIN_URL . 'services/user.php?id=' . $su->id_user);
}, $csrf_key);

$selected_user = $user_name ? [$user_id => $user_name] : null;

$types_details = Transaction::getTypesDetails();
$account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string;

$today = new \DateTime;

$tpl->assign(compact('today', 'grouped_services', 'csrf_key', 'selected_user', 'account_targets', 'user_name', 'user_id'));

$tpl->display('services/save.tpl');

Added src/www/admin/services/user.php version [e3055067d1].



































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
namespace Garradin;
use Garradin\Services\Services_User;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ACCES);

$user = (new Membres)->get((int) qg('id'));

if (!$user) {
	throw new UserException("Cet utilisateur est introuvable");
}

$form->runIf($session->canAccess('membres', Membres::DROIT_ECRITURE) && null !== qg('paid') && qg('su_id'), function () use ($user) {
	$su = Services_User::get((int) qg('su_id'));

	if (!$su) {
		throw new UserException("Cette inscription est introuvable");
	}

	$su->paid = (bool)qg('paid');
	$su->save();
}, null, ADMIN_URL . 'services/user.php?id=' . $user->id);

$list = Services_User::perUserList($user->id);
$list->setTitle(sprintf('Inscriptions — %s', $user->identite));
$list->loadFromQueryString();

$tpl->assign('services', Services_User::listDistinctForUser($user->id));
$tpl->assign(compact('list', 'user'));

$tpl->display('services/user.tpl');

Added src/www/admin/services/user_delete.php version [c5d8584c97].



















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
namespace Garradin;

use Garradin\Services\Services_User;

require_once __DIR__ . '/_inc.php';

$session->requireAccess('membres', Membres::DROIT_ECRITURE);

$su = Services_User::get((int) qg('id'));

if (!$su) {
	throw new UserException("Cette inscription n'existe pas");
}

$csrf_key = 'su_delete_' . $su->id();
$user_id = $su->id_user;

$form->runIf('delete', function () use ($su) {
	$su->delete();
}, $csrf_key, ADMIN_URL . 'services/user.php?id=' . $user_id);

$tpl->assign(compact('csrf_key'));

$tpl->display('services/user_delete.tpl');

Modified src/www/admin/static/admin.css from [4d61ee3dc8] to [18c59b252b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
@charset "UTF-8";

@font-face {
    font-family: 'gicon';
    src: url('font/garradin.eot?2018');
    src: url('font/garradin.eot?2018#iefix') format('embedded-opentype'),
        url('font/garradin.woff?2018') format('woff'),
        url('font/garradin.ttf?2018') format('truetype'),
        url('font/garradin.svg?2018#garradin') format('svg');
    font-weight: normal;
    font-style: normal;
}

body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, 
pre, form, fieldset, input, textarea, p, blockquote, th, td,
figure, article, aside, section, header, footer { 
    padding: 0;
    margin: 0;
}
fieldset, img { 
    border: 0;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}
ol, ul {
    list-style: none;
}
caption, th {
    text-align: left;
}
article, aside, figure, section, header, footer, main { display: block; }

span { display: inline-block; }

h1  { font-size: 2em; }
h2  { font-size: 1.5em; }
h3  { font-size: 1.2em; }
h4  { font-size: 1em; }
h5  { font-size: 0.9em; }
h6  { font-size: 0.8em; }

/*
    marron : #9c4f15 rgb(156, 79, 21)
    orange : #d98628 rgb(217, 134, 40)
*/

:root {
    --gBgColor: 255, 255, 255;
    --gMainColor: 156, 79, 21;
    --gSecondColor: 217, 134, 40;
}

html {
    width: 100%;
    height: 100%;
}

body {
    font-size: 100%;
    color: #000;
    font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif;
    padding-bottom: 1em;
    background: #fff url("gdin_bg.png") no-repeat 0px 0px fixed;
    background-color: rgb(var(--gBgColor));
    /* Pas possible d'utiliser une variable CSS pour l'image de fond, c'est bugué dans Chrome
    cf. https://bugs.chromium.org/p/chromium/issues/detail?id=618165 */
}

body#popup {
    background-position: -170px 0px;
}

body#transparent {
    background: transparent;
}

.header {
    color: #fff;
    color: rgb(var(--gBgColor));
}

.header h1 {
    color: #9c4f15;
    color: rgb(var(--gMainColor));
    margin-left: 180px;
    margin-bottom: 0.4em;
}

.header .menu {
    position: fixed;
    overflow: auto;
    z-index: 10000;
    width: 170px;
    top: 0;
    bottom: 0;
    padding-top: 100px;
    background: #9c4f15 url("gdin_bg.png") no-repeat 0px 0px;
    background-color: rgb(var(--gMainColor));
}

.header .menu::-webkit-scrollbar {
    width: 8px;
    background: rgba(255, 255, 255, 0.25);
    box-shadow: inset 0px 0px 10px #666;
}

.header .menu::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.5);
    border-radius: 10px;
}

.header .menu i {
    font-style: normal;
}

.header .menu a {
    color: #fff;
    color: rgb(var(--gBgColor));
    font-weight: bold;
    padding: 0.4em 0.4em 0.4em 1em;
    display: block;
    text-decoration: none;
    transition: background .3s;
}

.header .menu a:hover {
    text-decoration: underline;
    background: rgb(217, 134, 40);
    background: rgba(217, 134, 40, 0.5);
    background: rgba(var(--gSecondColor), 0.5);
}

.header .menu li li a {
    font-size: 0.8em;
    padding-left: 2em;
}

.header .menu li.current > a {
    background: #fff;
    background: rgb(var(--gBgColor));
    color: rgb(156, 79, 21);
    color: rgb(var(--gMainColor));
}

.header .menu a b {
    float: right;
    text-decoration: none;
    margin-top: -.2em;
    font-size: 20pt;
    color: rgb(70, 70, 70);
    color: rgba(255, 255, 255, .5);
}

.header .menu li.current > a b {
    color: rgb(217, 134, 40);
    color: rgba(217, 134, 40, 0.5);
    color: rgba(var(--gSecondColor), 0.5);
}

main {
    margin: 0px 1em 1em 180px;
    position: relative;
}

main img {
    max-width: 100%;
}

body#popup main {
    margin: 1em 1em 1em 2.5em;
}

body#transparent main {
    margin: 0;
    padding: .2em;
}

span.error, b.error {
    color: #900;
}

span.confirm, b.confirm {
    color: #090;
}

span.alert, b.alert {
    color: #990;
}

.alert p, .error p, .confirm p {
    margin-bottom: .8em;
}

p.alert, div.alert, p.error, div.error, p.confirm, div.confirm {
    border: 1px solid #ccc;
    padding: .5em;
    margin-bottom: 1em;
    border-radius: .3em;
    padding-left: 3em;
    position: relative;
}

p.error, div.error {
    border-color: #c00;
    background-color: #fcc;
}

p.confirm, div.confirm {
    border-color: #0c0;
    background-color: #cfc;
}

p.alert, div.alert {
    border-color: #cc0;
    background-color: #ffc;
}

p.confirm::before, div.confirm::before, p.alert::before, div.alert::before, p.error::before, div.error::before {
    font-family: "gicon";
    left: .5em;
    top: .2em;
    position: absolute;
    font-size: 1.5em;
    text-shadow: 2px 2px 5px #666;
}

p.confirm::before, div.confirm::before {
    content: "☑";
    color: green;
}

p.alert::before, div.alert::before {
    content: "⚠";
    color: yellow;
}

p.error::before, div.error::before {
    content: "⚠";
    color: red;
}

p.help {
    margin: 1em;
    color: #666;
}

p.intro {
    margin: 1em;
}

.error ul, .alert ul, .confirm ul {
    margin-left: 1.5em;
    list-style: disc;
}

/* Formulaires */
fieldset {
    border: 1px solid #ccc;
    padding: 0.8em 1em 0 1em;
    margin-bottom: 1em;
    padding: 0.5em;
}

fieldset legend {
    padding: 0 0.5em;
    font-weight: bold;
    color: #000;
}

label:hover {
    cursor: pointer;
    border-bottom: 1px dotted #900;
}

dl dt label {
    font-weight: bold;
}

fieldset dl dt b {
    color: #900;
    font-size: 0.7em;
    font-weight: normal;
    vertical-align: super;
}

fieldset dl dd.tip {
    color: #666;
}

fieldset dl dd {
    padding: 0.2em 0.5em 0.2em 1em;
}

fieldset dl dd ol, fieldset dl dd ul {
    margin-left: 1.5em;
}

fieldset dl dl {
    margin: .5em 0 .5em 1.2em;
}

input[type=text], textarea, input[type=password], input[type=email],
input[type=url], input[type=tel], select {
    padding: 0.2em 0.4em;
    font-family: Sans-serif;
    min-width: 20em;
    max-width: 100%;
}

input[type=password], input.clearTextPassword {
    font-family: monospace;
}

select, input[size] {
    min-width: 0;
}

input.time {
    text-align: center;
    padding: .2em 0;
}

input[type=number], input[type=date] {
    padding: 0.2em 0.4em;
    font-family: Sans-serif;
    min-width: 2em;
}

input[type=submit], input[type=button], input[type=checkbox], button {
    padding: 0.3em;
    cursor: pointer;
    transition: opacity .5s ease;
}

input.resetButton {
    padding: .1em;
    margin-left: 1em;
}

input[type=button].showPassword {
    position: absolute;
    margin-left: -2em;
    margin-right: 1em;
    background: none;
    margin-top: .1em;
}

input[type=button].showPassword:hover {
    background: none;
}

.loader {
    width: 100%;
    min-height: 32px;
    display: block;
    position: relative;
}

.loader.install {
    margin-top: -40px;
}

.loader b {
    text-shadow: 2px 2px 5px #999;
    background: rgb(255, 255, 255);
    background: rgba(255, 255, 255, 0.5);
    border-radius: .5em;
    font-size: 16px;
    line-height: 16px;
    height: 16px;
    z-index: 9999;
    position: absolute;
    display: block;
    left: 10px;
    top: 10px;
    padding: .2em;
}

.loader img {
    position: absolute;
    opacity: 0;
    transition: all 0.5s ease;
    z-index: 2;
}

select.large {
    width: 95%;
}

select.large .niveau_1 {
    background: #333;
    color: #fff;
    font-style: normal;
    font-size: 1.2em;
}

select.large optgroup.niveau_2 {
    background: #666;
    color: #fff;
    font-style: normal;
    padding-left: 1em;
}

select.large option {
    background: #fff;
    color: #000;
}

select.large .niveau_2 { font-style: italic; background: #eee; }
select.large .niveau_3 { padding-left: 1em; font-weight: bold; }
select.large .niveau_4 { padding-left: 2em; }
select.large .niveau_5 { padding-left: 3em; }
select.large .niveau_6 { padding-left: 4em; }

p.submit {
    margin: 1em;
}

.submit input[type=submit] {
    font-size: 1.2em;
}

.submit input.minor {
    font-size: .9em;
}

form .checkUncheck {
    float: left;
}

form span.password_check {
    margin-left: 1em;
    padding: .1em .3em;
    border-radius: .5em;
}

form span.password_check.fail { background-color: #f99; }
form span.password_check.weak { background-color: #ff9; }
form span.password_check.medium { background-color: #ccf; }
form span.password_check.ok { background-color: #cfc; }

dd.help input[type=text] {
    cursor: pointer;
    padding: 0;
    font-family: monospace;
}

form p.actions {
    float: right;
}

ul.actions {
    list-style-type: none;
    margin: 1em 0;
    border-bottom: .1em solid #9c4f15;
    border-bottom-color: rgb(var(--gMainColor));
    padding: 0 1em;
    z-index: 100;
}

ul.actions.sub {
    margin: -1em 2em 1em 0;
    padding-top: 1em;
    border-right: .1em solid #9c4f15;
    border-right-color: rgb(var(--gMainColor));
    border-bottom-right-radius: .5em;
}

ul.actions li {
    display: inline-block;
    margin: 0 0.2em;
}

ul.actions li a, ul.actions li label {
    display: inline-block;
    background: rgb(217, 134, 40);
    background: rgba(217, 134, 40, .5);
    background: rgba(var(--gSecondColor), .5);
    border-radius: .5em .5em 0 0;
    padding: .1em .5em;
    color: #000;
    text-decoration: none;
    transition: background-color .2s, color .2s;
}

ul.actions li input {
    display: none;
}

ul.actions li.current a, ul.actions li input:checked + label {
    background: #9c4f15;
    background: rgb(var(--gMainColor));
    color: #fff;
    color: rgb(var(--gBgColor));
}

ul.actions li a:hover, ul.actions li label:hover {
    color: #fff;
    background-color: rgb(var(--gMainColor));
    text-decoration: underline;
    border-bottom: none;
}

h3.warning {
    margin: 1em;
    color: red;
}

dd.help, small.help {
    color: #666;
}

ul.gallery {
    text-align: center;
}

ul.gallery li {
    display: inline-block;
    margin: .3em;
    vertical-align: middle;
    width: 150px;
}

ul.gallery li img {
    max-width: 150px;
    max-height: 150px;
}

ul.gallery .actions {
    text-align: center;
    z-index: 100;
}

table.list {
    margin-bottom: 1em;
    width: 100%;
}

table.list.auto {
    width: auto;
}

table.list table {
    margin: 0;
}

table.list th {
    text-align: left;
    font-weight: bold;
}

table.list thead {
    background: rgb(217, 134, 40);
    background: rgba(217, 134, 40, 0.5);
    background: rgba(var(--gSecondColor), 0.5);
}

table.list tfoot tr {
    background: rgb(247, 164, 70);
    background: rgba(217, 134, 40, 0.1);
    background: rgba(var(--gSecondColor), 0.2);
    color: rgb(156, 79, 21);
    color: rgb(var(--gMainColor));
}

table.list th, table.list td {
    padding: 0.2em 0.5em;
}

table.list tr {
    border: 1px solid rgb(217, 134, 40);
    border-color: rgba(var(--gSecondColor), 0.5);
    transition: background .5s
}

table.list tr:nth-child(even) {
    background: rgb(255, 174, 80);
    background: rgba(217, 134, 40, 0.2);
    background: rgba(var(--gSecondColor), 0.2);
}

table.list.multi tr:nth-child(even) {
    background: inherit;
}

table.list.multi tr:nth-child(4n+1), table.list.multi tr:nth-child(4n+2) {
    background: rgb(255, 174, 80);
    background: rgba(217, 134, 40, 0.2);
    background: rgba(var(--gSecondColor), 0.2);
}

table.list tr.checked {
    color: #633;
    background: #ffc;
}

table.list .error {
    color: red;
    font-weight: bold;
}

table.list .alert {
    color: darkred;
    font-weight: bold;
}

table.list .confirm {
    color: darkgreen;
}

table.list .num {
    text-align: center;
}

table.list .check {
    width: 1%;
}

table.search th {
    background: rgb(217, 134, 40);
    background: rgba(217, 134, 40, 0.5);
    background: rgba(var(--gSecondColor), 0.5);
}

table.list .disabled {
    background: #eee;
    color: #999;
}

#queryBuilder .column select, #queryBuilderForm .actions select {
    max-width: 15em;
}

#queryBuilder table td {
    vertical-align: top;
    padding: .1em .2em;
}

#queryBuilder table td.buttons {
    white-space: nowrap;
}

#queryBuilder input[type=button], #queryBuilder .values input {
    margin: .1em;
}

#queryBuilderForm .actions label {
    margin: 0 .5em;
}

#queryBuilder table .values label {
    margin: 0 .3em;
}

#queryBuilderForm input[type=number] {
    width: 4em;
}

.userOrder .cur {
    background: rgb(217, 134, 40);
    background: rgba(var(--gSecondColor), 1.0);
    color: #fff;
    color: rgb(var(--gBgColor));
}

.userOrder td, .userOrder th {
    position: relative;
}

.userOrder .icn {
    float: left;
    clear: left;
    color: #9c4f15;
    color: rgb(var(--gMainColor));
    text-decoration: none;
    font-size: 12pt;
    line-height: 6pt;
    width: 12pt;
    height: 8pt;
    vertical-align: middle;
    font-weight: normal;
    text-shadow: 0px 0px 1px #fff;
    text-shadow: 0px 0px 1px rgb(var(--gBgColor));
}

thead .icn:hover {
    color: darkred;
    text-shadow: none;
}

thead .cur.desc .icn.dn, thead .cur.asc .icn.up {
    color: #fff;
    text-shadow: none;
}

table.list .actions {
    text-align: right;
}

b.money {
    font-weight: inherit;
    white-space: pre;
}

#rapport h3 {
    text-align: center;
    margin-bottom: .5em;
}

#rapport table {
    width: 100%;
}

#rapport tr {
    vertical-align: top;
}

#rapport table table {
    border: 1px solid rgb(217, 134, 40);
    border-color: rgba(217, 134, 40, 0.5);
    border-color: rgba(var(--gSecondColor), 0.5);
}

#rapport table table tr th {
    width: 80%;
}

#rapport td, #rapport th {
    padding: 0.2em 0.5em;
    text-align: left;
}

#rapport td.money, #rapport th.money {
    text-align: right;
}

#rapport .compte th {
    font-weight: normal;
}

#rapport table table td {
    text-align: right;
}

#rapport .parent {
    font-weight: bold;
    background: rgb(247, 164, 70);
    background: rgba(217, 134, 40, 0.2);
    background: rgba(var(--gSecondColor), 0.2);
}

#rapport table table tfoot tr {
    background: rgb(247, 164, 70);
    background: rgba(217, 134, 40, 0.1);
    background: rgba(var(--gSecondColor), 0.1);
    color: rgb(156, 79, 21);
    color: rgb(var(--gMainColor));
}

#rapport .exercice {
    text-align: center;
    margin-bottom: .8em;
    padding-bottom: .5em;
    border-bottom: 1pt solid #999;
}

#rapport h1 {
    text-align: center;
}

h2.ruler {
    margin: .5em;
    text-align: center;
    color: #333;
    overflow: hidden;
}

h2.ruler:before, h2.ruler:after {
    background-color: #000;
    content: "";
    display: inline-block;
    height: 1px;
    position: relative;
    vertical-align: middle;
    width: 50%;
}

h2.ruler:before {
    right: 0.5em;
    margin-left: -50%;
}

h2.ruler:after {
    left: 0.5em;
    margin-right: -50%;
}

.icn, .icnl {
    font-family: "gicon", sans-serif;
    font-style: normal;
    font-weight: normal;
    speak: none;
    font-variant: normal;
    text-transform: none;
    position: relative;
}

.actions .icn, .icn.action {
    text-decoration: none;
    border-radius: 1em;
    display: inline-block;
    text-align: center;
    font-size: 1.2em;
    line-height: .8em;
    vertical-align: middle;
    padding: .2em;
    font-family: "gicon", sans-serif;
    color: #9c4f15;
    color: rgb(var(--gMainColor));
    text-shadow: 1px 1px 1px #999;
    border: none;
    cursor: pointer;
    position: relative;
    z-index: 200;
}

a.icn[title]:after {
    display: none;
    position: absolute;
    content: attr(title);
    font-size: 10pt;
    border-radius: .2em;
    top: 0;
    bottom: 0;
    right: 0;
    z-index: -1;
    padding: .3em .4em;
    background: #ff9;
    text-shadow: none;
    white-space: pre;
    box-shadow: 0px 0px 2px #000;
    text-decoration: underline;
    padding-right: 2em;
}

dl dd.actions a.icn[title]:after {
    right: auto;
    left: 0;
    padding-left: 2em;
    padding-right: .4em;
}

a.icn[title]:hover:after {
    display: inline-block;
}

.num a {
    text-decoration: none;
    border-radius: .5em;
    display: inline-block;
    text-align: center;
    padding: 0 .2em;
    background: rgb(247, 164, 70);
    background: rgba(217, 134, 40, 0.5);
    background: rgba(var(--gMainColor), 0.5);
}

.actions .icn:hover, .num a:hover, .icn.action:hover {
    color: darkred;
    background: #ff9;
    z-index: 300;
}

.droits b {
    border: 2px solid #999;
    border-radius: 1em;
    color: #000;
    background: #ccc;
    width: 16px;
    display: inline-block;
    text-align: center;
    font-size: 0.8em;
    cursor: help;
    vertical-align: middle;
    position: relative;
    z-index: 10;
    font-family: "gicon", "Trebuchet MS", Arial, Helvetica, sans-serif;
}

.droits b.aucun {
    border-color: #ccc;
    background: #eee;
    color: #999;
}

.droits b.acces {
    border-color: #cc9;
    color: #660;
    background: #ffe;
}

.droits b.ecriture {
    border-color: #9c9;
    color: #060;
    background: #efe;
}

.droits b.aucun:before {
    content: "X";
    position: absolute;
    left: 0;
    right: 0;
    top: -3px;
    color: #ccc;
    z-index: -1;
    font-size: 1.5em;
    overflow: hidden;
}

.droits b.admin {
    color: #900;
    border-color: #c99;
    background: #fee;
}

.infos {
    margin-bottom: 1em;
}

.infos h3 {
    margin-bottom: 0.5em;
}

.infos p {
    margin-bottom: 0.8em;
}

.infos dl {
    margin-bottom: 0.8em;
}

.infos dl dd {
    margin: 0.2em 1em;
}

.shortFormRight {
    width: 30em;
    float: right;
    font-size: 80%;
    text-align: center;
    margin-left: 1em;
}

.shortFormLeft {
    font-size: 80%;
}

.shortFormLeft .special {
    display: none;
}

.shortFormRight p.submit {
    margin-top: -2em;
    float: right;
}

.memberList {
    clear: both;
}

.pagination {
    clear: both;
    list-style-type: none;
    padding: 0.4em 0;
    text-align: center;
}

.pagination li {
    display: inline-block;
    margin: 0 0.3em;
}

.pagination li.current {
    font-size: 1.3em;
}

.pagination li a {
    color: #000;
}

fieldset.memberMessage {
    max-width: 30em;
}

fieldset.memberMessage #f_sujet, fieldset.memberMessage #f_message {
    width: 95%;
}

.templatesList ul {
    margin: 1em 2em;
}

.catList dt {
    font-size: 1.2em;
    font-weight: bold;
}

.catList dd.desc {
    color: #666;
    margin: .2em 0 .2em 2em;
}

.catList dd.compte {
    color: #9c4f15;
    color: rgb(var(--gMainColor));
    margin: .2em 0 .2em 2em;
}

.catList dd.actions {
    margin: .2em 0 1em 2em;
}

ul.accountList {
    list-style-type: square;
    margin-left: 2em;
}

ul.accountList > li > h4 {
    font-weight: normal;
    font-size: 1.2em;
}

ul.accountList > li {
    margin-bottom: .8em;
}

table.accountList .niveau_2 .libelle {
    font-weight: bold;
}

table.accountList .niveau_3 .libelle { padding-left: 1em; }
table.accountList .niveau_4 .libelle { padding-left: 2em; }
table.accountList .niveau_5 .libelle { padding-left: 3em; }
table.accountList .niveau_6 .libelle { padding-left: 4em; }

table.rib { display: inline-table; vertical-align: middle; font-size: .9em; text-align: center; border-collapse: collapse; }
table.rib th, table.rib td { padding: .1em .3em; border: 1px solid #ccc; }

dl.describe {
    margin-bottom: 1em;
}

dl.describe > dt {
    font-weight: bold;
    width: 15em;
    float: left;
    clear: left;
    margin-bottom: .5em;
}

dl.describe > dd {
    margin-bottom: .5em;
    float: left;
}

dl.describe ul {
    margin-left: 1.5em;
    list-style-type: disc;
}

dl.cotisation {
    background: rgb(255, 174, 80);
    background: rgba(217, 134, 40, 0.2);
    background: rgba(var(--gSecondColor), 0.2);
    padding: .5em;
    border-radius: .5em;
    margin: 1em;
}

dl.cotisation dt {
    font-weight: bold;
}

dl.cotisation dd {
    margin: .2em 0 .4em 1em;
}

aside.describe {
    width: 20em;
    float: right;
    margin: .5em;
    background: rgb(255, 174, 80);
    background: rgba(217, 134, 40, 0.2);
    background: rgba(var(--gSecondColor), 0.2);
    border-radius: .5em;
    border: 2px solid rgb(217, 134, 40);
    border-color: rgba(var(--gSecondColor), 0.5);
    padding: .5em;
    z-index: 200;
}

aside.describe dt {
    float: none;
}

#orderFields fieldset {
    position: relative;
    min-height: 2em;
    transition: all 1s;
    overflow: hidden;
    max-height: 5000px;
}

#orderFields fieldset legend {
    font-size: 1.2em;
    line-height: .8em;
    color: #666;
}

#orderFields fieldset .actions {
    display: block;
    position: absolute;
    top: 0.2em;
    right: 1em;
}

#orderFields fieldset .actions .icn {
    position: absolute;
}

#orderFields fieldset dl {
    overflow: hidden;
    transition: all .5s;
    opacity: 1;
    display: block;
    max-height: 5000px;
}

#orderFields fieldset dl.hidden {
    opacity: 0;
    max-height: 0;
}

#orderFields fieldset.removed {
    max-height: 0;
    opacity: 0;
    border-color: red;
    min-height: 0;
    height: 0;
}

#orderFields fieldset .actions .remove { right: 0em; }
#orderFields fieldset .actions .edit { right: 1.5em; }
#orderFields fieldset .actions .down { right: 3em; }
#orderFields fieldset .actions .up { right: 4.5em; }

#orderFields fieldset:nth-child(1) .actions .up, #orderFields fieldset:nth-last-child(1) .actions .down {
    display: none;
}

#orderFields fieldset .actions .icn {
    cursor: pointer;
}

#orderFields fieldset .interactive:hover {
    cursor: pointer;
    text-decoration: underline;
}

pre.sql_schema {
    float: right;
    color: #666;
    font-size: .9em;
    width: 30%;
    overflow: auto;
}

.hidden {
    display: none;
}

img.qrcode {
    float: right;
    padding: .5em;
    border: .5em solid #000;
    background: #fff;
}

.exercice button {
    margin: .5em;
    padding: .5em;
}

.exercice button b.icn {
    font-size: 2em;
    display: block;
}

.exercice button:hover {
    opacity: .7;
}
<
|
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

1










2

































































































































































































































































































3







4



































































































































































































































































































5




6






































































































































































































































































































7





8







































































































































































































































































































@import url("styles/00-reset.css");










@import url("styles/01-layout.css");

































































































































































































































































































@import url("styles/02-common.css");







@import url("styles/03-forms.css");



































































































































































































































































































@import url("styles/04-dialogs.css");




@import url("styles/05-navigation.css");






































































































































































































































































































@import url("styles/06-tables.css");





@import url("styles/10-accounting.css");






































































































































































































































































































Added src/www/admin/static/font/config.json version [cec9b888ae].





































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
{
  "name": "garradin",
  "css_prefix_text": "icn-",
  "css_use_suffix": false,
  "hinting": true,
  "units_per_em": 1000,
  "ascent": 850,
  "glyphs": [
    {
      "uid": "e45a3da2ebde8bc8e30a873f3bd51f30",
      "css": "eye",
      "code": 128065,
      "src": "elusive"
    },
    {
      "uid": "d218294e6f9f7191f6b0b3d1ff6239ff",
      "css": "eye-off",
      "code": 10539,
      "src": "elusive"
    },
    {
      "uid": "484363b699ea1b5c2f827fc0f62f4dca",
      "css": "search",
      "code": 128269,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M643 464Q643 361 569 288T393 214 216 288 143 464 216 641 393 714 569 641 643 464ZM929 929Q929 958 907 979T857 1000Q827 1000 807 979L616 788Q516 857 393 857 313 857 240 826T115 742 31 617 0 464 31 312 115 186 240 102 393 71 545 102 671 186 755 312 786 464Q786 587 717 687L908 878Q929 899 929 929Z",
        "width": 928.6
      },
      "search": [
        "search"
      ]
    },
    {
      "uid": "d9d608c26fff5d1d75dc959f185f034d",
      "css": "check",
      "code": 9745,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M786 519V696Q786 763 739 810T625 857H161Q94 857 47 810T0 696V232Q0 166 47 119T161 71H625Q660 71 690 85 699 89 700 98 702 108 695 114L668 142Q662 147 655 147 653 147 650 146 637 143 625 143H161Q124 143 98 169T71 232V696Q71 733 98 759T161 786H625Q662 786 688 759T714 696V555Q714 547 719 542L755 507Q761 501 768 501 771 501 775 503 786 507 786 519ZM915 246L460 700Q447 714 429 714T397 700L157 460Q143 447 143 429T157 397L218 335Q232 322 250 322T282 335L429 482 790 121Q803 108 821 108T853 121L915 182Q928 196 928 214T915 246Z",
        "width": 928.6
      },
      "search": [
        "check"
      ]
    },
    {
      "uid": "67dabb31f430a26bd27c25db8daa105b",
      "css": "user",
      "code": 128100,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M786 784Q786 851 745 890T637 929H149Q82 929 41 890T0 784Q0 754 2 726T10 665 25 605 49 550 83 505 131 475 193 464Q198 464 217 476T258 503 318 530 393 542 467 530 528 503 569 476 593 464Q627 464 655 475T703 505 737 550 761 605 776 665 784 726 786 784ZM607 286Q607 374 544 437T393 500 241 437 179 286 241 134 393 71 544 134 607 286Z",
        "width": 785.7
      },
      "search": [
        "user"
      ]
    },
    {
      "uid": "9f9457b7ef8733c515e852b738277774",
      "css": "users",
      "code": 128106,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M331 500Q241 503 183 571H108Q63 571 31 549T0 483Q0 286 69 286 73 286 94 297T148 321 214 333Q252 333 289 320 286 341 286 357 286 435 331 500ZM929 856Q929 922 888 961T780 1000H292Q224 1000 184 961T143 856Q143 826 145 798T153 737 167 676 191 622 226 577 274 547 336 536Q342 536 360 548T401 574 460 601 536 613 611 601 671 574 712 548 735 536Q770 536 798 547T845 577 880 622 904 676 919 737 927 798 929 856ZM357 143Q357 202 315 244T214 286 113 244 71 143 113 42 214 0 315 42 357 143ZM750 357Q750 446 687 509T536 571 384 509 321 357 384 206 536 143 687 206 750 357ZM1071 483Q1071 526 1040 549T963 571H888Q831 503 741 500 786 435 786 357 786 341 783 320 820 333 857 333 890 333 924 321T978 297 1002 286Q1071 286 1071 483ZM1000 143Q1000 202 958 244T857 286 756 244 714 143 756 42 857 0 958 42 1000 143Z",
        "width": 1071.4
      },
      "search": [
        "users"
      ]
    },
    {
      "uid": "13e9b9898958bd06a926490ec4c78b18",
      "css": "delete",
      "code": 10008,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M724 738Q724 760 709 776L633 852Q617 867 595 867T557 852L393 687 229 852Q213 867 191 867T153 852L77 776Q61 760 61 738T77 700L241 536 77 372Q61 356 61 334T77 296L153 220Q169 204 191 204T229 220L393 384 557 220Q573 204 595 204T633 220L709 296Q724 311 724 334T709 372L545 536 709 700Q724 715 724 738Z",
        "width": 785.7
      },
      "search": [
        "delete"
      ]
    },
    {
      "uid": "9856e9f63a26ab792b90c46148459ad7",
      "css": "plus",
      "code": 10133,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M786 411V518Q786 540 770 556T732 571H500V804Q500 826 484 841T446 857H339Q317 857 301 841T286 804V571H54Q31 571 16 556T0 518V411Q0 388 16 373T54 357H286V125Q286 103 301 87T339 71H446Q469 71 484 87T500 125V357H732Q754 357 770 373T786 411Z",
        "width": 785.7
      },
      "search": [
        "plus"
      ]
    },
    {
      "uid": "564500b6ab771051a66101066706e552",
      "css": "minus",
      "code": 10134,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M786 411V518Q786 540 770 556T732 571H54Q31 571 16 556T0 518V411Q0 388 16 373T54 357H732Q754 357 770 373T786 411Z",
        "width": 785.7
      },
      "search": [
        "minus"
      ]
    },
    {
      "uid": "ef59059d1776e2b5df2cdb61ad02a5c2",
      "css": "help",
      "code": 10067,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M393 701V835Q393 844 386 851T371 857H237Q228 857 221 851T214 835V701Q214 692 221 685T237 679H371Q379 679 386 685T393 701ZM569 366Q569 396 561 422T541 465 510 498 478 523 444 542Q421 555 406 579T391 616Q391 626 384 634T368 643H234Q226 643 220 633T214 612V586Q214 540 251 499T330 439Q363 424 377 407T391 365Q391 342 365 324T305 306Q269 306 245 322 225 336 185 386 178 395 168 395 161 395 154 391L63 321Q55 315 54 307T57 291Q146 143 316 143 361 143 406 160T487 206 546 278 569 366Z",
        "width": 571.4
      },
      "search": [
        "help"
      ]
    },
    {
      "uid": "e03a3396ad9ce1b09a85c28955799d92",
      "css": "home",
      "code": 8962,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M786 554V821Q786 836 775 847T750 857H536V643H393V857H179Q164 857 154 847T143 821V554Q143 553 143 552T143 550L464 286 785 550Q786 551 786 554ZM910 515L876 556Q871 561 864 562H862Q855 562 850 559L464 237 78 559Q71 563 65 563 58 561 53 556L18 515Q14 510 15 502T21 490L422 156Q440 141 464 141T507 156L643 270V161Q643 153 648 148T661 143H768Q776 143 781 148T786 161V388L908 490Q913 495 914 502T910 515Z",
        "width": 928.6
      },
      "search": [
        "home"
      ]
    },
    {
      "uid": "496e662a65eb5759e19b1c5d2e58a060",
      "css": "attach",
      "code": 128206,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M783 773Q783 838 739 882T630 926Q555 926 499 871L65 437Q2 373 2 286 2 198 64 136T214 74Q302 74 366 137L704 475Q709 480 709 487 709 496 692 513T666 530Q659 530 653 525L315 186Q271 143 214 143 155 143 114 185T74 286Q74 344 116 387L549 820Q584 855 630 855 666 855 689 832T713 773Q713 727 677 692L353 368Q339 354 320 354 304 354 293 365T282 392Q282 410 296 425L525 653Q531 659 531 666 531 675 513 692T487 709Q480 709 475 704L246 475Q211 441 211 392 211 346 243 314T320 282Q369 282 403 318L728 642Q783 696 783 773Z",
        "width": 785.7
      },
      "search": [
        "attach"
      ]
    },
    {
      "uid": "e5c7ed71e800e995ddfc3a195924913b",
      "css": "lock",
      "code": 128274,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M179 429H464V321Q464 262 422 220T321 179 220 220 179 321V429ZM643 482V804Q643 826 627 841T589 857H54Q31 857 16 841T0 804V482Q0 460 16 444T54 429H71V321Q71 219 145 145T321 71 498 145 571 321V429H589Q612 429 627 444T643 482Z",
        "width": 642.9
      },
      "search": [
        "lock"
      ]
    },
    {
      "uid": "f2a84f66f2d360b9c559376a275261ae",
      "css": "mail",
      "code": 9993,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M1000 396V839Q1000 876 974 902T911 929H89Q53 929 26 902T0 839V396Q25 424 56 445 258 582 334 637 366 661 385 674T438 701 499 714H501Q529 714 562 701T615 674 666 637Q761 569 944 445 976 423 1000 396ZM1000 232Q1000 276 973 316T905 385Q695 531 643 566 638 570 620 583T590 605 561 623 528 638 501 643H499Q487 643 472 638T439 623 410 605 380 583 357 566Q306 531 210 465T96 385Q61 362 31 321T0 244Q0 201 23 172T89 143H911Q947 143 973 169T1000 232Z",
        "width": 1000
      },
      "search": [
        "mail"
      ]
    },
    {
      "uid": "49aab36ecf027cc8608f81597c83d2d4",
      "css": "download",
      "code": 8659,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M714 750Q714 735 704 725T679 714 653 725 643 750 653 775 679 786 704 775 714 750ZM857 750Q857 735 847 725T821 714 796 725 786 750 796 775 821 786 847 775 857 750ZM929 625V804Q929 826 913 841T875 857H54Q31 857 16 841T0 804V625Q0 603 16 587T54 571H313L388 647Q421 679 464 679T540 647L616 571H875Q897 571 913 587T929 625ZM747 307Q757 330 739 347L489 597Q479 607 464 607T439 597L189 347Q172 330 181 307 191 286 214 286H357V36Q357 21 368 11T393 0H536Q550 0 561 11T571 36V286H714Q738 286 747 307Z",
        "width": 928.6
      },
      "search": [
        "download"
      ]
    },
    {
      "uid": "1ab77fb1c37bad79af7268c79a11b14f",
      "css": "edit",
      "code": 9998,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M203 857L253 806 122 675 71 726V786H143V857H203ZM494 339Q494 327 482 327 477 327 473 331L170 633Q166 637 166 643 166 655 179 655 184 655 188 651L491 349Q494 345 494 339ZM464 232L696 464 232 929H0V696ZM845 286Q845 315 825 336L732 429 500 196 593 104Q613 83 643 83 672 83 694 104L825 235Q845 257 845 286Z",
        "width": 857.1
      },
      "search": [
        "edit"
      ]
    },
    {
      "uid": "bf3c88e1a2208a0cf26001e0793ff403",
      "css": "print",
      "code": 9113,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M214 857H714V714H214V857ZM214 500H714V286H625Q603 286 587 270T571 232V143H214V500ZM857 536Q857 521 847 511T821 500 796 511 786 536 796 561 821 571 847 561 857 536ZM929 536V768Q929 775 923 780T911 786H786V875Q786 897 770 913T732 929H196Q174 929 159 913T143 875V786H18Q11 786 5 780T0 768V536Q0 492 32 460T107 429H143V125Q143 103 159 87T196 71H571Q594 71 621 83T663 109L748 194Q763 210 775 237T786 286V429H821Q866 429 897 460T929 536Z",
        "width": 928.6
      },
      "search": [
        "print"
      ]
    },
    {
      "uid": "4d879892b0e3a0da5d871e3df8d105e2",
      "css": "alert",
      "code": 9888,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M571 767V661Q571 653 566 648T554 643H446Q439 643 434 648T429 661V767Q429 775 434 780T446 786H554Q561 786 566 780T571 767ZM570 559L580 302Q580 296 575 292 568 286 561 286H439Q432 286 425 292 420 296 420 304L429 559Q429 564 435 568T448 571H551Q559 571 564 568T570 559ZM563 37L991 823Q1011 858 990 893 980 910 964 919T929 929H71Q53 929 36 919T10 893Q-11 858 9 823L438 37Q447 20 464 10T500 0 536 10 563 37Z",
        "width": 1000
      },
      "search": [
        "alert"
      ]
    },
    {
      "uid": "7091435c31f7a593060b9782a234db80",
      "css": "menu",
      "code": 119650,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M857 750V821Q857 836 847 846T821 857H36Q21 857 11 846T0 821V750Q0 735 11 725T36 714H821Q836 714 847 725T857 750ZM857 464V536Q857 550 847 561T821 571H36Q21 571 11 561T0 536V464Q0 450 11 439T36 429H821Q836 429 847 439T857 464ZM857 179V250Q857 265 847 275T821 286H36Q21 286 11 275T0 250V179Q0 164 11 153T36 143H821Q836 143 847 153T857 179Z",
        "width": 857.1
      },
      "search": [
        "menu"
      ]
    },
    {
      "uid": "08d8ae2e00462d737ce43e77738eee17",
      "css": "settings",
      "code": 9784,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M571 500Q571 441 530 399T429 357 328 399 286 500 328 601 429 643 530 601 571 500ZM857 439V563Q857 570 853 576T842 583L738 599Q728 629 717 650 736 677 776 727 782 733 782 740T777 753Q762 774 722 814T669 853Q662 853 655 848L578 788Q553 801 527 809 518 885 511 913 507 929 491 929H367Q359 929 353 924T347 912L331 809Q304 800 281 788L202 848Q196 853 188 853 180 853 174 847 104 783 82 753 78 748 78 740 78 734 83 728 91 716 111 691T141 651Q126 623 118 596L16 581Q9 580 5 574T0 561V437Q0 430 5 424T15 417L119 401Q127 376 141 350 118 318 81 273 75 266 75 259 75 254 80 247 95 227 135 187T188 147Q195 147 203 152L280 212Q304 199 330 191 339 115 347 87 350 72 367 72H491Q498 72 504 76T511 88L526 191Q554 200 576 212L656 152Q661 147 669 147 676 147 683 152 755 219 775 247 779 252 779 260 779 266 775 272 766 284 746 310T716 349Q730 377 739 404L841 419Q848 420 853 426T857 439Z",
        "width": 857.1
      },
      "search": [
        "settings"
      ]
    },
    {
      "uid": "b2af04a5076126cde5606aed07cfd647",
      "css": "down",
      "code": 8595,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M571 393Q571 407 561 418L311 668Q300 679 286 679T261 668L11 418Q0 407 0 393T11 368 36 357H536Q550 357 561 368T571 393Z",
        "width": 571.4
      },
      "search": [
        "down"
      ]
    },
    {
      "uid": "9a8f85e9fc8643b2587509f0e4b6cd1e",
      "css": "up",
      "code": 8593,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M571 679Q571 693 561 704T536 714H36Q21 714 11 704T0 679 11 653L261 403Q271 393 286 393T311 403L561 653Q571 664 571 679Z",
        "width": 571.4
      },
      "search": [
        "up"
      ]
    },
    {
      "uid": "fe71ca02f0de1b3624d28043ddb30c85",
      "css": "unlock",
      "code": 128275,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M929 321V464Q929 479 918 489T893 500H857Q843 500 832 489T821 464V321Q821 262 780 220T679 179 578 220 536 321V429H589Q612 429 627 444T643 482V804Q643 826 627 841T589 857H54Q31 857 16 841T0 804V482Q0 460 16 444T54 429H429V321Q429 218 502 145T679 71 855 145 929 321Z",
        "width": 928.6
      },
      "search": [
        "unlock"
      ]
    },
    {
      "uid": "56000699168af06774cd3f1646acbd5f",
      "css": "logout",
      "code": 10525,
      "src": "custom_icons",
      "selected": true,
      "svg": {
        "path": "M857 500Q857 587 823 666T732 803 595 895 429 929 262 895 126 803 34 666 0 500Q0 398 45 309T171 158Q195 140 225 144T271 172Q289 195 285 225T257 272Q203 313 173 373T143 500Q143 558 165 611T227 702 318 763 429 786 539 763 631 702 692 611 714 500Q714 432 684 373T600 272Q576 254 572 225T586 172Q603 148 633 144T686 158Q767 219 812 309T857 500ZM500 71V429Q500 458 479 479T429 500 378 479 357 429V71Q357 42 378 21T429 0 479 21 500 71Z",
        "width": 857.1
      },
      "search": [
        "logout"
      ]
    },
    {
      "uid": "474656633f79ea2f1dad59ff63f6bf07",
      "css": "star",
      "code": 9733,
      "src": "fontawesome"
    },
    {
      "uid": "f8aa663c489bcbd6e68ec8147dca841e",
      "css": "folder",
      "code": 128448,
      "src": "fontawesome"
    },
    {
      "uid": "178053298e3e5b03551d754d4b9acd8b",
      "css": "document",
      "code": 128453,
      "src": "fontawesome"
    },
    {
      "uid": "f9c3205df26e7778abac86183aefdc99",
      "css": "reset",
      "code": 8634,
      "src": "fontawesome"
    },
    {
      "uid": "eeec3208c90b7b48e804919d0d2d4a41",
      "css": "upload",
      "code": 8657,
      "src": "fontawesome"
    },
    {
      "uid": "390d6d13398cbf8c8c3c5493f7d34088",
      "css": "export",
      "code": 8631,
      "src": "entypo"
    },
    {
      "uid": "531bc468eecbb8867d822f1c11f1e039",
      "css": "calendar",
      "code": 128197,
      "src": "fontawesome"
    },
    {
      "uid": "4b900d04e8ab8c82f080c1cfbac5772c",
      "css": "uncheck",
      "code": 9744,
      "src": "fontawesome"
    },
    {
      "uid": "422e07e5afb80258a9c4ed1706498f8a",
      "css": "radio-unchecked",
      "code": 9711,
      "src": "fontawesome"
    },
    {
      "uid": "81bb68665e8e595505272a746db07c7a",
      "css": "radio-checked",
      "code": 11044,
      "src": "fontawesome"
    },
    {
      "uid": "c5845105a87df2ee1999826d90622f6a",
      "css": "paragraph",
      "code": 167,
      "src": "fontawesome"
    },
    {
      "uid": "a2a74f5e7b7d9ba054897d8c795a326a",
      "css": "list-ul",
      "code": 8226,
      "src": "fontawesome"
    },
    {
      "uid": "f6766a8b042c2453a4e153af03294383",
      "css": "list-ol",
      "code": 291,
      "src": "fontawesome"
    },
    {
      "uid": "0c708edd8fae2376b3370aa56d40cf9e",
      "css": "header",
      "code": 72,
      "src": "fontawesome"
    },
    {
      "uid": "a8cb1c217f02b073db3670c061cc54d2",
      "css": "italic",
      "code": 73,
      "src": "fontawesome"
    },
    {
      "uid": "8fb55fd696d9a0f58f3b27c1d8633750",
      "css": "table",
      "code": 9707,
      "src": "fontawesome"
    },
    {
      "uid": "cc1de8eafc95f6faffcd8683aa8e9aa1",
      "css": "bold",
      "code": 66,
      "src": "elusive"
    },
    {
      "uid": "381da2c2f7fd51f8de877c044d7f439d",
      "css": "image",
      "code": 128443,
      "src": "fontawesome"
    },
    {
      "uid": "d870630ff8f81e6de3958ecaeac532f2",
      "css": "left",
      "code": 8592,
      "src": "fontawesome"
    },
    {
      "uid": "399ef63b1e23ab1b761dfbb5591fa4da",
      "css": "right",
      "code": 8594,
      "src": "fontawesome"
    }
  ]
}

Modified src/www/admin/static/font/garradin.css from [d980f71e5b] to [1274bb9a83].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@charset "UTF-8";

 @font-face {
  font-family: 'garradin';
  src: url('../font/garradin.eot?31180986');
  src: url('../font/garradin.eot?31180986#iefix') format('embedded-opentype'),
       url('../font/garradin.woff2?31180986') format('woff2'),
       url('../font/garradin.woff?31180986') format('woff'),
       url('../font/garradin.ttf?31180986') format('truetype'),
       url('../font/garradin.svg?31180986#garradin') format('svg');
  font-weight: normal;
  font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
  @font-face {
    font-family: 'garradin';
    src: url('../font/garradin.svg?31180986#garradin') format('svg');
  }
}
*/
 
 [class^="icn-"]:before, [class*=" icn-"]:before {
  font-family: "garradin";
  font-style: normal;
  font-weight: normal;
  speak: none;
 
  display: inline-block;
  text-decoration: inherit;
  width: 1em;
  margin-right: .2em;
  text-align: center;
  /* opacity: .8; */




|
|
|
|
|
|









|








|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@charset "UTF-8";

 @font-face {
  font-family: 'garradin';
  src: url('../font/garradin.eot?36244460');
  src: url('../font/garradin.eot?36244460#iefix') format('embedded-opentype'),
       url('../font/garradin.woff2?36244460') format('woff2'),
       url('../font/garradin.woff?36244460') format('woff'),
       url('../font/garradin.ttf?36244460') format('truetype'),
       url('../font/garradin.svg?36244460#garradin') format('svg');
  font-weight: normal;
  font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
  @font-face {
    font-family: 'garradin';
    src: url('../font/garradin.svg?36244460#garradin') format('svg');
  }
}
*/
 
 [class^="icn-"]:before, [class*=" icn-"]:before {
  font-family: "garradin";
  font-style: normal;
  font-weight: normal;
  speak: never;
 
  display: inline-block;
  text-decoration: inherit;
  width: 1em;
  margin-right: .2em;
  text-align: center;
  /* opacity: .8; */
53
54
55
56
57
58
59







60

61



62
63
64




65
66
67
68
69
70
71
72
73
74
75

76
77
78
79

80
81
82
83



  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
 
  /* Uncomment for 3D effect */
  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
 







.icn-up:before { content: '\2191'; } /* '↑' */

.icn-down:before { content: '\2193'; } /* '↓' */



.icn-download:before { content: '\21d3'; } /* '⇓' */
.icn-home:before { content: '\2302'; } /* '⌂' */
.icn-print:before { content: '\2399'; } /* '⎙' */




.icn-check:before { content: '\2611'; } /* '☑' */
.icn-settings:before { content: '\2638'; } /* '☸' */
.icn-alert:before { content: '\26a0'; } /* '⚠' */
.icn-mail:before { content: '\2709'; } /* '✉' */
.icn-edit:before { content: '\270e'; } /* '✎' */
.icn-delete:before { content: '\2718'; } /* '✘' */
.icn-help:before { content: '\2753'; } /* '❓' */
.icn-plus:before { content: '\2795'; } /* '➕' */
.icn-minus:before { content: '\2796'; } /* '➖' */
.icn-logout:before { content: '\291d'; } /* '⤝' */
.icn-eye-off:before { content: '\292b'; } /* '⤫' */

.icn-menu:before { content: '𝍢'; } /* '\1d362' */
.icn-eye:before { content: '👁'; } /* '\1f441' */
.icn-user:before { content: '👤'; } /* '\1f464' */
.icn-users:before { content: '👪'; } /* '\1f46a' */

.icn-attach:before { content: '📎'; } /* '\1f4ce' */
.icn-search:before { content: '🔍'; } /* '\1f50d' */
.icn-lock:before { content: '🔒'; } /* '\1f512' */
.icn-unlock:before { content: '🔓'; } /* '\1f513' */










>
>
>
>
>
>
>

>

>
>
>



>
>
>
>











>




>




>
>
>
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
 
  /* Uncomment for 3D effect */
  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
 
.icn-bold:before { content: '\42'; } /* 'B' */
.icn-header:before { content: '\48'; } /* 'H' */
.icn-italic:before { content: '\49'; } /* 'I' */
.icn-paragraph:before { content: '\a7'; } /* '§' */
.icn-list-ol:before { content: '\0123'; } /* 'ģ' */
.icn-list-ul:before { content: '\2022'; } /* '•' */
.icn-left:before { content: '\2190'; } /* '←' */
.icn-up:before { content: '\2191'; } /* '↑' */
.icn-right:before { content: '\2192'; } /* '→' */
.icn-down:before { content: '\2193'; } /* '↓' */
.icn-export:before { content: '\21b7'; } /* '↷' */
.icn-reset:before { content: '\21ba'; } /* '↺' */
.icn-upload:before { content: '\21d1'; } /* '⇑' */
.icn-download:before { content: '\21d3'; } /* '⇓' */
.icn-home:before { content: '\2302'; } /* '⌂' */
.icn-print:before { content: '\2399'; } /* '⎙' */
.icn-table:before { content: '\25eb'; } /* '◫' */
.icn-radio-unchecked:before { content: '\25ef'; } /* '◯' */
.icn-star:before { content: '\2605'; } /* '★' */
.icn-uncheck:before { content: '\2610'; } /* '☐' */
.icn-check:before { content: '\2611'; } /* '☑' */
.icn-settings:before { content: '\2638'; } /* '☸' */
.icn-alert:before { content: '\26a0'; } /* '⚠' */
.icn-mail:before { content: '\2709'; } /* '✉' */
.icn-edit:before { content: '\270e'; } /* '✎' */
.icn-delete:before { content: '\2718'; } /* '✘' */
.icn-help:before { content: '\2753'; } /* '❓' */
.icn-plus:before { content: '\2795'; } /* '➕' */
.icn-minus:before { content: '\2796'; } /* '➖' */
.icn-logout:before { content: '\291d'; } /* '⤝' */
.icn-eye-off:before { content: '\292b'; } /* '⤫' */
.icn-radio-checked:before { content: '\2b24'; } /* '⬤' */
.icn-menu:before { content: '𝍢'; } /* '\1d362' */
.icn-eye:before { content: '👁'; } /* '\1f441' */
.icn-user:before { content: '👤'; } /* '\1f464' */
.icn-users:before { content: '👪'; } /* '\1f46a' */
.icn-calendar:before { content: '📅'; } /* '\1f4c5' */
.icn-attach:before { content: '📎'; } /* '\1f4ce' */
.icn-search:before { content: '🔍'; } /* '\1f50d' */
.icn-lock:before { content: '🔒'; } /* '\1f512' */
.icn-unlock:before { content: '🔓'; } /* '\1f513' */
.icn-image:before { content: '🖻'; } /* '\1f5bb' */
.icn-folder:before { content: '🗀'; } /* '\1f5c0' */
.icn-document:before { content: '🗅'; } /* '\1f5c5' */

Modified src/www/admin/static/font/garradin.eot from [4de801b16e] to [6d9991856c].

cannot compute difference between binary files

Modified src/www/admin/static/font/garradin.svg from [5bcfb6f494] to [91f74fefc9].

1
2
3
4
5
6
7
8














9
10


11
12






13
14
15
16
17








18
19
20
21
22
23
24
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata>
<defs>
<font id="garradin" horiz-adv-x="1000" >
<font-face font-family="garradin" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />














<glyph glyph-name="up" unicode="&#x2191;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />



<glyph glyph-name="down" unicode="&#x2193;" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />







<glyph glyph-name="download" unicode="&#x21d3;" d="M714 100q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />

<glyph glyph-name="home" unicode="&#x2302;" d="M786 296v-267q0-15-11-26t-25-10h-214v214h-143v-214h-214q-15 0-25 10t-11 26v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-4-7 2-12 7l-35 41q-4 5-3 13t6 12l401 334q18 15 42 15t43-15l136-114v109q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q5-5 6-12t-4-13z" horiz-adv-x="928.6" />

<glyph glyph-name="print" unicode="&#x2399;" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />









<glyph glyph-name="check" unicode="&#x2611;" d="M786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-2 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />

<glyph glyph-name="settings" unicode="&#x2638;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 15 20 15h124q7 0 13-4t7-12l15-103q28-9 50-21l80 60q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-13 0-6-4-12-9-12-29-38t-30-39q14-28 23-55l102-15q7-1 12-7t4-13z" horiz-adv-x="857.1" />

<glyph glyph-name="alert" unicode="&#x26a0;" d="M571 83v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-7 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 13 3t6 9z m-7 522l428-786q20-35-1-70-10-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />




|




>
>
>
>
>
>
>
>
>
>
>
>
>
>


>
>


>
>
>
>
>
>





>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2020 by original authors @ fontello.com</metadata>
<defs>
<font id="garradin" horiz-adv-x="1000" >
<font-face font-family="garradin" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="bold" unicode="&#x42;" d="M0-60l125 0 0 820-125 0 0 80 582 0q180 0 270-60t89-182q0-88-62-139t-184-64q147-14 226-79t79-173q0-145-111-215-110-68-344-68l-545 0 0 80z m379 0l94 0q125 0 185 49t61 154q0 105-62 157t-184 52l-94 0 0-412z m0 492l86 0q113 0 168 39 55 43 55 127 0 86-55 125-53 37-168 37l-86 0 0-328z" horiz-adv-x="1000" />

<glyph glyph-name="header" unicode="&#x48;" d="M939-79q-25 0-74 2t-75 2q-24 0-73-2t-74-2q-13 0-21 12t-7 25q0 18 9 26t22 9 29 4 25 9q18 11 18 78l0 218q0 12-1 17-7 3-28 3h-376q-22 0-29-3 0-5 0-17l-1-207q0-79 21-91 9-6 26-8t32-2 25-8 11-26q0-14-6-26t-21-13q-26 0-78 2t-77 2q-24 0-71-2t-71-2q-13 0-20 12t-7 25q0 17 9 25t20 10 26 4 24 9q18 13 18 80l-1 31v454q0 2 1 15t0 20-1 21-2 24-4 20-6 18-9 10q-8 5-25 7t-29 1-23 7-10 26q0 14 6 26t20 13q26 0 78-2t77-2q23 0 71 2t70 2q14 0 21-13t7-26q0-17-9-25t-22-8-27-2-24-7q-20-12-20-90l1-178q0-12 0-18 7-2 22-2h390q14 0 21 2 1 6 1 18l0 178q0 78-19 90-10 6-33 7t-37 7-14 28q0 14 7 26t21 13q24 0 74-2t73-2q24 0 72 2t72 2q14 0 21-13t7-26q0-17-10-25t-22-8-29-2-24-7q-20-13-20-90l1-526q0-66 19-78 9-6 25-8t30-2 23-9 10-25q0-14-6-26t-20-13z" horiz-adv-x="1000" />

<glyph glyph-name="italic" unicode="&#x49;" d="M0-78l10 48q12 4 34 9t40 11 33 13q16 19 23 56 1 4 35 162t63 303 29 165v14q-13 8-30 11t-39 4-32 3l10 58q19-1 67-4t84-4 67-1q27 0 55 1t68 4 54 4q-2-22-10-50-17-6-57-16t-60-19q-5-10-8-23t-5-23-4-25-4-24q-15-82-49-234t-43-198q-1-5-7-32t-11-51-9-46-4-32l1-10q9-3 103-18-2-24-9-55-6 0-18-1t-18-1q-16 0-49 6t-48 6q-77 1-115 1-28 0-79-5t-68-7z" horiz-adv-x="571.4" />

<glyph glyph-name="paragraph" unicode="&#xa7;" d="M713 745v-41q0-16-10-34t-24-18q-28 0-30-1-14-3-18-17-1-6-1-36v-643q0-14-11-24t-24-10h-60q-14 0-24 10t-10 24v680h-80v-680q0-14-9-24t-25-10h-60q-14 0-24 10t-10 24v277q-82 7-137 33-70 33-107 100-36 65-36 145 0 92 50 159 49 66 116 89 62 21 233 21h267q14 0 24-10t10-24z" horiz-adv-x="714.3" />

<glyph glyph-name="list-ol" unicode="&#x123;" d="M213-54q0-45-31-70t-75-26q-60 0-96 37l31 49q28-25 60-25 16 0 28 8t12 24q0 35-59 31l-14 31q4 6 18 24t24 31 20 21v1q-9 0-27-1t-27 0v-30h-59v85h186v-49l-53-65q28-6 45-27t17-49z m1 350v-89h-202q-4 20-4 30 0 29 14 52t31 38 37 27 31 24 14 25q0 14-9 22t-22 7q-25 0-45-32l-47 33q13 28 40 44t59 16q40 0 68-23t28-63q0-28-19-51t-42-36-42-28-20-30h71v34h59z m786-178v-107q0-7-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 8 5 13t13 5h678q7 0 13-6t5-12z m-786 502v-56h-187v56h60q0 22 0 67t1 68v7h-1q-5-10-28-30l-40 42 76 71h59v-225h60z m786-216v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" />

<glyph glyph-name="list-ul" unicode="&#x2022;" d="M214 64q0-44-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m0 286q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-107q0-7-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 7 5 12t13 6h678q7 0 13-6t5-12z m-786 518q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" />

<glyph glyph-name="left" unicode="&#x2190;" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />

<glyph glyph-name="up" unicode="&#x2191;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />

<glyph glyph-name="right" unicode="&#x2192;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />

<glyph glyph-name="down" unicode="&#x2193;" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />

<glyph glyph-name="export" unicode="&#x21b7;" d="M750 60l0 56 100 82 0-188q0-20-15-35t-35-15l-750 0q-20 0-35 15t-15 35l0 550q0 22 14 36t36 14l288 0q-32-24-59-49t-39-39l-10-12-130 0 0-450 650 0z m-82 348q-166 0-242-41t-160-181q0 8 1 22t9 56 22 79 44 83 70 79 107 56 149 23l0 156 332-250-332-260 0 178z" horiz-adv-x="1000" />

<glyph glyph-name="reset" unicode="&#x21ba;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z" horiz-adv-x="857.1" />

<glyph glyph-name="upload" unicode="&#x21d1;" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />

<glyph glyph-name="download" unicode="&#x21d3;" d="M714 100q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />

<glyph glyph-name="home" unicode="&#x2302;" d="M786 296v-267q0-15-11-26t-25-10h-214v214h-143v-214h-214q-15 0-25 10t-11 26v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-4-7 2-12 7l-35 41q-4 5-3 13t6 12l401 334q18 15 42 15t43-15l136-114v109q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q5-5 6-12t-4-13z" horiz-adv-x="928.6" />

<glyph glyph-name="print" unicode="&#x2399;" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />

<glyph glyph-name="table" unicode="&#x25eb;" d="M286 82v107q0 8-5 13t-13 5h-179q-7 0-12-5t-6-13v-107q0-8 6-13t12-5h179q8 0 13 5t5 13z m0 214v108q0 7-5 12t-13 5h-179q-7 0-12-5t-6-12v-108q0-7 6-12t12-5h179q8 0 13 5t5 12z m285-214v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m-285 429v107q0 8-5 13t-13 5h-179q-7 0-12-5t-6-13v-107q0-8 6-13t12-5h179q8 0 13 5t5 13z m285-215v108q0 7-5 12t-12 5h-179q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h179q7 0 12 5t5 12z m286-214v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m-286 429v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m286-215v108q0 7-5 12t-13 5h-178q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h178q8 0 13 5t5 12z m0 215v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m72 178v-607q0-37-27-63t-63-26h-750q-36 0-63 26t-26 63v607q0 37 26 63t63 27h750q37 0 63-27t27-63z" horiz-adv-x="928.6" />

<glyph glyph-name="radio-unchecked" unicode="&#x25ef;" d="M429 654q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152-41 152-110 111-152 41z m428-304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />

<glyph glyph-name="star" unicode="&#x2605;" d="M929 489q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />

<glyph glyph-name="uncheck" unicode="&#x2610;" d="M625 707h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v464q0 37-26 63t-63 26z m161-89v-464q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q66 0 114-48t47-113z" horiz-adv-x="785.7" />

<glyph glyph-name="check" unicode="&#x2611;" d="M786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-2 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />

<glyph glyph-name="settings" unicode="&#x2638;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 15 20 15h124q7 0 13-4t7-12l15-103q28-9 50-21l80 60q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-13 0-6-4-12-9-12-29-38t-30-39q14-28 23-55l102-15q7-1 12-7t4-13z" horiz-adv-x="857.1" />

<glyph glyph-name="alert" unicode="&#x26a0;" d="M571 83v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-7 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 13 3t6 9z m-7 522l428-786q20-35-1-70-10-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />

34
35
36
37
38
39
40


41
42
43
44
45
46
47
48


49
50
51
52
53
54
55






56
57
58

<glyph glyph-name="minus" unicode="&#x2796;" d="M786 439v-107q0-22-16-38t-38-15h-678q-23 0-38 15t-16 38v107q0 23 16 38t38 16h678q22 0 38-16t16-38z" horiz-adv-x="785.7" />

<glyph glyph-name="logout" unicode="&#x291d;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 22-111t62-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-24 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />

<glyph glyph-name="eye-off" unicode="&#x292b;" d="M0 326q6 49 64 110 79 80 176 128 129 61 260 61 29 0 59-2l74 129q10 16 23 18 4 0 8-2l51-32q17-7 2-33l-57-101-47-79-41-72-144-250-41-72-47-80-57-100q-15-25-31-15l-53 31q-15 8 0 33l49 86-8 4q-103 53-176 129-64 72-64 109z m264 0q0-74 47-133l48 84q-9 24-9 49 0 51 34 91t85 50l49 84-18 0q-98 0-167-66t-69-159z m177-295l41 71 18 0q98 0 167 65t69 159q0 74-47 133l63 109q2-2 4-3t4-1q103-52 176-128 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-25 0-59 4z m90 155l110 189q9-25 9-49 0-51-34-90t-85-50z" horiz-adv-x="1000" />



<glyph glyph-name="menu" unicode="&#x1d362;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />

<glyph glyph-name="eye" unicode="&#x1f441;" d="M0 350q6 49 64 110 79 80 176 129 129 60 260 60 137-2 260-60 103-53 176-129 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-137 2-260 61-103 53-176 129-64 72-64 109z m264 0q0-94 69-159t167-65 167 65 69 159-69 159-167 66-167-66-69-159z m86 1q0 60 44 102t106 42 106-42 44-102-44-102-106-43-106 43-44 102z" horiz-adv-x="1000" />

<glyph glyph-name="user" unicode="&#x1f464;" d="M786 66q0-67-41-106t-108-39h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q5 0 24-12t41-27 60-27 75-12 74 12 61 27 41 27 24 12q34 0 62-11t48-30 34-45 24-55 15-60 8-61 2-58z m-179 498q0-88-63-151t-151-63-152 63-62 151 62 152 152 63 151-63 63-152z" horiz-adv-x="785.7" />

<glyph glyph-name="users" unicode="&#x1f46a;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 23 12q35 0 63-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />



<glyph glyph-name="attach" unicode="&#x1f4ce;" d="M783 77q0-65-44-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 88 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-13 5l-338 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-35 81-35 36 0 59 23t24 59q0 46-36 81l-324 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-7 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 83-36l325-324q55-54 55-131z" horiz-adv-x="785.7" />

<glyph glyph-name="search" unicode="&#x1f50d;" d="M643 386q0 103-74 176t-176 74-177-74-73-176 73-177 177-73 176 73 74 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 152-31 126-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />

<glyph glyph-name="lock" unicode="&#x1f512;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />

<glyph glyph-name="unlock" unicode="&#x1f513;" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />






</font>
</defs>
</svg>







>
>








>
>







>
>
>
>
>
>



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

<glyph glyph-name="minus" unicode="&#x2796;" d="M786 439v-107q0-22-16-38t-38-15h-678q-23 0-38 15t-16 38v107q0 23 16 38t38 16h678q22 0 38-16t16-38z" horiz-adv-x="785.7" />

<glyph glyph-name="logout" unicode="&#x291d;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 22-111t62-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-24 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />

<glyph glyph-name="eye-off" unicode="&#x292b;" d="M0 326q6 49 64 110 79 80 176 128 129 61 260 61 29 0 59-2l74 129q10 16 23 18 4 0 8-2l51-32q17-7 2-33l-57-101-47-79-41-72-144-250-41-72-47-80-57-100q-15-25-31-15l-53 31q-15 8 0 33l49 86-8 4q-103 53-176 129-64 72-64 109z m264 0q0-74 47-133l48 84q-9 24-9 49 0 51 34 91t85 50l49 84-18 0q-98 0-167-66t-69-159z m177-295l41 71 18 0q98 0 167 65t69 159q0 74-47 133l63 109q2-2 4-3t4-1q103-52 176-128 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-25 0-59 4z m90 155l110 189q9-25 9-49 0-51-34-90t-85-50z" horiz-adv-x="1000" />

<glyph glyph-name="radio-checked" unicode="&#x2b24;" d="M571 350q0-59-41-101t-101-42-101 42-42 101 42 101 101 42 101-42 41-101z m-142 304q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152-41 152-110 111-152 41z m428-304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />

<glyph glyph-name="menu" unicode="&#x1d362;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />

<glyph glyph-name="eye" unicode="&#x1f441;" d="M0 350q6 49 64 110 79 80 176 129 129 60 260 60 137-2 260-60 103-53 176-129 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-137 2-260 61-103 53-176 129-64 72-64 109z m264 0q0-94 69-159t167-65 167 65 69 159-69 159-167 66-167-66-69-159z m86 1q0 60 44 102t106 42 106-42 44-102-44-102-106-43-106 43-44 102z" horiz-adv-x="1000" />

<glyph glyph-name="user" unicode="&#x1f464;" d="M786 66q0-67-41-106t-108-39h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q5 0 24-12t41-27 60-27 75-12 74 12 61 27 41 27 24 12q34 0 62-11t48-30 34-45 24-55 15-60 8-61 2-58z m-179 498q0-88-63-151t-151-63-152 63-62 151 62 152 152 63 151-63 63-152z" horiz-adv-x="785.7" />

<glyph glyph-name="users" unicode="&#x1f46a;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 23 12q35 0 63-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />

<glyph glyph-name="calendar" unicode="&#x1f4c5;" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />

<glyph glyph-name="attach" unicode="&#x1f4ce;" d="M783 77q0-65-44-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 88 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-13 5l-338 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-35 81-35 36 0 59 23t24 59q0 46-36 81l-324 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-7 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 83-36l325-324q55-54 55-131z" horiz-adv-x="785.7" />

<glyph glyph-name="search" unicode="&#x1f50d;" d="M643 386q0 103-74 176t-176 74-177-74-73-176 73-177 177-73 176 73 74 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 152-31 126-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />

<glyph glyph-name="lock" unicode="&#x1f512;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />

<glyph glyph-name="unlock" unicode="&#x1f513;" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />

<glyph glyph-name="image" unicode="&#x1f5bb;" d="M357 529q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />

<glyph glyph-name="folder" unicode="&#x1f5c0;" d="M929 511v-393q0-51-37-88t-88-37h-679q-51 0-88 37t-37 88v536q0 51 37 88t88 37h179q51 0 88-37t37-88v-18h375q51 0 88-37t37-88z" horiz-adv-x="928.6" />

<glyph glyph-name="document" unicode="&#x1f5c5;" d="M571 564v264q13-8 21-16l227-228q8-7 16-20h-264z m-71-18q0-22 16-37t38-16h303v-589q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h446v-304z" horiz-adv-x="857.1" />
</font>
</defs>
</svg>

Modified src/www/admin/static/font/garradin.ttf from [2aa8a2bf32] to [4beed9ef00].

cannot compute difference between binary files

Modified src/www/admin/static/font/garradin.woff from [ccddd0b63a] to [c61f50249f].

cannot compute difference between binary files

Added src/www/admin/static/font/garradin.woff2 version [a65634acfa].

cannot compute difference between binary files

Deleted src/www/admin/static/garradin.png version [e27a36efbf].

cannot compute difference between binary files

Modified src/www/admin/static/handheld.css from [2785cac0a7] to [02f3d81c1c].

1
2
3
4
5









6
7
8
9
10
11
12
13
14
15
16
17
18
body {
	background-position: -180px 0px;
	background-attachment: scroll;
	font-size: 11pt;
}










.header h1 {
	margin: 0;
	text-align: center;
	font-size: 1.2em;
	margin: 70pt 0 .3em 0;
}

.header .menu {
	background: none !important;
	position: fixed;
	bottom: inherit;
	overflow: visible;





>
>
>
>
>
>
>
>
>





|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
body {
	background-position: -180px 0px;
	background-attachment: scroll;
	font-size: 11pt;
}

input, select, textarea {
    min-width: 0 !important;
    max-width: calc(100% - 2em);
}

select {
	width: calc(100% - 3em);
}

.header h1 {
	margin: 0;
	text-align: center;
	font-size: 1.2em;
	margin: 6em 0 .3em 0;
}

.header .menu {
	background: none !important;
	position: fixed;
	bottom: inherit;
	overflow: visible;
77
78
79
80
81
82
83


84
85
86
87
88
89

90
91
92
93
94
95
96
97
98


99
100
101
102

103
104
105
106

107
108
109
110
111
112
113
114
115
116
117
118

119
120
121
122
123
124






125
126
127
128
129
130
131

.header .menu > ul > li.current_parent b {
	color: rgb(var(--gMainColor));
}

.header .menu > ul > li.current > ul, .header .menu > ul > li.current_parent > ul {
	display: flex;


	position: absolute;
	top: 30pt;
	left: 0;
	right: 0;
	background: #fff;
	background: rgba(255, 255, 255, 0.75);

}

.header .menu > ul > li.current > ul li, .header .menu > ul > li.current_parent > ul li {
	flex-grow: 1;
	text-align: center;
	display: block;
}

.header .menu > ul > li.current > ul a, .header .menu > ul > li.current_parent > ul a {


    color: #9c4f15;
    color: rgb(var(--gMainColor));
    padding: 2pt 3pt;
    text-decoration: underline;

}

.header .menu > ul > li > ul li.current a {
	color: rgb(var(--gSecondColor));

}

main {
	margin: 0;
	padding: .1em;
}

ul.actions {
	border: none;
	padding: 0;
	text-align: center;
	margin: .3em 0;

}

ul.actions li a, ul.actions li label {
	margin: .1em;
	border-radius: .5em;
}







.filterCategory, .searchMember {
	width: auto;
	float: none;
}

pre.sql_schema, .wikiChildren, fieldset.wikiMain, fieldset.wikiRights, fieldset.wikiEncrypt {







>
>




<

>



<





>
>
|
|
|
|
>



|
>







|


<

>


|



>
>
>
>
>
>







86
87
88
89
90
91
92
93
94
95
96
97
98

99
100
101
102
103

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

.header .menu > ul > li.current_parent b {
	color: rgb(var(--gMainColor));
}

.header .menu > ul > li.current > ul, .header .menu > ul > li.current_parent > ul {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	position: absolute;
	top: 30pt;
	left: 0;
	right: 0;

	background: rgba(255, 255, 255, 0.75);
	border-bottom: .2rem solid rgba(var(--gMainColor), 0.5);
}

.header .menu > ul > li.current > ul li, .header .menu > ul > li.current_parent > ul li {

	text-align: center;
	display: block;
}

.header .menu > ul > li.current > ul a, .header .menu > ul > li.current_parent > ul a {
	background: rgb(var(--gSecondColor), 0.3);
	border-radius: .5em;
	margin: .2rem;
	color: rgb(var(--gMainColor));
	padding: .3em .5em;
	font-size: .9em;
	font-weight: normal;
}

.header .menu > ul > li > ul li.current a {
	background: rgb(var(--gSecondColor));
	color: #fff;
}

main {
	margin: 0;
	padding: .1em;
}

nav.tabs ul {
	border: none;
	padding: 0;

	margin: .3em 0;
	justify-content: center;
}

nav.tabs ul li a, nav.tabs ul li label {
	margin: .1em;
	border-radius: .5em;
}

nav.tabs .sub {
	margin: 1em 0;
	border: none;
	border-top: .2rem dashed rgb(var(--gSecondColor));
}

.filterCategory, .searchMember {
	width: auto;
	float: none;
}

pre.sql_schema, .wikiChildren, fieldset.wikiMain, fieldset.wikiRights, fieldset.wikiEncrypt {
161
162
163
164
165
166
167




168
169
170
171
172
173
174
175
176
177









178
179
180
181
182
183
184
185
186
187







188



189















190


191
192





























	width: auto;
	margin-left: 0;
	margin-right: 0;
}

/* Petits écrans (smartphones) */
@media screen and (max-width:600px) {




	table.list td, table.list th {
		display: inline-block;
		border-left: 1px solid #999;
		width: auto !important;
	}

	colgroup {
		/* Hack pour désactiver les largeurs de colonnes */
		display: none;
	}










	table.list td:first-child, table.list th:first-child {
		border-left: none;
	}

	.infos_asso {
		float: none;
		width: auto;
	}








	input[type=text], textarea, input[type=password], input[type=email], input[type=url], input[type=tel], select {



		min-width: 3em;















		width: 95%;


	}
}




































>
>
>
>


|







>
>
>
>
>
>
>
>
>










>
>
>
>
>
>
>
|
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>

|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
	width: auto;
	margin-left: 0;
	margin-right: 0;
}

/* Petits écrans (smartphones) */
@media screen and (max-width:600px) {
	table.list tr {
		display: block;
	}

	table.list td, table.list th {
		display: inline-block;
		border-left: 2px dashed #999;
		width: auto !important;
	}

	colgroup {
		/* Hack pour désactiver les largeurs de colonnes */
		display: none;
	}

	.print-btn {
		display: none;
	}

	.radio-btn input {
		position: absolute;
		right: 1em;
	}

	table.list td:first-child, table.list th:first-child {
		border-left: none;
	}

	.infos_asso {
		float: none;
		width: auto;
	}

	nav.acc-year {
		flex-direction: column;
		border: none;
		border-radius: 0;
		background: rgba(var(--gMainColor), 0.2);
		font-size: .8em;
	}

	.actions, table.list .actions {
		text-align: center;
	}

	.actions a {
		margin: .2em;
		font-size: .9em;
	}

	nav.tabs aside {
		float: none;
		text-align: center;
	}

	#dialog {
		background: black;
	}

	#dialog > iframe {
		width: 100%;
		height: 100%;
		margin: 0;
	}

	#dialog > button {
		border: none;
		background: none;
	}

	body#popup main {
		margin: .2em;
	}

	dl.list {
		text-align: center;
	}

	table.statement td, table.statement tr {
		display: block;
	}

	dl.describe {
		grid-template: auto / 30% 1fr;
	}

	.datepicker-parent {
		position: static;
	}

	dialog.datepicker {
		margin: auto;
	}
}

Modified src/www/admin/static/print.css from [55c05d913f] to [5d6f8ad8c8].

1
2
3
4
5
6
7
8


9

10
11
12

13
14
15

16
17
18
19
20
21




22
23

24
25




26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45












46
47
48
49
50
51
52
53
54
55
56
57
58
59
60






61
62
63
64
65
66





@page {
    size: A4;
    margin: 1cm;
}

body {
    background: #fff;
    padding: 0;


}

header.header {
    display: none;
}

main {
    margin: 0;
}

.header h1 {
    margin: 0;
    text-align: center;
}

table.list thead {




    background: #000;
    color: #fff;

}





table.list tfoot tr {
    background: #666;
    color: #fff;
}

table.list tr {
    border: 1px solid #666;
}

table.list tr:nth-child(even) {
    background: #ddd;
}

table.list.multi tr:nth-child(even) {
    background: inherit;
}

table.list.multi tr:nth-child(4n+1), table.list.multi tr:nth-child(4n+2) {
    background: #ddd;
}













#rapport table table {
    border: 1px solid #666;
}

#rapport .parent {
    background: #ccc;
}

.noprint {
    display: none;
}

td.actions * {
    display: none;






}

a {
    color: inherit;
    text-decoration: none;
}






|
|





>
>

>



>



>






>
>
>
>
|
|
>


>
>
>
>
|
|
|

















>
>
>
>
>
>
>
>
>
>
>
>













|
|
>
>
>
>
>
>



|


>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@page {
    size: A4 landscape;
    margin: 0;
}

body {
    background: #fff;
    padding: 0;
    margin: 1cm;
    font-size: 10pt;
}

header.header {
    display: none;
}

main {
    margin: 0;
}

.header h1 {
    margin: 0;
    text-align: center;
}

table.list thead {
    border-bottom: solid .3rem #000;
}

table.list thead td, table.list thead th {
    background: #ccc !important;
    color: #000 !important;
    border-right: 1px solid #999;
}

table.list tfoot {
    border-top: double .3rem #000;
}

table.list tfoot tr td, table.list tfoot th {
    background: #fff;
    color: #000;
}

table.list tr {
    border: 1px solid #666;
}

table.list tr:nth-child(even) {
    background: #ddd;
}

table.list.multi tr:nth-child(even) {
    background: inherit;
}

table.list.multi tr:nth-child(4n+1), table.list.multi tr:nth-child(4n+2) {
    background: #ddd;
}

table.list td {
    border: 1px solid #999;
}

#rapport tr {
    color: #000 !important;
}

#rapport table .parent {
    border-top: 1px dashed #666;
}

#rapport table table {
    border: 1px solid #666;
}

#rapport .parent {
    background: #ccc;
}

.noprint {
    display: none;
}

td.actions *, nav.tabs, .icn-btn, .pagination, a.icn {
    display: none !important;
}

td.num a {
    border: none;
    padding: 0;
    background: none;
}

a {
    color: black !important;
    text-decoration: none;
}

/* Don't repeat the table footer on every printed page */
table tfoot{
    display:table-row-group;
}

Added src/www/admin/static/scripts/accounting.js version [0e2e45cbde].

























































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
function initTransactionForm(is_new) {
	// Advanced transaction: line management
	var lines = $('.transaction-lines tbody tr');

	function initLine(row) {
		var removeBtn = row.querySelector('button[name="remove_line"]');
		removeBtn.onclick = () => {
			var count = $('.transaction-lines tbody tr').length;
			var min = removeBtn.getAttribute('min');

			if (count <= min) {
				alert("Il n'est pas possible d'avoir moins de " + min + " lignes dans une écriture.");
				return false;
			}

			row.parentNode.removeChild(row);
			updateTotals();
		};

		// To be able to change input just by pressing up/down
		var inputs = row.querySelectorAll('input, select, button');

		inputs.forEach((i, k) => {
			i.onkeydown = (e) => {
				if (e.key == 'ArrowUp' && (p = row.previousElementSibling)) {
					p.querySelectorAll('input, select, button')[k].focus();
					return false;
				}
				else if (e.key == 'ArrowDown' && (n = row.nextElementSibling)) {
					n.querySelectorAll('input, select, button')[k].focus();
					return false;
				}
			};
		});

		// Update totals and disable other amount input
		var inputs = row.querySelectorAll('input.money');

		inputs.forEach((i, k) => {
			i.onkeyup = (e) => {
				var v = i.value.replace(/[^0-9,.]/);
				if (v.length && v != 0) {
					i.classList.remove('disabled');
					inputs[+!k].classList.add('disabled');
					inputs[+!k].value = '0';
					updateTotals();
				}
			};

			if (+i.value == 0 && +inputs[+!k].value != 0) {
				i.classList.add('disabled');
				i.value = '0';
			}
		});
	}

	lines.forEach(initLine);

	function updateTotals() {
		var amounts = $('.transaction-lines tbody input.money');
		var debit = credit = 0;

		amounts.forEach((i) => {
			if (!i.value) {
				return;
			}

			var v = g.getMoneyAsInt(i.value);

			if (i.name.match(/debit/)) {
				debit += v;
			}
			else {
				credit += v;
			}
		});

		if (m = $('#lines_message')) {
			var diff = credit - debit;
			m.innerHTML = (!diff) ? '' : '<span class="alert">Écriture non équilibrée (' + g.formatMoney(diff) + ')</span>';
		}

		debit = debit ? debit + '' : '000';
		credit = credit ? credit + '' : '000';
		$('#f_debit_total').value = g.formatMoney(debit);
		$('#f_credit_total').value = g.formatMoney(credit);
	}

	// Add row button
	$('.transaction-lines tfoot button')[0].onclick = () => {
		var line = $('.transaction-lines tbody tr')[0];
		var n = line.cloneNode(true);
		n.querySelectorAll('input').forEach((e) => {
			e.value = e.className.match(/money/) ? '0' : '';
		});
		if (l = n.querySelector('.input-list .label')) {
			l.parentNode.removeChild(l);
		}
		var b = n.querySelector('.input-list button');
		b.onclick = () => {
			g.current_list_input = b.parentNode;
			g.openFrameDialog(b.value);
			return false;
		};
		line.parentNode.appendChild(n);
		initLine(n);
	};

	updateTotals();

	// Hide type specific parts of the form
	function hideAllTypes() {
		g.toggle('[data-types]', false);
	}

	// Toggle parts of the form when a type is selected
	function selectType(v) {
		hideAllTypes();
		g.toggle('[data-types~=t' + v + ']', true);
		g.toggle('[data-types=all-but-advanced]', v != 0);
		// Disable required form elements, or the form won't be able to be submitted
		$('[data-types=all-but-advanced] input[required]').forEach((e) => {
			e.disabled = v == 'advanced' ? true : false;
		});

	}

	var radios = $('fieldset input[type=radio][name=type]');

	radios.forEach((e) => {
		e.onchange = () => {
			document.querySelectorAll('fieldset').forEach((e, k) => {
				if (!is_new || k == 0 || e.dataset.types) return;
				g.toggle(e, true);
				g.toggle('p.submit', true);
			});
			selectType(e.value);
		};
	});

	hideAllTypes();

	// In case of a pre-filled form: show the correct part of the form
	var current = document.querySelector('input[name=type]:checked');
	if (current) {
		selectType(current.value);
	}

	if (is_new) {
		document.querySelectorAll('fieldset').forEach((e, k) => {
			if (k == 0) return;
			g.toggle(e, false);
			g.toggle('p.submit', false);
		});
	}
}

Modified src/www/admin/static/scripts/color_helper.js from [e3dcda260f] to [97e321468f].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18







19
20
21
22
23
24
25
26
27

28
29
30
31

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60


61





62

63






























































64
65
66
67
68



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101




























102
103
(function () {
	if (!document.documentElement.style.setProperty 
		|| !window.CSS || !window.CSS.supports
		|| !window.CSS.supports('--var', 0))
	{
		return;
	}

	var logo_limit_x = 170;

	function colorToRGB(color)
	{
		// Conversion vers décimal RGB
		return color.replace(/^#/, '').match(/.{1,2}/g).map(function (el) {
			// On limite la luminosité comme ça, c'est pas parfait mais ça marche
			return Math.min(parseInt(el, 16), 210);
		});
	}








	function changeColor(element, color)
	{
		var new_color = colorToRGB(color).join(', ');

		// Mise à jour variable CSS
		document.documentElement.style.setProperty('--' + element, new_color);

		applyLogoColors();

	}

	function applyLogoColors()
	{

		var color = colorToRGB(document.getElementById('f_couleur2').value);

		var img = new Image;
		img.crossOrigin = "Anonymous";

		img.onload = function() {
			var canvas = document.createElement('canvas');
			var ctx = canvas.getContext('2d');
			canvas.width = img.width;
			canvas.height = img.height;
			ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);

			var imgData = ctx.getImageData(0, 0, img.width, img.height);
			var data = imgData.data;

			for (var i = 0, n = data.length; i < n; i += 4)
			{
				// Re-colorier l'image avec la couleur choisie
				data[i] = color[0]; // red
				data[i+1] = color[1]; // green
				data[i+2] = color[2]; // blue
				// i+3 = alpha channel, mais on n'y touche pas
			}

			ctx.putImageData(imgData, 0, 0);

			var i = canvas.toDataURL('image/png');

			// Prévisualisation


			document.querySelector('body').style.backgroundImage = 'url("' + i + '")';





			document.querySelector('.header .menu').style.backgroundImage = 'url("' + i + '")';
































































			document.getElementById('f_image_fond').value = i.substr(i.indexOf(',')+1);

			delete canvas2;
			delete canvas;
			delete ctx;



		};

		img.src = document.getElementById('f_image_fond').getAttribute('data-source');
	}

	garradin.onload(function () {
		var couleurs = {'couleur1': 'gMainColor', 'couleur2': 'gSecondColor'};

		for (var couleur in couleurs)
		{
			if (!couleurs.hasOwnProperty(couleur)) continue;

			var input = document.getElementById('f_' + couleur);

			input.oninput = function () {
				changeColor(couleurs[this.name], this.value);

			};

			// Ajout bouton remise à zéro de la couleur
			var reset_btn = document.createElement('input');
			reset_btn.className = 'resetButton';
			reset_btn.type = 'button';
			reset_btn.value = 'RàZ';

			reset_btn.onclick = function() {
				var input = this.previousSibling;
				input.value = input.getAttribute('placeholder');
				changeColor(couleurs[input.name], input.value);
				return false;
			};

			input.parentNode.insertBefore(reset_btn, input.nextSibling);
		}




























	});
})();










|




|


>
>
>
>
>
>
>



|




|
>


|

>
|











|


|
<

|
|
|








>
>
|
>
>
>
>
>
|
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|




>
>
>


|












|
>



|
|

|










>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
(function () {
	if (!document.documentElement.style.setProperty 
		|| !window.CSS || !window.CSS.supports
		|| !window.CSS.supports('--var', 0))
	{
		return;
	}

	var logo_limit_x = 170;

	function colorToRGB(color, type)
	{
		// Conversion vers décimal RGB
		return color.replace(/^#/, '').match(/.{1,2}/g).map(function (el) {
			// On limite la luminosité comme ça, c'est pas parfait mais ça marche
			return Math.min(parseInt(el, 16), type == 'gMainColor' ? 180 : 220);
		});
	}

	function RGBToHex(color) {
		// Conversion vers décimal RGB
		return '#' + color.split(/,/).map(function (el) {
			return ('0' + parseInt(el, 10).toString(16)).substr(-2);
		}).join('');
	}

	function changeColor(element, color)
	{
		var new_color = colorToRGB(color, element).join(',');

		// Mise à jour variable CSS
		document.documentElement.style.setProperty('--' + element, new_color);

		applyColors();
		return new_color;
	}

	function applyColors()
	{
		let input = $('#f_couleur2');
		var color = colorToRGB(input.value, 'gSecondColor');

		var img = new Image;
		img.crossOrigin = "Anonymous";

		img.onload = function() {
			var canvas = document.createElement('canvas');
			var ctx = canvas.getContext('2d');
			canvas.width = img.width;
			canvas.height = img.height;
			ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);

			var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
			var data = imgData.data;

			for (var i = 0; i < data.length; i += 4) {

				// Re-colorier l'image avec la couleur choisie
				data[i] = color[0];
				data[i+1] = color[1];
				data[i+2] = color[2];
				// i+3 = alpha channel, mais on n'y touche pas
			}

			ctx.putImageData(imgData, 0, 0);

			var i = canvas.toDataURL('image/png');

			// Prévisualisation
			document.documentElement.style.setProperty('--gBgImage', 'url("' + i + '")');
			$('#f_image_fond').value = i.substr(i.indexOf(',')+1);

			delete canvas2;
			delete canvas;
			delete ctx;
			delete img;
		};

		var bg = $('#f_image_fond');

		if (bg.value) {
			img.src = 'data:image/png;base64,' + bg.value;
		}
		else if (bg.dataset.source) {
			img.src = 'data:image/png;base64,' + bg.dataset.source;
		}
		else {
			img.src = bg.dataset.default;
		}
	}

	/**
	 * Imports a new image and makes it black and white
	 */
	function importBackgroundImage(data, callback)
	{
		var max_w = 380, max_h = 300;

		var img = new Image;
		img.crossOrigin = "Anonymous";

		img.onload = function() {
			var canvas = document.createElement('canvas');
			var ctx = canvas.getContext('2d');

			var w = img.width, h = img.height;

			if (w > max_w) {
				w = max_w;
				h = (w / img.width) * img.height;
			}

			if (h > max_h) {
				h = max_h;
				w = (h / img.height) * img.width;
			}

			canvas.width = w;
			canvas.height = h;
			ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, w, h);

			var imgData = ctx.getImageData(0, 0, w, h);
			var data = imgData.data;

			var i = 0;

			for(var y = 0; y < imgData.height; y++) {
				for(var x = 0; x < imgData.width; x++) {
					var avg = (data[i] * 0.3 + data[i+1] * 0.59 + data[i+2] * 0.11);
					var b = avg < 127 && (data[i+3] > 127);
					data[i] = b ? avg : 255; // red
					data[i+1] = b ? avg : 255; // green
					data[i+2] = b ? avg : 255; // blue
					data[i+3] = b ? (x > 170 ? 50 : 150) : 0;
					i += 4;
				}
			}

			ctx.putImageData(imgData, 0, 0);

			var i = canvas.toDataURL('image/png');

			$('#f_image_fond').value = i.substr(i.indexOf(',')+1);

			delete canvas2;
			delete canvas;
			delete ctx;
			delete img;

			callback();
		};

		img.src = data;
	}

	garradin.onload(function () {
		var couleurs = {'couleur1': 'gMainColor', 'couleur2': 'gSecondColor'};

		for (var couleur in couleurs)
		{
			if (!couleurs.hasOwnProperty(couleur)) continue;

			var input = document.getElementById('f_' + couleur);

			input.oninput = function () {
				var c = changeColor(couleurs[this.name], this.value);
				this.value = RGBToHex(c);
			};

			// Ajout bouton remise à zéro de la couleur
			var reset_btn = document.createElement('button');
			reset_btn.className = 'resetButton icn-btn';
			reset_btn.type = 'button';
			reset_btn.innerHTML = 'RàZ';

			reset_btn.onclick = function() {
				var input = this.previousSibling;
				input.value = input.getAttribute('placeholder');
				changeColor(couleurs[input.name], input.value);
				return false;
			};

			input.parentNode.insertBefore(reset_btn, input.nextSibling);
		}

		var bg = $('#f_background');
		bg.onchange = () => {
			if (!bg.files.length) return;

			var reader = new FileReader;
			reader.onload = (e) => {
				importBackgroundImage(e.target.result, applyColors);
				bg.disabled = true;
				bg.value = '';
			};
			reader.readAsDataURL(bg.files[0]);
		};

		var reset_btn = document.createElement('button');
		reset_btn.className = 'resetButton icn-btn';
		reset_btn.type = 'button';
		reset_btn.innerHTML = 'RàZ';

		reset_btn.onclick = () => {
			$('#f_image_fond').dataset.source = '';
			$('#f_image_fond').value = '';
			bg.disabled = false;

			applyColors();
		};

		bg.parentNode.insertBefore(reset_btn, bg.nextSibling);
	});
})();

Added src/www/admin/static/scripts/datepicker2.js version [180ac8cc87].



























































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
(function () {
	DATEPICKER_L10N = {};
	DATEPICKER_L10N.en = {
		weekdays: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
		months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
	};
	DATEPICKER_L10N.fr = {
		weekdays: ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'],
		months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
	};

	window.DatePicker = class {
		constructor(button, input, config) {
			this.button = button;
			this.input = input;
			this.date = null;

			Object.assign(this, {
				format: 0, // 0 = Y-m-d, 1 = d/m/Y
				lang: 'fr',
				class: 'datepicker'
			}, config);

			var c = document.createElement('dialog');
			c.className = this.class;
			this.container = button.parentNode.insertBefore(c, button.nextSibling);

			button.onclick = () => { this.container.hasAttribute('open') ? this.close() : this.open() };
		}

		open()
		{
			var d = this.input.value;
			if (d == '') {
				d = new CalendarDate;
			}
			else if (this.format == 1) {
				d = d.split('/');
				d = new CalendarDate(d[2], d[1] - 1, d[0]);
			}
			else {
				d = new CalendarDate(d);
			}

			this.date = d;

			this.refresh();

			this.focus();

			this.container.setAttribute('open', 'open');

			this.keyEvent = (e) => {
				var r = this.key(e.key);

				if (!r) {
					e.preventDefault();
				}

				return r;
			};

			document.addEventListener('keydown', this.keyEvent);
		}

		key(key)
		{
			switch (key) {
				case 'Enter': return !!this.select();
				case 'Escape': return !!this.close();
				case 'ArrowLeft': return !!this.day(-1);
				case 'ArrowRight': return !!this.day(1);
				case 'ArrowUp': return !!this.day(-7);
				case 'ArrowDown': return !!this.day(7);
				case 'PageDown': return !!this.month(1);
				case 'PageUp': return !!this.month(-1);
			}

			return true;
		}

		close()
		{
			this.container.innerHTML = '';
			this.container.removeAttribute('open');

			document.removeEventListener('keydown', this.keyEvent);
		}

		generateTable()
		{
			var c = (e) => { return document.createElement(e); }
			var table = c('table'),
				head = c('thead'),
				headRow = c('tr'),
				body = c('tbody');

			DATEPICKER_L10N[this.lang].weekdays.forEach((d) => {
				var cell = c('td');
				cell.innerHTML = d;
				headRow.appendChild(cell);
			});

			head.appendChild(headRow);
			table.appendChild(head);

			var weeks = this.date.getCalendarArray();

			weeks.forEach((week) => {
				var row = c('tr');

				week.forEach((day) => {
					var cell = c('td');
					cell.innerHTML = day ? day.getDate() : '';
					cell.onclick = (e) => { this.select(e); };
					row.appendChild(cell);
				});

				body.appendChild(row);
			});

			table.appendChild(body);

			return table;
		}

		refresh()
		{
			this.container.innerHTML = '';
			var header = document.createElement('nav');

			var p = document.createElement('input');
			p.type = 'button';
			p.value = '←';
			p.onclick = () => { this.month(-1); return false; };
			header.appendChild(p);

			var t = document.createElement('h3');
			t.innerHTML = DATEPICKER_L10N[this.lang].months[this.date.getMonth()] + ' ' + this.date.getFullYear();
			header.appendChild(t);

			var n = p.cloneNode(true);
			n.value = '→';
			n.onclick = () => { this.month(1); return false; };
			header.appendChild(n);

			this.container.appendChild(header);
			this.container.appendChild(this.generateTable());
		}

		month(change)
		{
			this.date.setMonth(this.date.getMonth() + change);
			this.refresh();
			this.focus();
		}

		day(change)
		{
			var old = new CalendarDate(this.date);
			this.date.setDate(this.date.getDate() + change);

			if (this.date.getMonth() != old.getMonth()) {
				this.refresh();
			}

			this.focus();
		}

		select(e)
		{
			if (e && e.target.textContent.match(/\d+/)) {
				this.date.setDate(parseInt(e.target.textContent, 10));
			}

			var y = this.date.getFullYear(),
				m = ('0' + (this.date.getMonth() + 1)).substr(-2),
				d = ('0' + this.date.getDate()).substr(-2);

			this.input.value = this.format == 1 ? d + '/' + m + '/' + y : y + '-' + m + '-' + d;
			this.close();
		}

		focus()
		{
			this.container.querySelectorAll('tbody td').forEach((cell) => {
				var v = parseInt(cell.innerHTML, 10);

				if (v === this.date.getDate()) {
					cell.className = 'focus';
				}
				else {
					cell.className = '';
				}
			});

			this.container.focus();
		}
	}

	class CalendarDate extends Date {
		getCalendarArray() {
			var date = new CalendarDate(this.getFullYear(), this.getMonth(), 1);
			var days = [];

			var day = date.getDayOfWeek();

			for (var i = 0; i < day - 1; i++) {
				days.push(null);
			}

			while (date.getMonth() === this.getMonth()) {
				days.push(new CalendarDate(date));
				date.setDate(date.getDate() + 1);
			}

			day = date.getDayOfWeek();
			for (var i = 0; i <= 7 - day; i++) {
				days.push(null);
			}

			var weeks = [];
			while (days.length) {
				weeks.push(days.splice(0, 7));
			}

			return weeks;
		}

		getDayOfWeek() {
			var day = this.getDay();
			if (day == 0) return 7;
			return day;
		}
	}

}());

Deleted src/www/admin/static/scripts/datepickr.css version [6ff0b57283].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
.calendar {
    font-family: 'Trebuchet MS', Tahoma, Verdana, Arial, sans-serif;
    font-size: 0.9em;
    background-color: #EEE;
    color: #333;
    border: 1px solid #DDD;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
    padding: 0.2em;
    width: 14em;
    box-shadow: 2px 2px 5px #666;
    transition: all .5s;
    opacity: 1;
    display: block;
    position: absolute;
    z-index: 100000;
}

.calendar.hidden {
    overflow: hidden;
    visibility: hidden;
    opacity: 0;
}

.calendar a {
    outline: none;
}

.calendar .months {
    background-color: #F6AF3A;
    border: 1px solid #E78F08;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
    color: #FFF;
    padding: 0.2em;
    text-align: center;
}

.calendar .prev-month,
.calendar .next-month {
    padding: 0;
}

.calendar .prev-month {
    float: left;
}

.calendar .next-month {
    float: right;
}

.calendar .current-month {
    margin: 0 auto;
}

.calendar .months a {
    color: #FFF;
    text-decoration: none;
    padding: 0 0.4em;
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
}

.calendar .months a:hover {
    background-color: #FDF5CE;
    color: #C77405;
}

.calendar table {
    border-collapse: collapse;
    padding: 0;
    font-size: 0.8em;
    width: 100%;
}

.calendar th {
    text-align: center;
}

.calendar table td {
    text-align: right;
    padding: 1px;
    width: 14.3%;
}

.calendar td a {
    display: block;
    color: #1C94C4;
    background-color: #F6F6F6;
    border: 1px solid #CCC;
    text-decoration: none;
    padding: 0.2em;
}

.calendar td a:hover {
    color: #C77405;
    background-color: #FDF5CE;
    border: 1px solid #FBCB09;
}

.calendar td.today a {
    background-color: #FFF0A5;
    border: 1px solid #FED22F;
    color: #363636;
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































































































































Deleted src/www/admin/static/scripts/datepickr.js version [491ae371e7].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
/*
	datepickr - pick your date not your nose
	Copyright (c) 2010 josh.salverda - 2012 bohwaz Apache License 2.0
	https://code.google.com/p/datepickr/
	http://dev.kd2.org/garradin/
*/

function datepickr(targetElement, userConfig) {

	var config = {
		fullCurrentMonth: true,
		dateFormat: 'F jS, Y',
		firstDayOfWeek: 1,
		weekdays: ['Sun', 'Mon', 'Tues', 'Wednes', 'Thurs', 'Fri', 'Satur'],
		months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
		suffix: { 1: 'st', 2: 'nd', 3: 'rd', 21: 'st', 22: 'nd', 23: 'rd', 31: 'st' },
		defaultSuffix: 'th'
	},
	currentDate = new Date(),
	currentPosition = new Array(0,0),
	currentMaxRows = 4,
	// shortcuts to get date info
	get = {
		current: {
			year: function() {
				return currentDate.getFullYear();
			},
			month: {
				integer: function() {
					return currentDate.getMonth();
				},
				string: function(full) {
					var date = currentDate.getMonth();
					return monthToStr(date, full);
				}
			},
			day: function() {
				return currentDate.getDate();
			}
		},
		month: {
			integer: function() {
				return currentMonthView;
			},
			string: function(full) {
				var date = currentMonthView;
				return monthToStr(date, full);
			},
			numDays: function() {
				// checks to see if february is a leap year otherwise return the respective # of days
				return (get.month.integer() == 1 && !(currentYearView & 3) && (currentYearView % 1e2 || !(currentYearView % 4e2))) ? 29 : daysInMonth[get.month.integer()];
			}
		}
	},
	// variables used throughout the class
	daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
	element, container, body, month, prevMonth, nextMonth,
	currentYearView = get.current.year(),
	currentMonthView = get.current.month.integer(),
	i, x, buildCache = [];

	function build(nodeName, attributes, content) {
		var element;

		if(!(nodeName in buildCache)) {
			buildCache[nodeName] = document.createElement(nodeName);
		}

		element = buildCache[nodeName].cloneNode(false);

		if(attributes != null) {
			for(var attribute in attributes) {
				element[attribute] = attributes[attribute];
			}
		}

		if(content != null) {
			if(typeof(content) == 'object') {
				element.appendChild(content);
			} else {
				element.innerHTML = content;
			}
		}

		return element;
	}

	function monthToStr(date, full) {
		return ((full == true) ? config.months[date] : ((config.months[date].length > 3) ? config.months[date].substring(0, 3) : config.months[date]));
	}

	function formatDate(milliseconds) {
		var formattedDate = '',
		dateObj = new Date(milliseconds),
		format = {
			d: function() {
				var day = format.j();
				return (day < 10) ? '0' + day : day;
			},
			D: function() {
				return config.weekdays[format.w()].substring(0, 3);
			},
			j: function() {
				return dateObj.getDate();
			},
			l: function() {
				return config.weekdays[format.w()] + 'day';
			},
			S: function() {
				return config.suffix[format.j()] || config.defaultSuffix;
			},
			w: function() {
				return dateObj.getDay();
			},
			F: function() {
				return monthToStr(format.n(), true);
			},
			m: function() {
				var month = format.n() + 1;
				return (month < 10) ? '0' + month : month;
			},
			M: function() {
				return monthToStr(format.n(), false);
			},
			n: function() {
				return dateObj.getMonth();
			},
			Y: function() {
				return dateObj.getFullYear();
			},
			y: function() {
				return format.Y().substring(2, 4);
			}
		},
		formatPieces = config.dateFormat.split('');

		for(i = 0, x = formatPieces.length; i < x; i++) {
			formattedDate += format[formatPieces[i]] ? format[formatPieces[i]]() : formatPieces[i];
		}

		return formattedDate;
	}

	function handleMonthClick() {
		// if we go too far into the past
		if(currentMonthView < 0) {
			currentYearView--;

			// start our month count at 11 (11 = december)
			currentMonthView = 11;
		}

		// if we go too far into the future
		if(currentMonthView > 11) {
			currentYearView++;

			// restart our month count (0 = january)
			currentMonthView = 0;
		}

		month.innerHTML = get.month.string(config.fullCurrentMonth) + ' ' + currentYearView;

		// rebuild the calendar
		while(body.hasChildNodes()){
			body.removeChild(body.lastChild);
		}
		body.appendChild(buildCalendar());
		bindDayLinks();

		return false;
	}

	function bindMonthLinks() {
		prevMonth.onclick = function() {
			currentMonthView--;
			return handleMonthClick();
		}

		nextMonth.onclick = function() {
			currentMonthView++;
			return handleMonthClick();
		}
	}

	// our link binding function
	function bindDayLinks() {
		var days = body.getElementsByTagName('a');

		for(i = 0, x = days.length; i < x; i++) {
			days[i].onclick = function() {
				currentDate = new Date(currentYearView, currentMonthView, this.innerHTML);
				element.value = formatDate(currentDate.getTime());
				element.onchange(element);
				close();
				return false;
			}
		}
	}

	function buildWeekdays() {
		var html = document.createDocumentFragment();
		// write out the names of each week day
		for(i = 0, x = config.weekdays.length; i < x; i++) {
			html.appendChild(build('th', {}, config.weekdays[i].substring(0, 2)));
		}
		return html;
	}

	function buildCalendar() {
		// get the first day of the month we are currently viewing
		var firstOfMonth = new Date(currentYearView, currentMonthView, config.firstDayOfWeek).getDay(),
		// get the total number of days in the month we are currently viewing
		numDays = get.month.numDays(),
		// declare our day counter
		dayCount = 0,
		weekCount = 0,
		html = document.createDocumentFragment(),
		row = build('tr');

		// print out previous month's "days"
		for(i = 1; i <= firstOfMonth; i++) {
			row.appendChild(build('td', {}, ''));
			dayCount++;
		}

		for(i = 1; i <= numDays; i++) {
			// if we have reached the end of a week, wrap to the next line
			if(dayCount == 7) {
				html.appendChild(row);
				row = build('tr');
				dayCount = 0;
				weekCount++;
			}

			// output the text that goes inside each td
			// if the day is the current day, add a class of "today"
			var today = (i == get.current.day() && currentMonthView == get.current.month.integer() && currentYearView == get.current.year());
			if (today)
			{
				currentPosition = [weekCount+1, dayCount];
			}
			row.appendChild(build('td', { className: today ? 'today' : '' }, build('a', { href: 'javascript:void(0)' }, i)));
			dayCount++;
		}

		// if we haven't finished at the end of the week, start writing out the "days" for the next month
		for(i = 1; i <= (7 - dayCount); i++) {
			row.appendChild(build('td', {}, ''));
		}

		html.appendChild(row);

		currentMaxRows = weekCount+1;

		return html;
	}

	function open() {
		element.onkeyup = element.onclick = null;

		document.onmousedown = function(e) {
			e = e || window.event;
			var target = e.target || e.srcElement;

			var parentNode = target.parentNode;
			if(target != element && parentNode != container) {
				while(parentNode != container) {
					parentNode = parentNode.parentNode;
					if(parentNode == null) {
						close();
						break;
					}
				}
			}

			if (target == element)
			{
				close(true);
			}

			e.preventDefault();
			return false;
		}

		document.onkeyup = function(e) {
			var k = e.keyCode || e.which;

			// Esc key pressed
			if (k == 27)
			{
				close();
			}
			else if ((k >= 37 && k <= 40) || k == 33 || k == 34 || k == 32 || k == 40)
			{
				// Handled in onkeypress
			}
			else if (e.target == element && element.value.match(/^\d+\/\d+\/\d{4}$/))
			{
				parseInputValue();
				handleMonthClick();
			}
		};

		document.onkeypress = function(e) {
			var k = e.keyCode || e.which;

			if (k == 33) // PgUp
			{
				e.preventDefault();
				currentMonthView--;
				return handleMonthClick();
			}
			else if (k == 34) // PgDn
			{
				e.preventDefault();
				currentMonthView++;
				return handleMonthClick();
			}
			else if (k >= 37 && k <= 40) // Arrows
			{
				e.preventDefault();
				var pos = currentPosition.slice();
				if (k == 37) { // left
					if (pos[1] == 0) return;
					pos[1]--;
				}
				else if (k == 38) { // up
					if (pos[0] <= 1) return;
					pos[0]--;
				}
				else if (k == 39) { // right
					if (pos[1] == 6) return;
					pos[1]++;
				}
				else { // down
					if (pos[0] == currentMaxRows) return;
					pos[0]++;
				}

				var table = container.getElementsByTagName('table')[0];
				var row = table.getElementsByTagName('td')[pos[0]*7+pos[1]-7];

				if (row.innerHTML == "") return;

				table.getElementsByTagName('td')[currentPosition[0]*7+currentPosition[1]-7].className = '';
				row.className = 'today';

				currentPosition = pos;
				currentDate = new Date(currentYearView, currentMonthView, row.firstChild.innerHTML);

				return false;
			}
			else if (k == 13 || k == 32)
			{
				element.value = formatDate(currentDate.getTime());
				element.onchange(element);
				close();
				e.preventDefault();
				return false;
			}
		}

		handleMonthClick();
		container.className = 'calendar';
		parseInputValue();
		handleMonthClick();
		container.focus();
	}

	function close(no_reopen = false) {
		document.onmousedown = null;
		document.onkeypress = null;
		container.className = 'calendar hidden';
		element.onclick = function (e) {
			if (no_reopen) {
				no_reopen = false;
				return;
			}

			open();
		};

		element.onkeyup = function (e) {
			var k = e.keyCode || e.which;
			var c = String.fromCharCode(e.charCode || e.keyCode);

			if (k == 40)
			{
				open();
			}
			else if (c.match(/[0-9\/]/) && element.value.match(/^\d{2}\/\d{2}\/\d{4}$/))
			{
				open();
			}
		};
	}

	function parseInputValue() {
		if (element.value)
		{
			var d = element.value.split('/').reverse();
			currentDate = new Date(parseInt(d[0], 10), parseInt(d[1], 10) - 1, parseInt(d[2], 10), 0, 0, 0, 0);
			currentYearView = get.current.year();
			currentMonthView = get.current.month.integer();
		}
	}

	function initialise(userConfig) {
		if(userConfig) {
			for(var key in userConfig) {
				if(config.hasOwnProperty(key)) {
					config[key] = userConfig[key];
				}
			}
		}

		parseInputValue();
		container = build('div', { className: 'calendar hidden' });
//		container.style.cssText = 'display: none; position: absolute; z-index: 9999;';

		var months = build('div', { className: 'months' });
		prevMonth = build('span', { className: 'prev-month' }, build('a', { href: '#' }, '&lt;'));
		nextMonth = build('span', { className: 'next-month' }, build('a', { href: '#' }, '&gt;'));
		month = build('span', { className: 'current-month' }, get.month.string(config.fullCurrentMonth) + ' ' + currentYearView);

		months.appendChild(prevMonth);
		months.appendChild(nextMonth);
		months.appendChild(month);

		var calendar = build('table', {}, build('thead', {}, build('tr', { className: 'weekdays' }, buildWeekdays())));
		body = build('tbody', {}, buildCalendar());

		calendar.appendChild(body);

		container.appendChild(months);
		container.appendChild(calendar);

		element.parentNode.style.position = 'relative';
		element.parentNode.appendChild(container);

		bindMonthLinks();

		element.onfocus = open;
		element.onblur = close;
	}

	return (function() {
		element = typeof(targetElement) == 'string' ? document.getElementById(targetElement) : targetElement;
		initialise(userConfig);
	})();
}

// Add-on for HTML5 input type="date" fallback

(function() {
	var config_fr = {
		fullCurrentMonth: true,
		dateFormat: 'd/m/Y',
		firstDayOfWeek: 0,
		weekdays: ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'],
		months: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
		suffix: { 1: 'er' },
		defaultSuffix: ''
	};

	function dateInputFallback()
	{
		var inputs = document.getElementsByTagName('input');
		var length = inputs.length;
		var enabled = false;

		for (i = 0; i < inputs.length; i++)
		{
			if (inputs[i].getAttribute('type') == 'date')
			{
				var new_input = inputs[i].cloneNode(true);
				inputs[i].type = 'hidden';
				inputs[i].removeAttribute('pattern');
				inputs[i].removeAttribute('id');
				inputs[i].removeAttribute('required');
				
				new_input.removeAttribute('name');
				new_input.setAttribute('type', 'text');
				new_input.className += ' date';
				new_input.size = 10;
				new_input.maxlength = 10;
				new_input.value = inputs[i].value.split('-').reverse().join('/');
				new_input.setAttribute('pattern', '([012][0-9]|3[01])/(0[0-9]|1[0-2])/[12][0-9]{3}');
				
				new_input.onchange = function ()
				{
					if (this.value.match(/\d{2}\/\d{2}\/\d{4}/))
						this.nextSibling.value = this.value.split('/').reverse().join('-');
					else
						this.nextSibling.value = this.value;
				};

				inputs[i].parentNode.insertBefore(new_input, inputs[i]);
				new datepickr(new_input, config_fr);
			}
		}
	}

	dateInputFallback();
} () );
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Modified src/www/admin/static/scripts/global.js from [cba439a7f0] to [054e0148e7].

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26





27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58


59

60

61
62
63
64
65
66
67
68
69
70
71
72
73





74
75
76

77
78
79
80
81
82
83
84
85
86
87





























































88
89
90
91
92
93
94
95
96
97


98
99

100
101
102
103
104
105
106
107
108
109
110






















111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

130
131
132
133
134
135
136
137
138

139

















140
141
142
143
144
145
146
147
148
149
150
151

152
153
154
155
156

157



158










159

160




161


162
163
164
165
166


167
168
169

170

171





172
173

174


175



176




177








178


179

180












181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
(function () {
	window.g = window.garradin = {
		url: window.location.href.replace(/\/admin\/.*?$/, ''),
		admin_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/'),
		static_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/static/'),
		version: document.head.querySelector('script').src.match(/\?(.*)$/)[1]

	};

	window.$ = function(selector) {
		if (!selector.match(/^[.#]?[a-z0-9_-]+$/i))
		{
			return document.querySelectorAll(selector);
		}
		else if (selector.substr(0, 1) == '.')
		{
			return document.getElementsByClassName(selector.substr(1));
		}
		else if (selector.substr(0, 1) == '#')
		{
			return document.getElementById(selector.substr(1));
		}
		else
		{
			return document.getElementsByTagName(selector);
		}
	};






	g.onload = function(callback, dom)
	{
		if (typeof dom == 'undefined')
			dom = true;

		var eventName = dom ? 'DOMContentLoaded' : 'load';

		if (document.addEventListener)
		{
			document.addEventListener(eventName, callback, false);
		}
		else
		{
			document.attachEvent('on' + eventName, callback);
		}
	};

	g.toggle = function(selector, visibility)
	{
		if (!('classList' in document.documentElement))
			return false;

		if (selector instanceof Array)
		{
			for (var i = 0; i < selector.length; i++)
			{
				g.toggle(selector[i], visibility);
			}

			return true;
		}




		var elements = $(selector);


		for (var i = 0; i < elements.length; i++)
		{
			if (!visibility)
				elements[i].classList.add('hidden');
			else
				elements[i].classList.remove('hidden');
		}

		return true;
	};

	g.script = function (file) {





		var script = document.createElement('script');
		script.type = 'text/javascript';
		script.src = this.static_url + file + '?' + this.version;

		return document.head.appendChild(script);
	};

	g.style = function (file) {
		var link = document.createElement('link');
		link.rel = 'stylesheet';
		link.type = 'text/css';
		link.href = this.static_url + file + '?' + this.version;
		return document.head.appendChild(link);
	};






























































	// From KD2fw/js/xhr.js
	g.load = function(b,d,f,e){var a=new XMLHttpRequest();if(!a||!b)return false;if(a.overrideMimeType)a.overrideMimeType('text/xml');b+=(b.indexOf('?')+1?'&':'?')+(+(new Date));a.onreadystatechange=function(){if(a.readyState!=4)return;if((s=a.status)==200){if(!d)return true;var c=a.responseText;if(f=='json'){return((j=window.JSON)&&j.parse)?j.parse(c):eval('('+c.replace(/[\n\r]/g,'')+')')}d(c)}else if(e){e(s)}};a.open('GET',b,true);a.send(null)};

	g.checkUncheck = function()
	{
		var elements = this.form.querySelectorAll('input[type=checkbox]');
		var el_length = elements.length;
		var checked = this.checked;

		for (var i = 0; i < el_length; i++)


		{
			var elm = elements[i];

			elm.checked = checked;

			if (elm.onchange && elm.name)
			{
				elm.onchange({target: elm});
			}
		}

		return true;
	};























	g.enhancePasswordField = function (field, repeat_field = null)
	{
		var show_password = document.createElement('input');
		show_password.type = 'button';
		show_password.className = 'icn action showPassword';
		show_password.title = 'Voir/cacher le mot de passe';
		show_password.value = '👁';
		show_password.onclick = function (e) {
			var pos = field.selectionStart;
			var hidden = field.type.match(/pass/i);
			field.type = hidden ? 'text' : 'password';
			this.value = !hidden ? '👁' : '⤫';
			field.classList.toggle('clearTextPassword');

			if (null !== repeat_field)
			{
				repeat_field.type = field.type;
				repeat_field.classList.toggle('clearTextPassword');
			}


			// Remettre le focus sur le champ mot de passe
			// on ne peut pas vraiment remettre le focus sur le champ
			// précis qui était utilisé avant de cliquer sur le bouton 
			// car il faudrait enregistrer les actions onfocus de tous
			// les champs de la page
			field.focus();
			field.selectionStart = field.selectionEnd = pos;
		};



















		field.parentNode.insertBefore(show_password, field.nextSibling);
	};

	var dateInputFallback = function ()
	{
		/*
		// Firefox dit implémenter date, mais ne l'implémente pas, aucun moyen de détecter ce cas
		// donc on force l'utilisation du custom datepicker de Garradin…
		var input = document.createElement('input');
		input.setAttribute('type', 'date');
		input.value = ':-)';
		input.style.position = 'absolute';

		input.style.visibility = 'hidden';
		document.body.appendChild(input);

		// If input type changed or value hasn't been sanitized then
		// the input type date element is not supported

		if (input.type !== 'text' && input.value !== ':-)')



		{










			document.body.removeChild(input);

		*/




			if (document.querySelector && !document.querySelector('input[type=date]'))


				return false;

			g.script('scripts/datepickr.js');
			g.style('scripts/datepickr.css');
		/*


		}
		else
		{

			document.body.removeChild(input);

		}*/





	};


	g.onload(dateInputFallback);






	if (!document.querySelectorAll)




	{








		return;


	}














	g.onload(function () {
		var tableActions = document.querySelectorAll('form table tfoot .actions select');

		for (var i = 0; i < tableActions.length; i++)
		{
			tableActions[i].onchange = function () {
				if (!this.form.querySelector('table tbody input[type=checkbox]:checked'))
				{
					return !window.alert("Aucune ligne sélectionnée !");
				}

				this.form.submit();
			};
		}

		// Ajouter action check/uncheck sur les checkbox de raccourci dans les tableaux
		var checkTables = document.querySelectorAll('table thead input[type=checkbox], table tfoot input[type=checkbox]');
		var l = checkTables.length;

		for (var i = 0; i < l; i++)
		{
			var masterCheck = checkTables[i];
			masterCheck.onchange = g.checkUncheck;

			var parent = masterCheck.parentNode;

			while (parent.nodeType != Node.ELEMENT_NODE || parent.tagName != 'TABLE')
			{
				parent = parent.parentNode;
			}

			var checkBoxes = parent.querySelectorAll('tbody tr input[type=checkbox]');
			var ll = checkBoxes.length;

			for (var j = 0; j < ll; j++)
			{
				checkBoxes[j].onchange = function (e) {
					var elm = e.target || this;
					var checked = elm.checked ? true : false;

					var parent = elm.parentNode;

					while (parent.nodeType != Node.ELEMENT_NODE || parent.tagName != 'TR')
					{
						parent = parent.parentNode;
					}

					if (checked)
						parent.className = parent.className.replace(/ checked$|$/, ' checked');
					else
						parent.className = parent.className.replace(/ checked/, '');
				};

				if (checkBoxes[j].checked)
				{
					checkBoxes[j].onchange({target: checkBoxes[j]});
				}
			}
		}
	});
})();





|
>




















>
>
>
>
>








<
<
|
<
<
<
<
<
















>
>
|
>
|
>












|
>
>
>
>
>
|


>
|










>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





<
<

|
|
>
>
|
<
>

<
<
<
<
|
<




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|

|

|
|
|
<
<
<
<
<
<

|
|
|
|
|
>



|





>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|


|
|
<
<
<
<
|
|
|
>
|
<
|
|
|
>
|
>
>
>
|
>
>
>
>
>
>
>
>
>
>
|
>
|
>
>
>
>
|
>
>
|
|
|
|
<
>
>
|
<
|
>
|
>
|
>
>
>
>
>


>
|
>
>

>
>
>
|
>
>
>
>
|
>
>
>
>
>
>
>
>
|
>
>
|
>

>
>
>
>
>
>
>
>
>
>
>
>
















|
<
<
<
<
<
|
<
<
<
<
<
<
|

<
<
|
<
<
<
<
<
|
<
|
<
<
<
|
<
<
<
<
<
|
<
<
<
<
|
|
<
<

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40


41





42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162


163
164
165
166
167
168

169
170




171

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204






205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244




245
246
247
248
249

250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282

283
284
285

286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354





355






356
357


358





359

360



361





362




363
364


365
(function () {
	window.g = window.garradin = {
		url: window.location.href.replace(/\/admin\/.*?$/, ''),
		admin_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/'),
		static_url: window.location.href.replace(/\/admin\/.*?$/, '/admin/static/'),
		version: document.head.querySelector('script').src.match(/\?(.*)$/)[1],
		loaded: {}
	};

	window.$ = function(selector) {
		if (!selector.match(/^[.#]?[a-z0-9_-]+$/i))
		{
			return document.querySelectorAll(selector);
		}
		else if (selector.substr(0, 1) == '.')
		{
			return document.getElementsByClassName(selector.substr(1));
		}
		else if (selector.substr(0, 1) == '#')
		{
			return document.getElementById(selector.substr(1));
		}
		else
		{
			return document.getElementsByTagName(selector);
		}
	};

	if (!document.querySelectorAll)
	{
		return;
	}

	g.onload = function(callback, dom)
	{
		if (typeof dom == 'undefined')
			dom = true;

		var eventName = dom ? 'DOMContentLoaded' : 'load';



		document.addEventListener(eventName, callback, false);





	};

	g.toggle = function(selector, visibility)
	{
		if (!('classList' in document.documentElement))
			return false;

		if (selector instanceof Array)
		{
			for (var i = 0; i < selector.length; i++)
			{
				g.toggle(selector[i], visibility);
			}

			return true;
		}
		else if (selector instanceof HTMLElement) {
			var elements = [selector];
		}
		else {
			var elements = document.querySelectorAll(selector);
		}

		for (var i = 0; i < elements.length; i++)
		{
			if (!visibility)
				elements[i].classList.add('hidden');
			else
				elements[i].classList.remove('hidden');
		}

		return true;
	};

	g.script = function (file, callback) {
		if (file in g.loaded) {
			callback();
			return;
		}

		var script = g.loaded[file] = document.createElement('script');
		script.type = 'text/javascript';
		script.src = this.static_url + file + '?' + this.version;
		script.onload = callback;
		document.head.appendChild(script);
	};

	g.style = function (file) {
		var link = document.createElement('link');
		link.rel = 'stylesheet';
		link.type = 'text/css';
		link.href = this.static_url + file + '?' + this.version;
		return document.head.appendChild(link);
	};

	g.dialog = null;

	g.openDialog = function (content) {
		if (null !== g.dialog) {
			g.closeDialog();
		}

		g.dialog = document.createElement('dialog');
		g.dialog.id = 'dialog';
		g.dialog.open = true;

		var btn = document.createElement('button');
		btn.className = 'icn-btn';
		btn.setAttribute('data-icon', '✘');
		btn.type = 'button';
		btn.innerHTML = 'Fermer';
		btn.onclick = g.closeDialog;
		g.dialog.appendChild(btn);

		if (typeof content == 'string') {
			var container = document.createElement('div');
			container.innerHTML = content;
			content = container;
		}

		content.style.opacity = g.dialog.style.opacity = 0;
		g.dialog.appendChild(content);
		g.dialog.onclick = (e) => { if (e.target == g.dialog) g.closeDialog(); };
		window.onkeyup = (e) => { if (e.key == 'Escape') g.closeDialog(); };

		document.body.appendChild(g.dialog);

		// Restore CSS defaults
		window.setTimeout(() => { g.dialog.style.opacity = ''; }, 50);
		window.setTimeout(() => { content.style.opacity = ''; }, 100);
	}

	g.openFrameDialog = function (url) {
		var iframe = document.createElement('iframe');
		iframe.src = url;
		iframe.name = 'dialog';
		iframe.frameborder = '0';
		iframe.scrolling = 'yes';
		iframe.width = iframe.height = 0;
		iframe.onload = () => { iframe.contentWindow.onkeyup = (e) => { if (e.key == 'Escape') g.closeDialog(); };}

		g.openDialog(iframe);
	};

	g.closeDialog = function () {
		if (null === g.dialog) {
			return;
		}

		var d = g.dialog;
		d.style.opacity = 0;
		window.onkeyup = g.dialog = null;

		window.setTimeout(() => { d.parentNode.removeChild(d); }, 500);
	}

	// From KD2fw/js/xhr.js
	g.load = function(b,d,f,e){var a=new XMLHttpRequest();if(!a||!b)return false;if(a.overrideMimeType)a.overrideMimeType('text/xml');b+=(b.indexOf('?')+1?'&':'?')+(+(new Date));a.onreadystatechange=function(){if(a.readyState!=4)return;if((s=a.status)==200){if(!d)return true;var c=a.responseText;if(f=='json'){return((j=window.JSON)&&j.parse)?j.parse(c):eval('('+c.replace(/[\n\r]/g,'')+')')}d(c)}else if(e){e(s)}};a.open('GET',b,true);a.send(null)};

	g.checkUncheck = function()
	{


		var checked = this.checked;
		this.form.querySelectorAll('tbody input[type=checkbox]').forEach((elm) => {
			elm.checked = checked;
			elm.dispatchEvent(new Event("change"));
		});


		this.form.querySelectorAll('thead input[type=checkbox], tfoot input[type=checkbox]').forEach((elm) => {
			elm.checked = checked;




		});


		return true;
	};

	g.togglePasswordVisibility = (field, repeat, show) => {
		if (typeof show == 'undefined') {
			show = field.type.toLowerCase() == 'password';
		}

		var btn = field.nextSibling;

		if (!btn) {
			throw Error('button not found');
		}

		field.type = show ? 'text' : 'password';
		btn.dataset.icon = !show ? '👁' : '⤫';
		btn.innerHTML = !show ? 'Voir le mot de passe' : 'Cacher le mot de passe';
		field.classList.toggle('clearTextPassword', !show);

		if (repeat) {
			repeat.type = field.type;
			repeat.classList.toggle('clearTextPassword', !show);
		}
	};

	g.enhancePasswordField = function (field, repeat_field)
	{
		var show_password = document.createElement('button');
		show_password.type = 'button';
		show_password.className = 'icn-btn';

		field.parentNode.insertBefore(show_password, field.nextSibling);







		g.togglePasswordVisibility(field, repeat_field, false);

		show_password.onclick = function (e) {
			var pos = field.selectionStart;

			g.togglePasswordVisibility(field, repeat_field);

			// Remettre le focus sur le champ mot de passe
			// on ne peut pas vraiment remettre le focus sur le champ
			// précis qui était utilisé avant de cliquer sur le bouton
			// car il faudrait enregistrer les actions onfocus de tous
			// les champs de la page
			field.focus();
			field.selectionStart = field.selectionEnd = pos;
		};
	};

	g.enhanceDateField = (input) => {
		var span = document.createElement('span');
		span.className = 'datepicker-parent';
		var btn = document.createElement('button');
		var cal = null;
		btn.className = 'icn-btn';
		btn.setAttribute('data-icon', '📅');
		btn.type = 'button';
		btn.onclick = () => {
			g.script('scripts/datepicker2.js', () => {
				if (null == cal) {
					cal = new DatePicker(btn, input, {lang: 'fr', format: 1});
					cal.open();
				}
			});
		};
		span.appendChild(btn);
		input.parentNode.insertBefore(span, input.nextSibling);
	};

	g.current_list_input = null;





	g.inputListSelected = function(value, label) {
		var i = g.current_list_input;

		if (!i) {
			throw Error('Parent input list not found');

		}

		var multiple = i.firstChild.getAttribute('data-multiple');
		var name = i.firstChild.getAttribute('data-name');

		var span = document.createElement('span');
		span.className = 'label';
		span.innerHTML = '<input type="hidden" name="' + name + '[' + value + ']" value="' + label + '" />' + label;

		// Add delete button
		if (parseInt(multiple, 10) == 1) {
			var btn = document.createElement('button');
			btn.className = 'icn-btn';
			btn.type = 'button';
			btn.setAttribute('data-icon', '✘');
			btn.onclick = () => span.parentNode.removeChild(span);
			span.appendChild(btn);
		}
		else if (old = i.querySelector('span')) {
			i.removeChild(old);
		}

		i.appendChild(span);
		g.closeDialog();
		i.firstChild.focus();
	};

	g.formatMoney = (v) => {
		if (!v) {
			return '0,00';
		}

		var s = v < 0 ? '-' : '';

		v = '' + Math.abs(v);
		return s + (v.substr(0, v.length-2) || '0') + ',' + ('00' + v).substr(-2);
	};


	g.getMoneyAsInt = (v) => {
		v = v.replace(/[^0-9.,]/, '');
		if (v.length == 0) return;

		v = v.split(/[,.]/);
		var d = v.length == 2 ? v[1] : '0';
		v = v[0] + (d + '00').substr(0, 2);
		v = parseInt(v, 10);
		return v;
	};

	// Focus on first form input when loading the page
	g.onload(() => {
		if (!document.activeElement || document.activeElement.tagName.toLowerCase() == 'body') {
			let form = document.querySelector('form[data-focus]');

			if (!form) {
				return;
			}

			var i = form.querySelector(form.dataset.focus == 1 ? '[name]' : form.dataset.focus);
			i.focus();
		}
	}, 'dom');

	// Sélecteurs de listes
	g.onload(() => {
		var inputs = $('form .input-list > button');

		inputs.forEach((i) => {
			i.onclick = () => {
				g.current_list_input = i.parentNode;
				g.openFrameDialog(i.value);
				return false;
			};
		});

		var multiples = $('form .input-list span button');

		multiples.forEach((btn) => {
			btn.onclick = () => btn.parentNode.parentNode.removeChild(btn.parentNode);
		});
	});

	g.onload(() => {
		document.querySelectorAll('input[data-input="date"]').forEach((e) => {
			g.enhanceDateField(e);
		});
	});

	// To be able to select a whole table line just by clicking the row
	g.onload(function () {
		var tableActions = document.querySelectorAll('form table tfoot .actions select');

		for (var i = 0; i < tableActions.length; i++)
		{
			tableActions[i].onchange = function () {
				if (!this.form.querySelector('table tbody input[type=checkbox]:checked'))
				{
					return !window.alert("Aucune ligne sélectionnée !");
				}

				this.form.submit();
			};
		}

		// Ajouter action check/uncheck sur les checkbox de raccourci dans les tableaux
		document.querySelectorAll('table thead input[type=checkbox], table tfoot input[type=checkbox]').forEach((elm) => {





			elm.addEventListener('change', g.checkUncheck);






		});



		document.querySelectorAll('table tbody input[type=checkbox]').forEach((elm) => {





			elm.addEventListener('change', () => {

				elm.parentNode.parentNode.classList.toggle('checked', elm.checked);



			});





		});




	});



})();

Modified src/www/admin/static/scripts/password.js from [ae6eb9a86f] to [3ab672a055].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	{
		pw_elm = (typeof password == 'string') ? document.getElementById(password) : password;
		pw2_elm = (typeof password2 == 'string') ? document.getElementById(password2) : password2;
		suggest_elm = (typeof suggest == 'string') ? document.getElementById(suggest) : suggest;

		g.enhancePasswordField(pw_elm, pw2_elm);

		suggest_elm.size = suggest_elm.value.length;

		suggest_elm.onclick = function () {
	        pw_elm.value = this.value;
	        pw2_elm.value = this.value;
	        this.select();
	        checkPasswordStrength();
	        checkPasswordMatch();







|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	{
		pw_elm = (typeof password == 'string') ? document.getElementById(password) : password;
		pw2_elm = (typeof password2 == 'string') ? document.getElementById(password2) : password2;
		suggest_elm = (typeof suggest == 'string') ? document.getElementById(suggest) : suggest;

		g.enhancePasswordField(pw_elm, pw2_elm);

		suggest_elm.style.width = suggest_elm.value.length + 'ch';

		suggest_elm.onclick = function () {
	        pw_elm.value = this.value;
	        pw2_elm.value = this.value;
	        this.select();
	        checkPasswordStrength();
	        checkPasswordMatch();
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
		match_elm.className = 'password_check';

		pw2_elm.parentNode.appendChild(match_elm);

		pw_elm.onkeyup = checkPasswordStrength;
		pw_elm.onchange = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw_elm.onblur = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw2_elm.onkeypress = checkPasswordMatch;
		pw2_elm.onblur = checkPasswordMatch;
		pw2_elm.onchange = checkPasswordMatch;

		pw_elm.form.addEventListener('submit', function (e) {
			if (pw_elm.value == '') return true;
			if (scorePassword(pw_elm.value) <= 30 && !window.confirm("Êtes-vous sûr de vouloir utiliser un mot de passe aussi mauvais que ça ?"))
			{
				e = e || window.event;
				if(e.preventDefault)







<
|
<







32
33
34
35
36
37
38

39

40
41
42
43
44
45
46
		match_elm.className = 'password_check';

		pw2_elm.parentNode.appendChild(match_elm);

		pw_elm.onkeyup = checkPasswordStrength;
		pw_elm.onchange = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw_elm.onblur = function () { checkPasswordStrength(); checkPasswordMatch(); };

		pw2_elm.onkeyup = checkPasswordMatch;


		pw_elm.form.addEventListener('submit', function (e) {
			if (pw_elm.value == '') return true;
			if (scorePassword(pw_elm.value) <= 30 && !window.confirm("Êtes-vous sûr de vouloir utiliser un mot de passe aussi mauvais que ça ?"))
			{
				e = e || window.event;
				if(e.preventDefault)

Modified src/www/admin/static/scripts/query_builder.min.js from [f2e9e173b4] to [a0102fba69].

1
!function(){var e=function(e){this.columns=e};(window.SQLQueryBuilder=e).prototype.loadDefaultOperators=function(){for(var e in this.operators={"= ?":this.__("is equal to"),"!= ?":this.__("is not equal to"),"IN (??)":this.__("is equal to one of"),"NOT IN (??)":this.__("is not equal to one of"),"> ?":this.__("is greater than"),">= ?":this.__("is greater than or equal to"),"< ?":this.__("is less than"),"<= ?":this.__("is less than or equal to"),"BETWEEN ? AND ?":this.__("is between"),"NOT BETWEEN ? AND ?":this.__("is not between"),"IS NULL":this.__("is null"),"IS NOT NULL":this.__("is not null"),"LIKE ?%":this.__("begins with"),"NOT LIKE ?%":this.__("doesn't begin with"),"LIKE %?":this.__("ends with"),"NOT LIKE %?":this.__("doesn't end with"),"LIKE %?%":this.__("contains"),"NOT LIKE %?%":this.__("doesn't contain"),"&":this.__("matches one of"),"= 1":this.__("is true"),"= 0":this.__("is false")},this.types_operators={integer:["= ?","!= ?","IN (??)","NOT IN (??)","> ?",">= ?","< ?","<= ?","BETWEEN ? AND ?","NOT BETWEEN ? AND ?"],enum:["= ?","!= ?","IN (??)","NOT IN (??)"],boolean:["= 1","= 0"],text:["= ?","!= ?","IN (??)","NOT IN (??)","LIKE ?%","NOT LIKE ?%","LIKE %?","NOT LIKE %?","LIKE %?%","NOT LIKE %?%"],bitwise:["&"]},this.types_operators){var t={};for(var i in this.types_operators[e]){var o=this.types_operators[e][i];t[o]=this.operators[o]}this.types_operators[e]=t}this.types_operators.date=JSON.parse(JSON.stringify(this.types_operators.integer)),delete this.types_operators.date["<= ?"],delete this.types_operators.date[">= ?"],this.types_operators.date["< ?"]=this.__("before"),this.types_operators.date["> ?"]=this.__("after"),this.types_operators.datetime=this.types_operators.date},e.prototype.__=function(e){return e},e.prototype.init=function(e){this.parent=e;var t={"":"---"};for(column in this.columns)t[column]=this.columns[column].label;this.columnSelect=this.buildSelect(t)},e.prototype.addGroup=function(t,e){var i=document.createElement("fieldset"),o=document.createElement("legend"),n=this.buildSelect({AND:this.__("Matches ALL of the following conditions:"),OR:this.__("Matches ANY of the following conditions:"),ADD:this.__("Add a new set of conditions below this one"),DEL:this.__("Remove this set of conditions")});n.onfocus=function(){this.oldValue=this.value},n.value=e;var r=this;n.onchange=function(){if("DEL"==this.value){if(1==t.childNodes.length)return void(this.value=this.oldValue);t.removeChild(i)}else if("ADD"==this.value){var e=r.addGroup(t,"AND");r.addRow(e),this.value=this.oldValue}},o.appendChild(n),i.appendChild(o);var s=document.createElement("table");return i.appendChild(s),t.appendChild(i),i},e.prototype.addRow=function(e,t){var i=e.getElementsByTagName("table")[0],o=document.createElement("tr");(s=document.createElement("td")).className="buttons";var n,r=this;(n=this.buildInput("button","+")).onclick=function(){r.addRow(function(e,t){for(;(e=e.parentElement)&&!(e.matches||e.matchesSelector).call(e,t););return e}(this,"fieldset"),this.parentNode.parentNode)},s.appendChild(n),(n=this.buildInput("button","-")).onclick=function(){r.deleteRow(this.parentNode.parentNode)},s.appendChild(n),o.appendChild(s),(s=document.createElement("td")).className="column";var s,a=this.columnSelect.cloneNode(!0);return a.onchange=function(){return r.switchColumn(this)},s.appendChild(a),o.appendChild(s),(s=document.createElement("td")).className="operator",o.appendChild(s),(s=document.createElement("td")).className="values",o.appendChild(s),void 0===t?i.appendChild(o):i.insertBefore(o,t.nextSibling),o},e.prototype.deleteRow=function(e){e.parentNode.childNodes.length<=1||e.parentNode.removeChild(e)},e.prototype.switchColumn=function(e){var t=e.parentNode.parentNode;if(t.childNodes[2].innerHTML="",t.childNodes[3].innerHTML="",e.value){var i=this.addOperator(t,this.columns[e.value]);i.value=i.children[1].value,this.switchOperator(i,null)}},e.prototype.addOperator=function(e,t){var i=this.types_operators[t.type],o={"":"---"};for(var n in t.null&&(i["IS NULL"]=this.operators["IS NULL"],i["IS NOT NULL"]=this.operators["IS NOT NULL"]),i)o[n]=i[n];var r=this.buildSelect(o),s=this;return r.onchange=function(){return s.switchOperator(this)},e.childNodes[2].appendChild(r),r},e.prototype.switchOperator=function(e,t){var i=e.parentNode.parentNode;i.childNodes[3].innerHTML="";var o=i.childNodes[3],n=i.childNodes[1].firstChild,r=e.value,s=this.columns[n.value];if(r){var a=1,l=!1,d=null,h=r.match(/\?/g);if(h&&r.match(/\?\?/))a=t?t.length:3,l=!0;else if(h&&1<=h.length)a=h.length;else{if("bitwise"!=s.type||"&"!=r)return;a=1}for(var u=0;u<a;u++)if(d=this.addMatchField(o,d,s,r),"bitwise"==s.type&&t)for(var p=0;p<s.values.length;p++)o.querySelectorAll("input")[p].checked=-1!=t.indexOf(p.toString());else t&&(d.value=t[u]);if(l){(c=this.buildInput("button","-")).onclick=function(){this.parentNode.childNodes.length<=3||(this.parentNode.removeChild(this.previousSibling),this.parentNode.removeChild(this.previousSibling))},o.appendChild(c);var c=this.buildInput("button","+"),v=this;c.onclick=function(){v.addMatchField(o,this.previousSibling.previousSibling,s,r)},o.appendChild(c)}}},e.prototype.addMatchField=function(e,t,i,o){if("enum"==i.type)var n=this.buildSelect(i.values);else if("bitwise"==i.type){n=document.createElement("span");for(var r in i.values){var s=this.buildInput("checkbox",r),a=document.createElement("label");a.appendChild(s),a.appendChild(document.createTextNode(" "+i.values[r])),n.appendChild(a.cloneNode(!0))}}else n=this.buildInput(i.type,"",i);return n=e.insertBefore(n,t?t.nextSibling:null),t&&e.insertBefore(document.createElement("br"),n),n},e.prototype.buildInput=function(e,t,i){var o=document.createElement("input");return o.type="integer"==e?"number":e,o.value=t,o},e.prototype.buildSelect=function(e){var t=document.createElement("select");for(var i in e){var o=document.createElement("option");o.value=i,o.innerHTML=e[i],t.appendChild(o)}return t},e.prototype.import=function(e){for(var t in e)if(0!=e[t].conditions.length){var i=this.addGroup(this.parent,e[t].operator);for(var o in e[t].conditions){var n=e[t].conditions[o],r=this.addRow(i);r.childNodes[1].firstChild.value=n.column;var s=this.addOperator(r,this.columns[n.column]);s.value=n.operator,this.switchOperator(s,n.values)}}},e.prototype.export=function(){var e=this.parent.querySelectorAll("table"),t=[];for(var i in e)if(e.hasOwnProperty(i)){for(var o=(i=e[i]).rows,n=[],r=0;r<o.length;r++){var s=o[r];if(s.getElementsByTagName("select")[0].value){var a=Array.prototype.slice.call(s.cells[3].querySelectorAll("input, select")).map(function(e){return"checkbox"==e.type?e.checked?e.value:null:"button"!=e.type?e.value:void 0});a=a.filter(function(e){return null!==e});var l={column:s.cells[1].firstChild.value,operator:s.cells[2].firstChild.value,values:a};l.operator&&(l.operator.match(/\?\?/)&&(l.values=l.values.slice(0,-2)),n.push(l))}}t.push({operator:i.parentNode.firstChild.firstChild.value,conditions:n})}return t}}();
|
1
!function(){var e=function(e){this.columns=e};(window.SQLQueryBuilder=e).prototype.loadDefaultOperators=function(){for(var e in this.operators={"= ?":this.__("is equal to"),"!= ?":this.__("is not equal to"),"IN (??)":this.__("is equal to one of"),"NOT IN (??)":this.__("is not equal to one of"),"> ?":this.__("is greater than"),">= ?":this.__("is greater than or equal to"),"< ?":this.__("is less than"),"<= ?":this.__("is less than or equal to"),"BETWEEN ? AND ?":this.__("is between"),"NOT BETWEEN ? AND ?":this.__("is not between"),"IS NULL":this.__("is null"),"IS NOT NULL":this.__("is not null"),"LIKE ?%":this.__("begins with"),"NOT LIKE ?%":this.__("doesn't begin with"),"LIKE %?":this.__("ends with"),"NOT LIKE %?":this.__("doesn't end with"),"LIKE %?%":this.__("contains"),"NOT LIKE %?%":this.__("doesn't contain"),"&":this.__("matches one of"),"= 1":this.__("is true"),"= 0":this.__("is false")},this.types_operators={integer:["= ?","!= ?","IN (??)","NOT IN (??)","> ?",">= ?","< ?","<= ?","BETWEEN ? AND ?","NOT BETWEEN ? AND ?"],enum:["= ?","!= ?","IN (??)","NOT IN (??)"],boolean:["= 1","= 0"],text:["= ?","!= ?","IN (??)","NOT IN (??)","LIKE ?%","NOT LIKE ?%","LIKE %?","NOT LIKE %?","LIKE %?%","NOT LIKE %?%"],bitwise:["&"]},this.types_operators){var t={};for(var i in this.types_operators[e]){var o=this.types_operators[e][i];t[o]=this.operators[o]}this.types_operators[e]=t}this.types_operators.date=JSON.parse(JSON.stringify(this.types_operators.integer)),delete this.types_operators.date["<= ?"],delete this.types_operators.date[">= ?"],this.types_operators.date["< ?"]=this.__("before"),this.types_operators.date["> ?"]=this.__("after"),this.types_operators.datetime=this.types_operators.date},e.prototype.__=function(e){return e},e.prototype.init=function(e){this.parent=e;var t={"":"---"};for(column in this.columns)t[column]=this.columns[column].label;this.columnSelect=this.buildSelect(t)},e.prototype.addGroup=function(t,e){var i=document.createElement("fieldset"),o=document.createElement("legend"),n=this.buildSelect({AND:this.__("Matches ALL of the following conditions:"),OR:this.__("Matches ANY of the following conditions:"),ADD:this.__("Add a new set of conditions below this one"),DEL:this.__("Remove this set of conditions")});n.onfocus=function(){this.oldValue=this.value},n.value=e;var r=this;n.onchange=function(){if("DEL"==this.value){if(1==t.childNodes.length)return void(this.value=this.oldValue);t.removeChild(i)}else if("ADD"==this.value){var e=r.addGroup(t,"AND");r.addRow(e),this.value=this.oldValue}},o.appendChild(n),i.appendChild(o);var s=document.createElement("table");return i.appendChild(s),t.appendChild(i),i},e.prototype.addRow=function(e,t){var i=e.getElementsByTagName("table")[0],o=document.createElement("tr");(s=document.createElement("td")).className="buttons";var n,r=this;(n=this.buildInput("button","+")).onclick=function(){r.addRow(function(e,t){for(;(e=e.parentElement)&&!(e.matches||e.matchesSelector).call(e,t););return e}(this,"fieldset"),this.parentNode.parentNode)},s.appendChild(n),(n=this.buildInput("button","-")).onclick=function(){r.deleteRow(this.parentNode.parentNode)},s.appendChild(n),o.appendChild(s),(s=document.createElement("td")).className="column";var s,l=this.columnSelect.cloneNode(!0);return l.onchange=function(){return r.switchColumn(this)},s.appendChild(l),o.appendChild(s),(s=document.createElement("td")).className="operator",o.appendChild(s),(s=document.createElement("td")).className="values",o.appendChild(s),void 0===t?i.appendChild(o):i.insertBefore(o,t.nextSibling),o},e.prototype.deleteRow=function(e){e.parentNode.childNodes.length<=1||e.parentNode.removeChild(e)},e.prototype.switchColumn=function(e){var t=e.parentNode.parentNode;if(t.childNodes[2].innerHTML="",e.value){var i=this.addOperator(t,this.columns[e.value]);i.value=i.children[1].value,this.switchOperator(i,null)}},e.prototype.addOperator=function(e,t){var i=this.types_operators[t.type],o={"":"---"};for(var n in t.null&&(i["IS NULL"]=this.operators["IS NULL"],i["IS NOT NULL"]=this.operators["IS NOT NULL"]),i)o[n]=i[n];var r=this.buildSelect(o),s=this;return r.onchange=function(){return s.switchOperator(this)},e.childNodes[2].appendChild(r),r},e.prototype.switchOperator=function(e,t){var i=e.parentNode.parentNode;!t&&i.childNodes[3].firstChild&&(t=[i.childNodes[3].firstChild.value]),i.childNodes[3].innerHTML="";var o=i.childNodes[3],n=i.childNodes[1].firstChild,r=e.value,s=this.columns[n.value];if(r){var l=1,a=!1,d=null,h=r.match(/\?/g);if(h&&r.match(/\?\?/))l=t?t.length:1,a=!0;else if(h&&1<=h.length)l=h.length;else{if("bitwise"!=s.type||"&"!=r)return;l=1}for(var u=0;u<l;u++)if(d=this.addMatchField(o,d,s,r),"bitwise"==s.type&&t)for(var p=0;p<s.values.length;p++)o.querySelectorAll("input")[p].checked=-1!=t.indexOf(p.toString());else t&&(d.value=t[u]);if(a){(c=this.buildInput("button","-")).onclick=function(){this.parentNode.childNodes.length<=3||(this.parentNode.removeChild(this.previousSibling),this.parentNode.removeChild(this.previousSibling))},o.appendChild(c);var c=this.buildInput("button","+"),f=this;c.onclick=function(){f.addMatchField(o,this.previousSibling.previousSibling,s,r)},o.appendChild(c)}}},e.prototype.addMatchField=function(e,t,i,o){if("enum"==i.type)var n=this.buildSelect(i.values);else if("bitwise"==i.type){n=document.createElement("span");for(var r in i.values){var s=this.buildInput("checkbox",r),l=document.createElement("label");l.appendChild(s),l.appendChild(document.createTextNode(" "+i.values[r])),n.appendChild(l.cloneNode(!0))}}else n=this.buildInput(i.type,"",i);return n=e.insertBefore(n,t?t.nextSibling:null),t&&e.insertBefore(document.createElement("br"),n),n},e.prototype.buildInput=function(e,t,i){var o=document.createElement("input");return o.type="integer"==e?"number":e,o.value=t,o},e.prototype.buildSelect=function(e){var t=document.createElement("select");for(var i in e){var o=document.createElement("option");o.value=i,o.innerHTML=e[i],t.appendChild(o)}return t},e.prototype.import=function(e){for(var t in e)if(0!=e[t].conditions.length){var i=this.addGroup(this.parent,e[t].operator);for(var o in e[t].conditions){var n=e[t].conditions[o],r=this.addRow(i);r.childNodes[1].firstChild.value=n.column;var s=this.addOperator(r,this.columns[n.column]);s.value=n.operator,this.switchOperator(s,n.values)}}},e.prototype.export=function(){var e=this.parent.querySelectorAll("table"),t=[];for(var i in e)if(e.hasOwnProperty(i)){for(var o=(i=e[i]).rows,n=[],r=0;r<o.length;r++){var s=o[r];if(s.getElementsByTagName("select")[0].value){var l=Array.prototype.slice.call(s.cells[3].querySelectorAll("input, select")).map(function(e){return"checkbox"==e.type?e.checked?e.value:null:"button"!=e.type?e.value:void 0});l=l.filter(function(e){return null!==e});var a={column:s.cells[1].firstChild.value,operator:s.cells[2].firstChild.value,values:l};a.operator&&n.push(a)}}t.push({operator:i.parentNode.firstChild.firstChild.value,conditions:n})}return t}}();

Added src/www/admin/static/scripts/selector.js version [049f79c975].







































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
RegExp.escape = function(string) {
  return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
};

function normalizeString(str) {
	return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
}

var buttons = document.querySelectorAll('button');

buttons.forEach((e) => {
	e.onclick = () => {
		window.parent.g.inputListSelected(e.value, e.getAttribute('data-label'));
	};
});

var rows = document.querySelectorAll('table tr');

rows.forEach((e, k) => {
	e.classList.add('clickable');
	var l = e.querySelector('td').innerText + ' ' + e.querySelector('th').innerText;
	e.setAttribute('data-search-label', normalizeString(l));

	e.querySelector('button').onfocus = () => {
		rows.forEach((r) => {
			if (r == e) {
				return;
			}

			r.classList.remove('focused');
		});

		e.classList.add('focused');
	};

	e.onclick = (evt) => {
		if (evt.target.tagName && evt.target.tagName == 'BUTTON') {
			return;
		}

		e.querySelector('button').click();
	};
});

document.onkeydown = (evt) => {
	let focus = document.activeElement;
	let new_focus;

	// Get first element
	if (focus.tagName != 'BUTTON') {
		new_focus = document.querySelector('table tr');
	}

	if (evt.key == 'ArrowUp' && !new_focus) {
		let idx = focus.parentNode.parentNode.dataset.idx - 1;

		if (idx == 0) {
			return true;
		}

		new_focus = rows[idx - 1];
	}
	else if (evt.key == 'ArrowDown' && !new_focus) {
		let idx = focus.parentNode.parentNode.dataset.idx - 1;

		if (idx >= rows.length - 1) {
			return true;
		}

		new_focus = rows[idx + 1];
	}
	else {
		new_focus = null;
	}

	if (!new_focus) {
		return true;
	}

	new_focus.querySelector('button').focus();
	return false;
};

buttons[0].focus();

var q = document.getElementById('lookup');

if (q) {
	q.onkeyup = (e) => {
		var query = new RegExp(RegExp.escape(normalizeString(q.value)), 'i');

		rows.forEach((elm) => {
			if (elm.getAttribute('data-search-label').match(query)) {
				elm.style.display = null;
			}
			else {
				elm.style.display = 'none';
			}
		});

		return false;
	};

	q.focus();
}

var o = document.getElementById('f_typed_only_0');

if (o) {
	o.onchange = () => {
		let s = new URLSearchParams(window.location.search);
		s.set("all", o.checked ? 0 : 1);
		window.location.search = s.toString();
	};
}

Modified src/www/admin/static/scripts/skel_editor.js from [0e1522caf5] to [6b35de296d].

1
2
3
4
5
6
7
8
9
10
11
12
(function (){
	g.style('scripts/skel_editor.css');
	g.script('scripts/code_editor.min.js').onload = function ()
	{
		var save_btn = document.querySelector('input[name=save]');
		save_btn.type = 'hidden';

		var code = new codeEditor('f_content');

		code.params.lang = {
			search: "Texte à chercher ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace: "Texte pour le remplacement ?\n(utiliser $1, $2... pour les captures d'expression régulière)",


|

|







1
2
3
4
5
6
7
8
9
10
11
12
(function (){
	g.style('scripts/skel_editor.css');
	g.script('scripts/code_editor.min.js', function ()
	{
		var save_btn = document.querySelector('[name=save]');
		save_btn.type = 'hidden';

		var code = new codeEditor('f_content');

		code.params.lang = {
			search: "Texte à chercher ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace: "Texte pour le remplacement ?\n(utiliser $1, $2... pour les captures d'expression régulière)",
191
192
193
194
195
196
197
198
199
				m.className = msg.className;
				toolbar.appendChild(m);
				msg.parentNode.removeChild(msg);
			}

			window.location.hash = '';
		}
	};
}());







|

191
192
193
194
195
196
197
198
199
				m.className = msg.className;
				toolbar.appendChild(m);
				msg.parentNode.removeChild(msg);
			}

			window.location.hash = '';
		}
	});
}());

Modified src/www/admin/static/scripts/wiki_editor.css from [d04c8ee367] to [30c3c98262].

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
}

nav.te {
    margin-bottom: .5em;
    height: 30px;
}

nav.te input {
    margin: 4px .5em;
    padding: 0;
    height: 24px;
    border: none;
    border-bottom: 1px solid #999;
    border-radius: .2em;
    cursor: pointer;
    background: transparent no-repeat center center;
    display: inline-block;
    vertical-align: bottom;
    transition: all .2s;


}

nav.te input:hover { background-color: #fff; }

nav.te .bold, nav.te .italic, nav.te .title, nav.te .link {
    font-family: Georgia, "Times New Roman", serif;
}

nav.te .bold { font-weight: bold; }
nav.te .italic { font-style: italic; }
nav.te .link { text-decoration: underline; color: blue; }
nav.te .title { font-size: 1.2em; }

nav.te .ext {
    border: 1px solid #999;
    box-shadow: 2px 2px 5px #999;
    min-width: 24px;
}

nav.te .fullscreen {
    text-indent: -70em;

    overflow: hidden; 
}

nav.te .icnl {
    font-size: 18px;
}

nav.te .ext.icnl {







|
<
<
<
|
<
<

|



>
>


<
<







<
<
<
<
<
<
<



>
|







16
17
18
19
20
21
22
23



24


25
26
27
28
29
30
31
32
33


34
35
36
37
38
39
40







41
42
43
44
45
46
47
48
49
50
51
52
}

nav.te {
    margin-bottom: .5em;
    height: 30px;
}

nav.te button {



    text-decoration: none;


    cursor: pointer;
    background: #eee no-repeat center center;
    display: inline-block;
    vertical-align: bottom;
    transition: all .2s;
    border: 1px solid #999;
    box-shadow: 2px 2px 5px #999;
}



nav.te .bold, nav.te .italic, nav.te .title, nav.te .link {
    font-family: Georgia, "Times New Roman", serif;
}

nav.te .bold { font-weight: bold; }
nav.te .italic { font-style: italic; }
nav.te .link { text-decoration: underline; color: blue; }








nav.te .fullscreen {
    text-indent: -70em;
    width: 32px;
    overflow: hidden;
}

nav.te .icnl {
    font-size: 18px;
}

nav.te .ext.icnl {
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    margin-left: 2em;
}

.textEditor.fullscreen nav.te .fullscreen {
    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABNTVFOTlBOTlBLB/faAAAAA3RSTlMAOsPdsomtAAAAQElEQVQI12NIEWCRZNi5gTePIe8D/08G6Q/8Txj0P/B/YVj/gf8fAzawHyQhD1ICJvI/8O9k2FnAl8eQosAiCQCgixb13aKGIwAAAABJRU5ErkJggg==");
}

.textEditor nav input.close {
    display: none;
    float: right;
}

.textEditor nav input.reload {
    display: none;
    float: left;
}

.textEditor.fullscreen {
    position: fixed;
    top: 0;







|




|







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
    margin-left: 2em;
}

.textEditor.fullscreen nav.te .fullscreen {
    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABNTVFOTlBOTlBLB/faAAAAA3RSTlMAOsPdsomtAAAAQElEQVQI12NIEWCRZNi5gTePIe8D/08G6Q/8Txj0P/B/YVj/gf8fAzawHyQhD1ICJvI/8O9k2FnAl8eQosAiCQCgixb13aKGIwAAAABJRU5ErkJggg==");
}

.textEditor nav button.close {
    display: none;
    float: right;
}

.textEditor nav button.reload {
    display: none;
    float: left;
}

.textEditor.fullscreen {
    position: fixed;
    top: 0;
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
    height: 90%;
}

.textEditor.iframe textarea {
    display: none;
}

.textEditor.iframe nav input {
    display: none;
}

.textEditor.iframe nav input.close, .textEditor.iframe nav input.reload {
    display: inline-block;
}

.textEditor iframe {
    border: none;
    background: #eee;
    border-radius: .5em;







|



|







99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
    height: 90%;
}

.textEditor.iframe textarea {
    display: none;
}

.textEditor.iframe nav button {
    display: none;
}

.textEditor.iframe nav button.close, .textEditor.iframe nav button.reload {
    display: inline-block;
}

.textEditor iframe {
    border: none;
    background: #eee;
    border-radius: .5em;

Modified src/www/admin/static/scripts/wiki_editor.js from [e1b49b3d06] to [2d8c9cc5c3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function () {
	var wiki_id = window.location.search.match(/id=(\d+)/)[1];

	g.onload(function () {
		g.style('scripts/wiki_editor.css');

		g.script('scripts/text_editor.min.js').onload = function () {
			var t = new textEditor('f_contenu');
			t.parent = t.textarea.parentNode;

			var toolbar = document.createElement('nav');
			toolbar.className = 'te';

			var toggleFullscreen = function (e)






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function () {
	var wiki_id = window.location.search.match(/id=(\d+)/)[1];

	g.onload(function () {
		g.style('scripts/wiki_editor.css');

		g.script('scripts/text_editor.min.js', function () {
			var t = new textEditor('f_contenu');
			t.parent = t.textarea.parentNode;

			var toolbar = document.createElement('nav');
			toolbar.className = 'te';

			var toggleFullscreen = function (e)
118
119
120
121
122
123
124
125
126
127




128

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
				t.parent.className = t.parent.className.replace(/ iframe$/, '');
				t.iframe.className = 'hidden';
			};


			var appendButton = function (name, title, action, altTitle)
			{
				var btn = document.createElement('input');
				btn.type = 'button';
				btn.title = altTitle ? altTitle : title;




				btn.value = title;

				btn.className = name;
				btn.onclick = function () { action.call(); return false; };

				toolbar.appendChild(btn);
				return btn;
			};

			var wrapTags = function (left, right)
			{
				t.wrapSelection(t.getSelection(), left, right);
				return true;
			};

			appendButton('title', "== Titre", function () { wrapTags("== ", ""); } );
			appendButton('bold', '**gras**', function () { wrapTags('**', '**'); } );
			appendButton('italic', "''italique''", function () { wrapTags("''", "''"); } );
			appendButton('link', "[[lien|http://]]", function () { 
				if (url = window.prompt('Adresse URL ?')) 
					wrapTags("[[", "|" + url + ']]'); 
			} );
			appendButton('icnl file', "📎", openFileInsert, 'Insérer fichier / image');

			appendButton('ext icnl preview', '⎙', openPreview, 'Prévisualiser');

			appendButton('ext icnl help', '❓', openSyntaxHelp, 'Aide sur la syntaxe');
			appendButton('ext fullscreen', 'Plein écran', toggleFullscreen, 'Plein écran');
			appendButton('ext close', 'Fermer', closeIFrame);
			
			t.parent.insertBefore(toolbar, t.parent.firstChild);

			t.shortcuts.push({key: 'F11', callback: toggleFullscreen});
			t.shortcuts.push({ctrl: true, key: 'b', callback: function () { return wrapTags('**', '**'); } });
			t.shortcuts.push({ctrl: true, key: 'g', callback: function () { return wrapTags('**', '**'); } });
			t.shortcuts.push({ctrl: true, key: 'i', callback: function () { return wrapTags("''", "''"); } });

			if (window.location.hash.match(/fullscreen/))
			{
				t.toggleFullscreen();
				window.location.hash = '';
			}
		};
	});
}());







|


>
>
>
>
|
>
|



















|

|

|















|


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
				t.parent.className = t.parent.className.replace(/ iframe$/, '');
				t.iframe.className = 'hidden';
			};


			var appendButton = function (name, title, action, altTitle)
			{
				var btn = document.createElement('button');
				btn.type = 'button';
				btn.title = altTitle ? altTitle : title;
				if (title.length == 1) {
					btn.dataset.icon = title;
				}
				else {
					btn.innerText = title;
				}
				btn.className = 'icn-btn ' +name;
				btn.onclick = function () { action.call(); return false; };

				toolbar.appendChild(btn);
				return btn;
			};

			var wrapTags = function (left, right)
			{
				t.wrapSelection(t.getSelection(), left, right);
				return true;
			};

			appendButton('title', "== Titre", function () { wrapTags("== ", ""); } );
			appendButton('bold', '**gras**', function () { wrapTags('**', '**'); } );
			appendButton('italic', "''italique''", function () { wrapTags("''", "''"); } );
			appendButton('link', "[[lien|http://]]", function () { 
				if (url = window.prompt('Adresse URL ?')) 
					wrapTags("[[", "|" + url + ']]'); 
			} );
			appendButton('file', "📎", openFileInsert, 'Insérer fichier / image');

			appendButton('ext preview', '⎙', openPreview, 'Prévisualiser');

			appendButton('ext help', '❓', openSyntaxHelp, 'Aide sur la syntaxe');
			appendButton('ext fullscreen', 'Plein écran', toggleFullscreen, 'Plein écran');
			appendButton('ext close', 'Fermer', closeIFrame);
			
			t.parent.insertBefore(toolbar, t.parent.firstChild);

			t.shortcuts.push({key: 'F11', callback: toggleFullscreen});
			t.shortcuts.push({ctrl: true, key: 'b', callback: function () { return wrapTags('**', '**'); } });
			t.shortcuts.push({ctrl: true, key: 'g', callback: function () { return wrapTags('**', '**'); } });
			t.shortcuts.push({ctrl: true, key: 'i', callback: function () { return wrapTags("''", "''"); } });

			if (window.location.hash.match(/fullscreen/))
			{
				t.toggleFullscreen();
				window.location.hash = '';
			}
		});
	});
}());

Modified src/www/admin/static/scripts/wiki_fichiers.js from [fdc7825154] to [0dc8ddc2bb].

73
74
75
76
77
78
79
80
81
82
83

84
85
86
87
88
89
90
                        thumb: this.firstChild.src
                    });
                    return false;
                };
            }
        }

        var a = document.createElement('a');
        a.className = 'icn';
        a.title = 'Supprimer';
        a.innerHTML = '✘';

        a.onclick = function() { if (confirm('Supprimer ce fichier ?')) this.parentNode.submit(); };

        var items = document.body.getElementsByTagName('form');

        for (var i = 0; i < items.length, form = items[i]; i++)
        {
            if (form.className != 'actions') continue;







|
|
|
|
>







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
                        thumb: this.firstChild.src
                    });
                    return false;
                };
            }
        }

        var a = document.createElement('button');
        a.className = 'icn-btn';
        a.innerText = 'Supprimer';
        a.dataset.icon = '✘';
        a.type = 'button';
        a.onclick = function() { if (confirm('Supprimer ce fichier ?')) this.parentNode.submit(); };

        var items = document.body.getElementsByTagName('form');

        for (var i = 0; i < items.length, form = items[i]; i++)
        {
            if (form.className != 'actions') continue;

Added src/www/admin/static/styles/00-reset.css version [763506382d].























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@charset "UTF-8";

@font-face {
    font-family: 'gicon';
    src: url('../font/garradin.eot?2021');
    src: url('../font/garradin.eot?2021#iefix') format('embedded-opentype'),
        url('../font/garradin.woff?2021') format('woff'),
        url('../font/garradin.woff2?2021') format('woff2'),
        url('../font/garradin.ttf?2021') format('truetype'),
        url('../font/garradin.svg?2021#garradin') format('svg');
    font-weight: normal;
    font-style: normal;
}

body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, 
pre, form, fieldset, input, textarea, p, blockquote, th, td,
figure, article, aside, section, header, footer { 
    padding: 0;
    margin: 0;
}
fieldset, img { 
    border: 0;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}
ol, ul {
    list-style: none;
}
caption, th {
    text-align: left;
}
article, aside, figure, section, header, footer, main { display: block; }

span { display: inline-block; }

h1  { font-size: 2em; }
h2  { font-size: 1.5em; }
h3  { font-size: 1.2em; }
h4  { font-size: 1em; }
h5  { font-size: 0.9em; }
h6  { font-size: 0.8em; }

Added src/www/admin/static/styles/01-layout.css version [91d3f8d7aa].































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/*
    marron : #9c4f15 rgb(156, 79, 21)
    orange : #d98628 rgb(217, 134, 40)
*/

:root {
    --gBgColor: 255, 255, 255;
    --gMainColor: 156, 79, 21;
    --gSecondColor: 217, 134, 40;
    --gBgImage: url("../gdin_bg.png");
}

html {
    width: 100%;
    height: 100%;
}

body {
    font-size: 100%;
    color: #000;
    font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif;
    padding-bottom: 1em;
    background: rgb(var(--gBgColor)) var(--gBgImage) no-repeat 0px 0px fixed;
}

main {
    margin: 0px 1em 1em 180px;
    position: relative;
}

main img {
    max-width: 100%;
}



.header h1 {
    color: rgb(var(--gMainColor));
    margin-left: 180px;
    margin-bottom: 0.4em;
}

.header .menu {
    position: fixed;
    overflow: auto;
    z-index: 10000;
    width: 170px;
    top: 0;
    bottom: 0;
    padding-top: 100px;
    background: rgb(var(--gMainColor)) var(--gBgImage) no-repeat 0px 0px;
}

.header .menu::-webkit-scrollbar {
    width: 8px;
    background: rgba(255, 255, 255, 0.25);
    box-shadow: inset 0px 0px 10px #666;
}

.header .menu::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.5);
    border-radius: 10px;
}

.header .menu i {
    font-style: normal;
}

.header .menu a {
    color: #fff;
    color: rgb(var(--gBgColor));
    font-weight: bold;
    padding: 0.4em 0.4em 0.4em 1em;
    display: block;
    text-decoration: none;
    transition: background .3s;
}

.header .menu a:hover {
    text-decoration: underline;
    background: rgba(var(--gSecondColor), 0.5);
}

.header .menu li li a {
    font-size: 0.8em;
    padding-left: 2em;
}

.header .menu li.current > a {
    background: #fff;
    background: rgb(var(--gBgColor));
    color: rgb(var(--gMainColor));
}

.header .menu a b {
    float: right;
    text-decoration: none;
    margin-top: -.2em;
    font-size: 20pt;
    color: rgba(255, 255, 255, .5);
}

.header .menu li.current > a b {
    color: rgba(var(--gSecondColor), 0.5);
}


ul.gallery {
    text-align: center;
}

ul.gallery li {
    display: inline-block;
    margin: .3em;
    vertical-align: middle;
    width: 150px;
}

ul.gallery li img {
    max-width: 150px;
    max-height: 150px;
}

ul.gallery .actions {
    text-align: center;
    z-index: 100;
}

Added src/www/admin/static/styles/02-common.css version [14d9c47672].





























































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
/** Alert, confirm and error messages */
h3.warning {
    margin: 1em;
    color: red;
}

span.error, b.error {
    color: #900;
}

span.confirm, b.confirm {
    color: #090;
}

span.alert, b.alert {
    color: #990;
}

.alert p, .error p, .confirm p {
    margin-bottom: .8em;
}

.alert.block, .error.block, .confirm.block, .help.block {
    border: 1px solid #ccc;
    padding: .5em;
    margin-bottom: 1em;
    border-radius: .3em;
    padding-left: 3em;
    position: relative;
}

.alert.block {
    border-color: #cc0;
    background-color: #ffc;
}

.error.block {
    border-color: #c00;
    background-color: #fcc;
}

.confirm.block {
    border-color: #0c0;
    background-color: #cfc;
}

.help.block {
    border-color: #999;
    background-color: #eee;
}

.confirm.block::before, .alert.block::before, .error.block::before, .help.block::before {
    font-family: "gicon";
    left: .5em;
    top: .2em;
    position: absolute;
    font-size: 1.5em;
    text-shadow: 2px 2px 5px #666;
}

.confirm.block::before {
    content: "☑";
    color: green;
}

.alert.block::before {
    content: "⚠";
    color: yellow;
}

.error.block::before {
    content: "⚠";
    color: red;
}

.help.block::before {
    content: "❓";
    color: #666;
}

.help {
    color: #666;
}

p.help {
    margin: 1em;
}

.help ul li {
    list-style-type: disc;
    margin: .5em;
    margin-left: 2em;
}

dd em.help {
    margin-left: 1em;
}

dd.help li {
    list-style-type: disc;
}

.error ul, .alert ul, .confirm ul {
    margin-left: 1.5em;
    list-style: disc;
}

h2.ruler {
    margin: .5em;
    text-align: center;
    color: #333;
    overflow: hidden;
}

h2.ruler:before, h2.ruler:after {
    background-color: #000;
    content: "";
    display: inline-block;
    height: 1px;
    position: relative;
    vertical-align: middle;
    width: 50%;
}

h2.ruler:before {
    right: 0.5em;
    margin-left: -50%;
}

h2.ruler:after {
    left: 0.5em;
    margin-right: -50%;
}

.num a, a.num {
    text-decoration: none;
    border-radius: .5rem;
    display: inline-block;
    text-align: center;
    padding: 0 .3rem;
    background: rgba(var(--gMainColor), 0.7);
    color: #fff;
    white-space: pre;
}

.droits b {
    border: 2px solid #999;
    border-radius: 1em;
    color: #000;
    background: #ccc;
    width: 16px;
    display: inline-block;
    text-align: center;
    font-size: 0.8em;
    cursor: help;
    vertical-align: middle;
    position: relative;
    z-index: 10;
    font-family: "gicon", "Trebuchet MS", Arial, Helvetica, sans-serif;
}

.droits b.aucun {
    border-color: #ccc;
    background: #eee;
    color: #999;
}

.droits b.acces {
    border-color: #cc9;
    color: #660;
    background: #ffe;
}

.droits b.ecriture {
    border-color: #9c9;
    color: #060;
    background: #efe;
}

.droits b.aucun:before {
    content: "X";
    position: absolute;
    left: 0;
    right: 0;
    top: -3px;
    color: #ccc;
    z-index: -1;
    font-size: 1.5em;
    overflow: hidden;
}

.droits b.admin {
    color: #900;
    border-color: #c99;
    background: #fee;
}

.infos {
    margin-bottom: 1em;
}

.infos h3 {
    margin-bottom: 0.5em;
}

.infos p {
    margin-bottom: 0.8em;
}

.infos dl {
    margin-bottom: 0.8em;
}

.infos dl dd {
    margin: 0.2em 1em;
}

.shortFormRight {
    width: 30em;
    float: right;
    text-align: center;
    margin-left: 1em;
}

.shortFormLeft p {
    display: inline-block;
}

.shortFormLeft .special {
    display: none;
}

.shortFormRight p.submit {
    margin-top: -2em;
    float: right;
}

.memberList {
    clear: both;
}

.pagination {
    clear: both;
    list-style-type: none;
    padding: 0.4em 0;
    text-align: center;
}

.pagination li {
    display: inline-block;
    margin: 0 0.3em;
}

.pagination li.current {
    font-size: 1.3em;
}

.pagination li a {
    color: #000;
}

.templatesList ul {
    margin: 1em 2em;
}

dl.list dt, dl.list dd {
    margin: .3em 0;
}

dl.list dt {
    font-size: 1.2em;
    font-weight: bold;
    margin-top: .8em;
}

dl.list dd.desc {
    color: #666;
}

dl.describe {
    margin-bottom: 1rem;
    display: grid;
    grid-template: auto / 15rem 1fr;
}

dl.describe > dt {
    grid-column: 1;
    margin: .2rem .5rem;
    text-align: right;
    color: #666;
    align-self: center;
}

dl.describe > dd {
    grid-column: 2;
    margin: .2rem .5rem;
    align-self: center;
}

dl.describe ul {
    margin-left: 1.5em;
    list-style-type: disc;
}

dl.cotisation {
    background: rgb(255, 174, 80);
    background: rgba(217, 134, 40, 0.2);
    background: rgba(var(--gSecondColor), 0.2);
    padding: .5em;
    border-radius: .5em;
    margin: 1em;
}

dl.cotisation dt {
    font-weight: bold;
}

dl.cotisation dd {
    margin: .2em 0 .4em 1em;
}

aside.describe {
    width: 20em;
    float: right;
    margin: .5em;
    background: rgba(var(--gSecondColor), 0.2);
    border-radius: .5em;
    border: 2px solid rgba(var(--gSecondColor), 0.5);
    padding: .5em;
    z-index: 200;
    color: #666;
}

aside.describe dl.describe {
    display: block;
}

aside.describe dl.describe dt {
    text-align: left;
    font-weight: bold;
    color: #000;
}

pre.sql_schema {
    float: right;
    color: #666;
    font-size: .9em;
    width: 30%;
    overflow: auto;
}

.hidden {
    display: none;
}

img.qrcode {
    float: right;
    padding: .5em;
    border: .5em solid #000;
    background: #fff;
}

details summary {
    list-style: none;
    padding: 0.2em 0.5em;
    transition: background-color .2s;
    position: relative;
}

details summary:hover {
    cursor: pointer;
    background-color: rgba(var(--gMainColor), 0.1);
}

details summary::-webkit-details-marker {
    display: none;
}

details summary::after {
    content: "↓";
    position: absolute;
    left: 0;
    top: 0;
    /* From .icn-btn */
    display: inline-block;
    color: rgb(var(--gMainColor));
    border: 1px solid rgba(var(--gSecondColor), 0.5);
    background: #fff;
    font-size: 1.5em;
    border-radius: .2em;
    padding: .2em .4em;
    margin: .2em .5em;
    transition: color .3s, background-color .3s;
    font-family: "gicon", sans-serif;
    text-shadow: 1px 1px 1px #999;
}

details[open] summary::after {
    content: "↑";
}

details summary:hover::after {
    background-color: rgb(var(--gMainColor));
    color: #fff;
}

details summary.block {
    padding-right: 4em !important;
}

details summary.block::after {
    right: 0;
    left: inherit;
}

Added src/www/admin/static/styles/03-forms.css version [c5b73006cf].

















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
/* Forms */
fieldset {
    border: 1px solid #ccc;
    padding: 0.8em 1em 0 1em;
    margin-bottom: 1em;
    padding: 0.5em;
}

fieldset legend {
    padding: 0 0.5em;
    font-weight: bold;
    color: #000;
}

table tr.clickable:hover {
    cursor: pointer;
    color: #633;
    background: #ffc;
}

table tr.focused {
    color: #633;
    background: #ffc !important;
    box-shadow: 0 0 5px .2rem #990;
}

dl dt label {
    font-weight: bold;
}

fieldset dl dt b {
    color: #900;
    font-size: 0.7em;
    font-weight: normal;
    vertical-align: super;
}

fieldset dl dt i {
    color: #999;
    font-size: 0.7em;
    font-weight: normal;
    vertical-align: super;
}

fieldset dl dd.tip {
    color: #666;
}

fieldset dl dd {
    padding: 0.2em 0.5em 0.2em 1em;
}

fieldset dl dd ol, fieldset dl dd ul {
    margin-left: 1.5em;
}

fieldset dl dl {
    margin: .5em 0 .5em 1.2em;
}

label:hover {
    cursor: pointer;
    border-bottom: 1px dotted #900;
}

input[type=checkbox] + label:hover {
    border: none;
}

/* We can't use :not([type=checkbox]):not([type=radio]) here as it is too specific
and then it's a mess to override the selector after... */
input[type=text], input[type=number], input[type=color],
input[type=date], input[type=datetime-local], input[type=datetime], input[type=time], input[type=week],
input[type=email], input[type=file], input[type=url], input[type=month],
input[type=password], input[type=range], input[type=search], input[type=tel],
textarea, select, .input-list {
    padding: .4rem .6rem;
    font-family: inherit;
    min-width: 20em;
    max-width: 100%;
    border: 1px solid rgb(var(--gMainColor));
    font-size: inherit;
    background: #fff;
    color: #000;
    border-radius: .25rem;
    transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}

input:not(:placeholder-shown):focus:invalid {
    border-color: #f33;
}

input.time {
    text-align: center;
    padding: .2em 0;
}

/* Fake checkbox and radio buttons */
input[type=checkbox], input[type=radio] {
    position: absolute;
    opacity: 0;
}

input[type=checkbox] + label::before, input[type=radio] + label::before {
    display: inline-block;
    width: 1em;
    height: 1em;
    text-align: center;
    transition: color .2s, box-shadow .2s ease-in-out;
    text-shadow: 1px 1px 3px #ccc;
    cursor: pointer;
    font-family: gicon;
    font-size: 1.2rem;
    font-weight: normal;
    color: rgb(var(--gMainColor));
    margin-right: .5em;
    border-radius: .25rem;
    padding: .1rem .2rem;
}

input[type=checkbox] + label::before {
    content: "☐";
}

input[type=checkbox]:checked + label::before {
    content: "☑";
}

input[type=radio] + label::before {
    content: "◯";
}

input[type=radio]:checked + label::before {
    content: "⬤";
}

input:hover + label::before {
    color: rgb(var(--gSecondColor));
}

input:checked + label::before {
    text-shadow: 1px 1px 5px #ff9;
}

input:focus, button:focus, select:focus, textarea:focus, input[type=radio]:focus + label::before, input[type=checkbox]:focus + label::before {
    box-shadow: 0 0 5px .2rem rgb(var(--gSecondColor));
    outline: 0;
}

/* buttons */

input[type=submit], input[type=button], button, input[type=file] {
    border-radius: 1em;
    border: none;
    box-shadow: 0px 0px 5px 0 #ccc;
    cursor: pointer;
    border: 2px solid rgba(var(--gMainColor), 0.5);
    background-color: rgba(var(--gSecondColor), 0.1);
    display: inline-block;
    font-size: inherit;
    border-radius: .2em;
    padding: .2em .4em;
    margin: .2em .5em;
    text-decoration: none;
    transition: color .3s, background-color .3s;
    color: #000;
}

a.icn-btn {
    cursor: pointer;
    color: #003;
    border: 1px solid rgba(var(--gMainColor), 0.5);
    background-color: rgba(var(--gSecondColor), 0.1);
    user-select: none;
    display: inline-block;
    font-size: inherit;
    border-radius: .2em;
    padding: .2em .4em;
    margin: .2em .5em;
    white-space: pre;
    transition: color .3s, background-color .3s;
    text-decoration: underline;
}

.submit .main {
    color: #000;
    font-size: 1.2em;
    border-radius: 1em;
    padding: .5em 1em;
}

.submit .main[data-icon]:before {
    display: none;
}

p.submit .main[data-icon]:after {
    padding: 0 0 0 .5rem;
    color: rgba(var(--gSecondColor));
    font-size: 1.5rem;
    line-height: .2em;
}

.submit .minor {
    font-size: .9em;
}

input[type=submit]:hover, input[type=button]:hover, button:hover, a.icn-btn:hover, input[type=file]:hover,
.radio-btn:hover div, a.num:hover, .num a:hover {
    background-color: rgba(var(--gSecondColor), 0.2);
    color: darkred !important;
    border-color: rgb(var(--gSecondColor));
}

input[type=submit]:active, input[type=button]:active, button:active, input[type=file]:active {
    box-shadow: 0 0 10px .1rem rgb(var(--gSecondColor));
}

input[type=color] {
    cursor: pointer;
}

input.resetButton {
    margin-left: 1em;
}

input[readonly], input.disabled, input[disabled], textarea[disabled], select[disabled] {
    cursor: not-allowed;
    color: #666;
    background-color: #eee;
    border-color: #999;
}

input[disabled]:hover, input[readonly]:hover {
    background-color: unset;
    color: unset;
    border-color: unset;
}

select, input[size], input[type=color], button, input[type=button], input[type=submit], input[type=number] {
    min-width: 0;
}

/* Radio button lists (eg. new transaction) */
form .radio-btn {
    display: table;
    width: 100%;
    padding: 0;
    border-spacing: .5rem .2rem;
}

form .radio-btn label {
    display: table-row;
}

form .radio-btn input + label::before {
    display: table-cell;
}

form .radio-btn input:focus + label::before {
    box-shadow: none;
}

form .radio-btn input:focus + label div {
    box-shadow: 0 0 5px .2rem rgb(var(--gSecondColor));
}

form .radio-btn div {
    display: table-cell;
    border: 1px solid rgba(var(--gSecondColor), 0.5);
    background-color: rgba(var(--gSecondColor), 0.1);
    font-size: inherit;
    border-radius: .2em;
    padding: .2em .4em;
    transition: color .3s, background-color .3s;
    color: #333;
}

form .radio-btn h3 {
    text-decoration: underline;
}

form .radio-btn input {
    margin: 1em;
}

form .radio-btn .help {
    margin: .8em 0 0 0;
    font-size: .8em;
}

form .radio-btn input:checked + label div {
    background-color: rgba(var(--gSecondColor), 0.3);
}

/* Custom list input */
form .input-list {
    padding: 0;
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: start;
    min-width: 0;
}

form .input-list .label {
    padding: .4rem .6rem;
}

form .input-list button {
    padding: .4rem .6rem;
    align-self: stretch;
    flex-shrink: 0;
    margin: 0;
    border: none;
}

form .input-list .label button {
    padding: .2rem;
    border: none;
    background: none;
}

input.money {
    text-align: right;
}

input.money + b {
    padding: .2rem .6rem;
    line-height: 1.5rem;
    color: #999;
}

p.submit {
    margin: 1em;
}


form .checkUncheck {
    float: left;
}

form span.password_check {
    margin-left: 1em;
    padding: .1em .3em;
    border-radius: .5em;
}

form span.password_check.fail { background-color: #f99; }
form span.password_check.weak { background-color: #ff9; }
form span.password_check.medium { background-color: #ccf; }
form span.password_check.ok { background-color: #cfc; }

dd.help input[type=text] {
    cursor: pointer;
    font-family: monospace;
}

form p.actions {
    float: right;
}

/** Datepicker widget */
.datepicker-parent {
    position: relative;
}

dialog {
    display: none;
}

dialog[open] {
    display: block;
}

dialog.datepicker {
    position: absolute;
    left: 0;
    margin: 0;
    padding: .3rem;
    border: none;
    box-shadow: 0 0 5px #000;
    border-radius: .5rem;
    z-index: 1000;
}

.datepicker nav {
    display: flex;
    justify-content: space-between;
    text-align: center;
}

.datepicker h3 {
    font-size: inherit;
    margin: 0 .5rem;
}

.datepicker table {
    border-collapse: collapse;
    width: 100%;
}

.datepicker thead td {
    font-size: 80%;
    color: #999;
}

.datepicker tbody tr:nth-child(even) {
    background-color: #eee;
}

.datepicker tbody td:nth-child(6) {
    color: #666;
}
.datepicker tbody td:nth-child(7) {
    color: #999;
}

.datepicker tbody td {
    padding: .2rem .4rem;
    text-align: center;
    width: 14.3%;
}

.datepicker tbody td:not(:empty):hover {
    cursor: pointer;
    background: #fcc;
    text-decoration: underline;
}

.datepicker tbody td.focus {
    background: #339;
    color: #fff;
}

.datepicker input {
    font-family: gicon;
}

[data-icon]:before, summary::after, .main[data-icon]:after {
    display: inline-block;
    font-family: "gicon", sans-serif;
    text-shadow: 1px 1px 1px #ccc;
    padding-right: .5em;
    font-size: 1.2em;
    line-height: .8em;
    vertical-align: middle;
    content: attr(data-icon);
}

[data-icon]:empty:before {
    padding: 0;
}

.icn, .icnl {
    font-family: "gicon", sans-serif;
    font-style: normal;
    font-weight: normal;
    speak: none;
    font-variant: normal;
    text-transform: none;
    position: relative;
}

.actions .icn, .icn.action {
    text-decoration: none;
    border-radius: 1em;
    display: inline-block;
    text-align: center;
    font-size: 1.2em;
    line-height: .8em;
    vertical-align: middle;
    padding: .2em;
    font-family: "gicon", sans-serif;
    color: #9c4f15;
    color: rgb(var(--gMainColor));
    text-shadow: 1px 1px 1px #999;
    border: none;
    cursor: pointer;
    position: relative;
    z-index: 200;
}

fieldset.memberMessage {
    max-width: 30em;
}

fieldset.memberMessage #f_sujet, fieldset.memberMessage #f_message, fieldset.memberMessage select {
    width: calc(100% - 2em);
}


#queryBuilder .column select, #queryBuilderForm .actions select {
    max-width: 15em;
}

#queryBuilder table td {
    vertical-align: top;
    padding: .1em .2em;
}

#queryBuilder table td.buttons {
    white-space: nowrap;
}

#queryBuilder input[type=button], #queryBuilder .values input {
    margin: .1em;
}

#queryBuilderForm .actions label {
    margin: 0 .5em;
}

#queryBuilder table .values label {
    margin: 0 .3em;
}

#queryBuilderForm input[type=number] {
    width: 4em;
}

@media screen and (max-width: 1279px) {
    #queryBuilder table tr {
        display: flex;
        flex-wrap: wrap;
        padding: .5em 0;
        margin-left: 6rem;
        border-top: .2rem solid #ccc;
        clear: both;
    }
    #queryBuilder table td {
        display: block;
    }
    #queryBuilder table td.buttons {
        margin-left: -6rem;
    }
}

Added src/www/admin/static/styles/04-dialogs.css version [9ca4a3ffe3].















































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
body#popup {
    background-position: -170px 0px;
}

body#transparent {
    background: transparent;
}

body#popup main {
    margin: 1em 1em 1em 2.5em;
}

body#transparent main {
    margin: 0;
    padding: .2em;
}

/** Dialogs pop-ins */
#dialog {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.7);
    position: fixed;
    opacity: 1;
    transition: opacity .5s;
    z-index: 100000;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: none;
}

#dialog > iframe {
    width: 90%;
    height: 90%;
    border: none;
    box-shadow: 0px 0px 5px #000;
}

#dialog button {
    background: rgb(var(--gMainColor));
    color: #fff;
    font-size: 1.3em;
    display: block;
    width: 90%;
}

.loader {
    width: 100%;
    min-height: 32px;
    display: block;
    position: relative;
}

.loader.install {
    margin-top: -40px;
}

.loader b {
    text-shadow: 2px 2px 5px #999;
    background: rgb(255, 255, 255);
    background: rgba(255, 255, 255, 0.5);
    border-radius: .5em;
    font-size: 16px;
    line-height: 16px;
    height: 16px;
    z-index: 9999;
    position: absolute;
    display: block;
    left: 10px;
    top: 10px;
    padding: .2em;
}

.loader img {
    position: absolute;
    opacity: 0;
    transition: all 0.5s ease;
    z-index: 2;
}

Added src/www/admin/static/styles/05-navigation.css version [e121b7221f].















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/** Context-specific tabs */
nav.tabs ul {
    list-style-type: none;
    margin: 1em 0;
    border-bottom: 2px solid #9c4f15;
    border-bottom-color: rgb(var(--gMainColor));
    padding: 0 1em;
    z-index: 100;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
}

nav.tabs .sub {
    margin: -1em 0 1em 2em;
    padding-top: 1em;
    border-left: 2px solid rgb(var(--gMainColor));
    border-bottom-left-radius: .5em;
}

nav.tabs .sub .title {
    margin: 0 1em 0 -1em;
    font-weight: bold;
    padding: .1em .5em;
}

nav.tabs li {
    margin: 0 0.2em;
}

nav.tabs li a {
    display: inline-block;
    background: rgba(var(--gSecondColor), .5);
    border-radius: .5em .5em 0 0;
    padding: .1em .5em;
    color: #000;
    text-decoration: none;
    transition: background-color .2s, color .2s;
}

nav.tabs .current a {
    background: rgb(var(--gMainColor));
    color: rgb(var(--gBgColor));
}

nav.tabs li a:hover {
    color: rgb(var(--gBgColor));
    background-color: rgb(var(--gMainColor));
    text-decoration: underline;
    border-bottom: none;
}

nav.tabs aside {
    float: right;
}

Added src/www/admin/static/styles/06-tables.css version [6775097e90].





































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
table.list {
    margin-bottom: 1em;
    width: 100%;
}

table.list caption {
    text-align: center;
    font-size: 1.2em;
}

table.list tbody td.desc {
    font-size: .9em;
    color: #666;
}

table.list.auto {
    width: auto;
}

table.list table {
    margin: 0;
}

table.list th {
    text-align: left;
    font-weight: bold;
}

table.list thead {
    background: rgba(var(--gSecondColor), 0.5);
}

table.list tfoot tr {
    background: rgba(var(--gSecondColor), 0.2);
}

table.list th, table.list td {
    padding: 0.2em 0.5em;
}

table.list tr {
    border: 1px solid rgba(var(--gSecondColor), 0.5);
    transition: background .2s
}

table.list tr:nth-child(even), table.multi tbody:nth-child(even) {
    background: rgba(var(--gSecondColor), 0.2);
}

table.list tr.disabled {
    color: #666;
}

table.multi tr {
    background: inherit !important;
}

table.list tr.checked {
    color: #633;
    background: #ffc;
}

table.list .error {
    color: red;
    font-weight: bold;
}

table.list .alert {
    color: darkred;
    font-weight: bold;
}

table.list .confirm {
    color: darkgreen;
}

table.list .num {
    text-align: center;
}

table.list .check {
    width: 1%;
}

table.search th {
    background: rgb(217, 134, 40);
    background: rgba(217, 134, 40, 0.5);
    background: rgba(var(--gSecondColor), 0.5);
}

table.list .disabled {
    background: #eee;
    color: #999;
}

.userOrder .cur {
    background: rgba(var(--gSecondColor), 1.0);
    color: rgb(var(--gBgColor));
}

table.list .userOrder td, table.list .userOrder th {
    position: relative;
    padding-left: 2.3em;
}

table.list .userOrder .check {
    padding-left: .5em;
}

.userOrder .icn {
    position: absolute;
    left: .3em;
    top: 0;
    color: rgb(var(--gMainColor));
    text-decoration: none;
    font-size: 1.5em;
    line-height: .5em;
    width: 1em;
    text-align: center;
    vertical-align: middle;
    font-weight: normal;
    text-shadow: 0px 0px 1px rgb(var(--gBgColor));
    margin: 0;
}

.userOrder .icn.dn {
    margin-top: .6em;
}

thead .icn:hover {
    color: darkred;
    text-shadow: none;
}

thead .cur.desc .icn.dn, thead .cur.asc .icn.up {
    color: #fff;
    text-shadow: none;
}

table.list .actions {
    text-align: right;
}

table.list .separator {
    border-left: 2px dashed #999;
}

Added src/www/admin/static/styles/10-accounting.css version [2779caf5e1].





















































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/** Accounting specifics */
.money {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

b.money {
    font-weight: inherit;
    white-space: pre;
}

.transaction-lines td.money {
    text-align: left;
}

.transaction-lines td.money input + b {
    display: none;
}

.transaction-lines select {
    max-width: 10em;
}

.transaction-lines input[type=text] {
    min-width: 0 !important;
}

nav.acc-year {
    background: white;
    text-align: center;
    border-radius: .5rem;
    border: .2rem solid rgba(var(--gMainColor), 0.5);
    display: flex;
    align-items: center;
    margin-bottom: .5rem;
}

nav.acc-year > * {
    padding: .2rem .5rem;
}

nav.acc-year h4 {
    font-weight: normal;
}

table.statement, table.statement table {
    width: 100%;
}

table.statement td, table.statement th {
    padding: .5rem;
    vertical-align: top;
}

table.statement table.list td, table.statement table.list th {
    padding: .2rem .5rem;
}

table.statement table {
    border: 1px solid rgba(var(--gSecondColor), 0.5);
}

td.money, th.money {
    text-align: right;
}

.statement table tfoot tr {
    background: rgba(var(--gSecondColor), 0.1);
    color: rgb(var(--gMainColor));
}

.year-header {
    text-align: center;
    margin-bottom: .8em;
    padding-bottom: .5em;
    border-bottom: 1pt solid #999;
}

.year-header .print-btn button {
    font-size: 1.3rem;
}

.year-infos {
    text-align: center;
}

.year-infos .graphs {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

.year-infos .graphs figure {
    margin: 1rem;
}

table.accounts { width: 100%; }
table.accounts .actions { text-align: right; }
table.accounts tbody tr td:first-child { font-family: monospace; }
table.accounts th { font-weight: normal; }
table.accounts .account-level-1 th { font-size: 1.6em; }
table.accounts .account-level-2 th { padding-left: 1em; font-size: 1.3em; }
table.accounts .account-level-3 th { padding-left: 2em; }
table.accounts .account-level-4 th { padding-left: 3em; }
table.accounts .account-level-5 th { padding-left: 4em; }
table.accounts .account-level-6 th { padding-left: 5em; }

Modified src/www/admin/static/styles/config.css from [ec00c1eea1] to [bc72f7c8a4].

32
33
34
35
36
37
38

































































	border: 1px solid #999;
	padding: .2em .5em;
}

.error .event table {
	margin: .5em;
}








































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
	border: 1px solid #999;
	padding: .2em .5em;
}

.error .event table {
	margin: .5em;
}


#orderFields fieldset {
    position: relative;
    min-height: 2em;
    transition: all 1s;
    overflow: hidden;
    max-height: 5000px;
}

#orderFields fieldset legend {
    font-size: 1.2em;
    line-height: .8em;
    color: #666;
}

#orderFields fieldset .actions {
    display: block;
    position: absolute;
    top: 1em;
    right: 1em;
}

#orderFields fieldset .actions .icn {
    position: absolute;
}

#orderFields fieldset dl {
    overflow: hidden;
    transition: all .5s;
    opacity: 1;
    display: block;
    max-height: 5000px;
}

#orderFields fieldset dl.hidden {
    opacity: 0;
    max-height: 0;
}

#orderFields fieldset.removed {
    max-height: 0;
    opacity: 0;
    border-color: red;
    min-height: 0;
    height: 0;
}

#orderFields fieldset .actions .remove { right: 0em; }
#orderFields fieldset .actions .edit { right: 1.5em; }
#orderFields fieldset .actions .down { right: 3em; }
#orderFields fieldset .actions .up { right: 4.5em; }

#orderFields fieldset:nth-child(1) .actions .up, #orderFields fieldset:nth-last-child(1) .actions .down {
    display: none;
}

#orderFields fieldset .actions .icn {
    cursor: pointer;
}

#orderFields fieldset .interactive:hover {
    cursor: pointer;
    text-decoration: underline;
}

Modified src/www/admin/upgrade.php from [accfe43dfe] to [0bcf1f55dd].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php

namespace Garradin;

use Garradin\Membres\Session;

const UPGRADE_PROCESS = true;

require_once __DIR__ . '/../../include/test_required.php';
require_once __DIR__ . '/../../include/init.php';

$config = Config::getInstance();

$v = $config->getVersion();

if (version_compare($v, garradin_version(), '>='))
{
    throw new UserException("Pas de mise à jour à faire.");
}

// versions pré-0.7.0: démerdez-vous !
if (!$v || version_compare($v, '0.7.0', '<'))
{
    throw new UserException("Votre version de Garradin est trop ancienne pour être mise à jour. Mettez à jour vers Garradin 0.8.5 avant de faire la mise à jour vers cette version.");
}

Install::checkAndCreateDirectories();

if (Static_Cache::exists('upgrade'))
{
    $path = Static_Cache::getPath('upgrade');
    throw new UserException('Une mise à jour est déjà en cours.'
        . PHP_EOL . 'Si celle-ci a échouée et que vous voulez ré-essayer, supprimez le fichier suivant:'
        . PHP_EOL . $path);
}

// Voir si l'utilisateur est loggé, on le fait ici pour le cas où
// il y aurait déjà eu des entêtes envoyés au navigateur plus bas
$session = new Session;
$user_is_logged = $session->isLogged(true);

Static_Cache::store('upgrade', 'Mise à jour en cours.');

$db = DB::getInstance();
$redirect = true;

// Créer une sauvegarde automatique
$backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version());

echo '<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="static/admin.css" media="all" />




<
<







<
|
<
<
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







1
2
3
4


5
6
7
8
9
10
11

12


13
14





























15
16
17
18
19
20
21
<?php

namespace Garradin;



const UPGRADE_PROCESS = true;

require_once __DIR__ . '/../../include/test_required.php';
require_once __DIR__ . '/../../include/init.php';

$config = Config::getInstance();


if (!Upgrade::preCheck()) {


	throw new UserException('Aucune mise à jour à effectuer, tout est à jour :-)');
}






























echo '<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="static/admin.css" media="all" />
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
<div id="loader" class="loader" style="margin: 2em 0; height: 50px;"></div>
<script>
animatedLoader(document.getElementById("loader"), 5);
</script>';

flush();

try {
    if (version_compare($v, '0.7.0', '<'))
    {
        $db->toggleForeignKeys(false);
        $db->begin();

        // Mise à jour base de données
        $db->exec(file_get_contents(ROOT . '/include/data/0.7.0.sql'));

        // Changement de syntaxe du Wiki vers SkrivML
        $wiki = new Wiki;
        $res = $db->get('SELECT id_page, contenu, revision, chiffrement FROM wiki_revisions GROUP BY id_page ORDER BY revision DESC;');

        foreach ($res as $row)
        {
            // Ne pas convertir le contenu chiffré, de toute évidence
            if ($row->chiffrement)
                continue;

            $content = $row->contenu;
            $content = Utils::HTMLToSkriv($content);
            $content = Utils::SpipToSkriv($content);

            if ($content != $row->contenu)
            {
                $wiki->editRevision($row->id_page, $row->revision, [
                    'id_auteur'     =>  null,
                    'contenu'       =>  $content,
                    'modification'  =>  'Mise à jour 0.7.0 (transformation SPIP vers SkrivML)',
                ]);
            }
        }

        $db->commit();
    }

    if (version_compare($v, '0.7.2', '<'))
    {
        $db->toggleForeignKeys(false);
        $db->begin();

        // Mise à jour base de données
        $db->exec(file_get_contents(ROOT . '/include/data/0.7.2.sql'));

        $db->commit();
    }

    if (version_compare($v, '0.8.0-beta4', '<'))
    {
        // Inscription de l'appid
        $db->exec('PRAGMA application_id = ' . DB::APPID . ';');

        // Changement de la taille de pagesize
        // Cecit devrait améliorer les performances de la DB
        $db->exec('PRAGMA page_size = 4096;');

        // Application du changement de taille de page
        $db->exec('VACUUM;');

        // Désactivation des foreign keys AVANT le début de la transaction
        $db->toggleForeignKeys(false);

        $db->begin();

        $db->import(ROOT . '/include/data/0.8.0.sql');

        $db->commit();

        $config = Config::getInstance();

        // Ajout champ numéro de membre
        $champs = (array) $config->get('champs_membres')->getAll();
        $presets = Membres\Champs::importPresets();

        // Ajout du numéro au début
        $champs = array_merge(['numero' => $presets['numero']], $champs);
        (new Membres\Champs($champs))->save();

        // Si l'ID était l'identificant, utilisons le numéro de membre à la place
        if ($config->get('champ_identifiant') == 'id')
        {
            $config->set('champ_identifiant', 'numero');
            $config->save();
        }

        // Nettoyage de la base de données
        $db->exec('VACUUM;');

        // Mise à jour plan comptable: ajout comptes encaissement
        $comptes = new Compta\Comptes;
        $comptes->importPlan();
    }

    if (version_compare($v, '0.8.3', '<'))
    {
        // Désactivation des foreign keys AVANT le début de la transaction
        $db->toggleForeignKeys(false);

        $db->begin();

        $db->import(ROOT . '/include/data/0.8.3.sql');

        $db->commit();
    }

    if (version_compare($v, '0.8.4', '<'))
    {
        $db->begin();

        $db->import(ROOT . '/include/data/0.8.4.sql');

        $db->commit();
    }

    if (version_compare($v, '0.9.0-rc1', '<'))
    {
        $db->toggleForeignKeys(false);
        $db->begin();

        $db->import(ROOT . '/include/data/0.9.0.sql');

        // Correction des ID parents des comptes qui ont été mal renseignés
        // exemple : compte 512A avec "5" comme parent (c'était permis,
        // par erreur, par le formulaire d'ajout de compte dans le plan)
        // Serait probablement possible en 3-4 lignes de SQL avec
        // WITH RECURSIVE mais c'est au delà de mes compétences
        $comptes = $db->iterate('SELECT id FROM compta_comptes WHERE parent != length(id) - 1;');

        foreach ($comptes as $compte)
        {
            $parent = false;
            $id = $compte->id;

            while (!$parent && strlen($id))
            {
                // On enlève un caractère à la fin jusqu'à trouver un compte parent correspondant
                $id = substr($id, 0, -1);
                $parent = $db->firstColumn('SELECT id FROM compta_comptes WHERE id = ?;', $id);
            }

            if (!$parent)
            {
                // Situation normalement impossible !
                throw new \LogicException(sprintf('Le compte %s est invalide et n\'a pas de compte parent possible !', $compte->id));
            }

            $db->update('compta_comptes', ['parent' => $parent], 'id = :id', ['id' => $compte->id]);
        }

        $champs = $config->get('champs_membres');

        if ($champs->get('lettre_infos'))
        {
            // Ajout d'une recherche avancée en exemple
            $query = [
                'query' => [[
                    'operator' => 'AND',
                    'conditions' => [
                        [
                            'column'   => 'lettre_infos',
                            'operator' => '= 1',
                            'values'   => [],
                        ],
                    ],
                ]],
                'order' => 'numero',
                'desc' => true,
                'limit' => '10000',
            ];

            $recherche = new Recherche;
            $recherche->add('Membres inscrits à la lettre d\'information', null, $recherche::TYPE_JSON, 'membres', $query);
        }

        $db->commit();

        $config->set('desactiver_site', false);
        $config->save();
    }

    if (version_compare($v, '0.9.1', '<'))
    {
        // Mise à jour plan comptable: ajout compte licences fédérales
        $comptes = new Compta\Comptes;
        $comptes->importPlan();

        $db->toggleForeignKeys(false);
        $db->begin();

        $db->exec('INSERT INTO "compta_categories" VALUES(NULL,-1,\'Licences fédérales\',\'Licences payées pour les adhérents (par exemple fédération sportive etc.)\',\'652\');');

        $db->import(ROOT . '/include/data/0.9.1.sql');

        $db->commit();
    }

    if (version_compare($v, '0.9.5', '<'))
    {
        $db->begin();
        // Créer les tables manquantes
        $db->import(ROOT . '/include/data/schema.sql');
        $db->commit();
    }

    if (version_compare($v, '0.9.7', '<'))
    {
        $db->begin();

        // Conversion des champs date
        $champs = (array) $config->get('champs_membres')->getAll();
        $formats = ['d/m/Y', 'd/m/Y H:i:s', 'd/m/Y H:i', 'd/m/y', 'd-m-Y'];

        foreach ($champs as $key => $champ) {
            if ($champ->type == 'date') {
                $target_format = 'Y-m-d';
            }
            elseif ($champ->type == 'datetime') {
                $target_format = 'Y-m-d H:i:s';
            }
            else {
                continue;
            }

            $sql = sprintf('SELECT id, %s AS date FROM membres WHERE %01$s IS NOT NULL AND date(%01$s) IS NULL;', $db->quoteIdentifier($key));

            foreach ($db->iterate($sql) as $row) {
                foreach ($formats as $format) {
                    $date = \DateTime::createFromFormat($format, $row->date);

                    if ($date) {
                        break;
                    }
                }

                if ($date) {
                    $date = $date->format($target_format);
                }
                else {
                    $date = null;
                }

                $db->update('membres', [$key => $date], 'id = ' . (int)$row->id);
            }
        }

        $db->commit();
    }

    Utils::clearCaches();

    $config->setVersion(garradin_version());

    Static_Cache::remove('upgrade');

    // Réinstaller les plugins système si nécessaire
    Plugin::checkAndInstallSystemPlugins();

    // Mettre à jour les plugins si nécessaire
    foreach (Plugin::listInstalled() as $id=>$infos)
    {
        // Ne pas tenir compte des plugins dont le code n'est pas dispo
        if ($infos->disabled)
        {
            continue;
        }

        $plugin = new Plugin($id);

        if ($plugin->needUpgrade())
        {
            $plugin->upgrade();
        }

        unset($plugin);
    }
}
catch (\Exception $e)
{
    $s = new Sauvegarde;
    $s->restoreFromLocal($backup_name);
    $s->remove($backup_name);
    Static_Cache::remove('upgrade');
    throw $e;
}

// Forcer à rafraîchir les données de la session si elle existe
if ($user_is_logged)
{
    $session->refresh();
}

echo '<h2>Mise à jour terminée.</h2>
<p><a href="'.ADMIN_URL.'">Retour</a></p>';

if ($redirect)
{
    echo '
    <script type="text/javascript">
    window.setTimeout(function () { 
        window.location.href = "'.ADMIN_URL.'"; 
        stopAnimatedLoader();
    }, 1000);
    </script>';
}

echo '
</main>
</body>
</html>';







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


|

<
<
<
|
|
|
|
|
|
<
<
<



31
32
33
34
35
36
37














































































































































































































































































38



















39
40
41
42



43
44
45
46
47
48



49
50
51
<div id="loader" class="loader" style="margin: 2em 0; height: 50px;"></div>
<script>
animatedLoader(document.getElementById("loader"), 5);
</script>';

flush();















































































































































































































































































Upgrade::upgrade();




















echo '<h2>Mise à jour terminée.</h2>
<p><a href="'.ADMIN_URL.'">Retour</a></p>




<script type="text/javascript">
window.setTimeout(function () { 
    window.location.href = "'.ADMIN_URL.'"; 
    stopAnimatedLoader();
}, 1000);
</script>



</main>
</body>
</html>';

Modified src/www/admin/wiki/_fichiers.php from [abf2d3676b] to [aec8dcbc2d].

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
elseif (f('delete'))
{
    if ($form->check($csrf_id))
    {
        try {
            $fichier = new Fichiers(f('delete'));
            
            if (!$fichier->checkAccess($session))
            {
                throw new UserException('Vous n\'avez pas accès à ce fichier.');
            }

            $fichier->remove();
            Utils::redirect(ADMIN_URL . 'wiki/_fichiers.php?page=' . $page->id);
        }







|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
elseif (f('delete'))
{
    if ($form->check($csrf_id))
    {
        try {
            $fichier = new Fichiers(f('delete'));
            
            if (!$fichier->checkAccess($session, true))
            {
                throw new UserException('Vous n\'avez pas accès à ce fichier.');
            }

            $fichier->remove();
            Utils::redirect(ADMIN_URL . 'wiki/_fichiers.php?page=' . $page->id);
        }

Modified src/www/index.php from [f59003a96f] to [1c65f87820].

1
2
3




4
5
6
7
8
9
10
11
12
13
<?php

namespace Garradin;





require __DIR__ . '/_inc.php';

if (Config::getInstance()->get('desactiver_site'))
{
	Utils::redirect(ADMIN_URL);
}

$squelette = new Squelette;
$squelette->dispatchURI();



>
>
>
>










1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace Garradin;

if (PHP_VERSION_ID < 70200) {
	die("PHP 7.2 ou supérieur est requis.");
}

require __DIR__ . '/_inc.php';

if (Config::getInstance()->get('desactiver_site'))
{
	Utils::redirect(ADMIN_URL);
}

$squelette = new Squelette;
$squelette->dispatchURI();

Modified tests/run.php from [e0e24bf932] to [43c2b61a01].

1
2



3
4
5
6
7
8
9
10
<?php




require __DIR__ . '/../src/include/init.php';

if (!empty($_SERVER['argv'][1]))
{
	require $_SERVER['argv'][1];
	exit;
}
else


>
>
>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

define('Garradin\WWW_URI', '/');
define('Garradin\WWW_URL', 'http://localhost/');

const INIT = __DIR__ . '/../src/include/init.php';

if (!empty($_SERVER['argv'][1]))
{
	require $_SERVER['argv'][1];
	exit;
}
else

tests/run.sh became executable with contents [476c017c61].

Modified tests/unit_tests/01_basic/db.php from [88dbf0c61d] to [66b9155618].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
<?php

namespace Garradin;

use KD2\Test;

const DB_FILE = ':memory:';


require __DIR__ . '/../init.php';

$db = DB::getInstance();

// test exec
Test::assert($db->exec('CREATE TABLE test (a, b);'));

// test insert







>

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace Garradin;

use KD2\Test;

const DB_FILE = ':memory:';
const INSTALL_PROCESS = true;

require_once INIT;

$db = DB::getInstance();

// test exec
Test::assert($db->exec('CREATE TABLE test (a, b);'));

// test insert
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Test::assert($db->update('test', ['a' => 5, 'b' => 6], 'a = :a AND b = :b', ['a' => 3, 'b' => 4]));

// test update with mixed type bindings
try {
	$db->update('test', ['a' => 5, 'b' => 6], 'a = ? AND b = ?', [3, 4]);
	$failed = false;
}
catch (\InvalidArgumentException $e) {
	$failed = true;
}

Test::assert($failed === true);

// test if update worked
Test::equals((object)['a' => 5, 'b' => 6], $db->first('SELECT a, b FROM test LIMIT 1, 1;'));







|







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Test::assert($db->update('test', ['a' => 5, 'b' => 6], 'a = :a AND b = :b', ['a' => 3, 'b' => 4]));

// test update with mixed type bindings
try {
	$db->update('test', ['a' => 5, 'b' => 6], 'a = ? AND b = ?', [3, 4]);
	$failed = false;
}
catch (\LogicException $e) {
	$failed = true;
}

Test::assert($failed === true);

// test if update worked
Test::equals((object)['a' => 5, 'b' => 6], $db->first('SELECT a, b FROM test LIMIT 1, 1;'));
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
$expected = [(object)['a' => 1, 'b' => 2], (object)['a' => 9, 'b' => 10]];
Test::equals($expected, $db->get('SELECT * FROM test LIMIT 2;'));

$expected = [1 => 2, 9 => 10];
Test::equals($expected, $db->getAssoc('SELECT * FROM test LIMIT 2;'));

$expected = [1 => (object) ['a' => 1, 'b' => 2], 9 => (object) ['a' => 9, 'b' => 10]];
Test::equals($expected, $db->getAssocKey('SELECT * FROM test LIMIT 2;'));

// test transactions
Test::assert($db->begin());

Test::assert($db->insert('test', ['a' => 42, 'b' => 43]));

// test nested transaction
Test::assert(!$db->begin());
Test::assert(!$db->commit());

Test::assert($db->insert('test', ['a' => 44, 'b' => 45]));

// test rollback
Test::assert($db->rollback());

Test::equals(3, $db->firstColumn('SELECT COUNT(*) FROM test;'));








|






<
<
<
<







63
64
65
66
67
68
69
70
71
72
73
74
75
76




77
78
79
80
81
82
83
$expected = [(object)['a' => 1, 'b' => 2], (object)['a' => 9, 'b' => 10]];
Test::equals($expected, $db->get('SELECT * FROM test LIMIT 2;'));

$expected = [1 => 2, 9 => 10];
Test::equals($expected, $db->getAssoc('SELECT * FROM test LIMIT 2;'));

$expected = [1 => (object) ['a' => 1, 'b' => 2], 9 => (object) ['a' => 9, 'b' => 10]];
Test::equals(json_encode($expected), json_encode($db->getGrouped('SELECT * FROM test LIMIT 2;')));

// test transactions
Test::assert($db->begin());

Test::assert($db->insert('test', ['a' => 42, 'b' => 43]));





Test::assert($db->insert('test', ['a' => 44, 'b' => 45]));

// test rollback
Test::assert($db->rollback());

Test::equals(3, $db->firstColumn('SELECT COUNT(*) FROM test;'));

Modified tests/unit_tests/01_basic/paths.php from [802a0c36b5] to [10b0dc1a36].

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace Garradin;
use KD2\Test;

require __DIR__ . '/../init.php';

Test::assert(defined('Garradin\ROOT'));
Test::assert(is_readable(ROOT));

Test::assert(defined('Garradin\PLUGINS_ROOT'));
Test::assert(is_readable(PLUGINS_ROOT));






|







1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace Garradin;
use KD2\Test;

require_once INIT;

Test::assert(defined('Garradin\ROOT'));
Test::assert(is_readable(ROOT));

Test::assert(defined('Garradin\PLUGINS_ROOT'));
Test::assert(is_readable(PLUGINS_ROOT));

Modified tests/unit_tests/01_basic/version.php from [20ce832281] to [9444e092b3].

1
2
3
4
5
6
7
8
9
<?php

namespace Garradin;
use KD2\Test;

require __DIR__ . '/../init.php';

Test::assert(function_exists('Garradin\garradin_version'));
Test::assert(garradin_version());





|



1
2
3
4
5
6
7
8
9
<?php

namespace Garradin;
use KD2\Test;

require_once INIT;

Test::assert(function_exists('Garradin\garradin_version'));
Test::assert(garradin_version());

Added tests/unit_tests/02_accounting/money.php version [b5a7b488bb].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace Garradin;
use KD2\Test;

require_once INIT;

Test::strictlyEquals(500, Utils::moneyToInteger('5'));
Test::strictlyEquals(442, Utils::moneyToInteger('4,42'));
Test::strictlyEquals(442, Utils::moneyToInteger('4.42'));
Test::strictlyEquals(4, Utils::moneyToInteger('0,04'));
Test::strictlyEquals(30, Utils::moneyToInteger('0,3'));
Test::strictlyEquals(202034, Utils::moneyToInteger('2020,34'));

Test::strictlyEquals('5,50', Utils::money_format(550));
Test::strictlyEquals('0,05', Utils::money_format(5));
Test::strictlyEquals('0,50', Utils::money_format(50));
Test::strictlyEquals('1 000,50', Utils::money_format(100050));