Overview
Comment:Merge stable changes in dev
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: d0ca9be742dc998fd584ac6bc8ca4a1e36e686fe50a2633b09853ad619226a2b
User & Date: bohwaz on 2021-10-05 00:52:50
Other Links: branch diff | manifest | tags
Context
2021-10-05
01:53
Remove unused variable check-in: 20b010c98b user: bohwaz tags: dev
00:52
Merge stable changes in dev check-in: d0ca9be742 user: bohwaz tags: dev
00:47
Implement generic signals for entities check-in: 84133f87d4 user: bohwaz tags: trunk, stable
2021-06-07
19:23
Merge trunk/stable changes check-in: b7a5f89a8c user: bohwaz tags: dev
Changes

Modified doc/index.md from [e4968f9379] to [1d3794b203].

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
*  la gestion des __adhérent⋅e⋅s__ : ajout, modification, suppression, possibilité de choisir les informations présentes sur les fiches adhérent, envoi de mails collectifs aux adhérent⋅e⋅s
*  la tenue de la __comptabilité__ : avoir une gestion comptable complète à même de satisfaire un expert-comptable tout en restant à la portée de celles et ceux qui ne savent pas ce qu'est la comptabilité à double entrée, permettre la production des rapports et bilans annuels et de suivre au jour le jour le budget de l'association
*  la gestion des __cotisations__ et __activités__ : suivi des cotisations à jour, inscriptions et paiement des activités, rappels automatiques par e-mail, etc.
*  le travail __collaboratif__ et __collectif__ : gestion fine des droits d'accès aux fonctions, échange de mails entre membres…
*  la __simplification administrative__ : prise de notes en réunion, archivage et partage de fichiers (afin d'éliminer le besoin d'archiver les documents papier), etc.
*  la publication d'un __site web__ pour l'association, simple mais suffisamment flexible pour pouvoir adapter le fonctionnement à la plupart des besoins
*  l'__autonomisation des adhérents__ : possibilité de mettre à jour leurs informations par eux-même, ou de s'inscrire seul depuis un ordinateur ou un smartphone
*  la possibilité d'adapter aux besoins spécifiques de chaque association via des __extensions__.

Tous ces objectifs ne sont pas encore réalisés, voir :

* [la liste des fonctionnalités disponibles](/wiki/?name=Fonctionnalités) pour ce qui est actuellement disponible ;
* [la feuille de route](/wiki/?name=Roadmap) pour la liste des fonctionnalités qu'il reste à implémenter.

Garradin est un logiciel libre disponible sous licence [AGPL v3](https://www.gnu.org/licenses/why-affero-gpl.fr.html).

Garradin signifie *argent* en *Wagiman*, un dialecte aborigène du nord de l'Australie.

## Documentation et entraide

*  D'abord lire la [documentation](/wiki/?name=Documentation) et notamment la [foire aux questions](/wiki/?name=FAQ)
*  La [liste de discussion d'entraide entre utilisateurs](https://admin.kd2.org/lists/aide@garradin.eu) est le meilleur moyen de vous faire aider :)
*  [Chat d'entraide en direct](https://kiwiirc.com/nextclient/#irc://irc.libera.chat/#garradin?nick=garradin%7C?), ou via IRC : salon `#garradin` sur `irc.libera.chat` (port `6697` avec TLS)

## Participer

Tout coup de main est le bienvenu, pas besoin d'avoir des connaissances techniques ! Nous avons un [guide de contribution](/wiki/?name=Contribuer) pour vous aider à voir comment vous pouvez participer à Garradin :)

### Développement

Garradin est un logiciel libre, développé en PHP, utilisant la base de données SQLite, et avec une interface utilisant HTML, CSS et un peu de Javascript.

Nous acceptons les contributions (plugins, patch, code, tickets, etc.) avec plaisir, consultez la [documentation développeur⋅euse](/wiki/?name=Documentation développeur) pour découvrir comment vous pouvez contribuer.







|












|
|
<










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
*  la gestion des __adhérent⋅e⋅s__ : ajout, modification, suppression, possibilité de choisir les informations présentes sur les fiches adhérent, envoi de mails collectifs aux adhérent⋅e⋅s
*  la tenue de la __comptabilité__ : avoir une gestion comptable complète à même de satisfaire un expert-comptable tout en restant à la portée de celles et ceux qui ne savent pas ce qu'est la comptabilité à double entrée, permettre la production des rapports et bilans annuels et de suivre au jour le jour le budget de l'association
*  la gestion des __cotisations__ et __activités__ : suivi des cotisations à jour, inscriptions et paiement des activités, rappels automatiques par e-mail, etc.
*  le travail __collaboratif__ et __collectif__ : gestion fine des droits d'accès aux fonctions, échange de mails entre membres…
*  la __simplification administrative__ : prise de notes en réunion, archivage et partage de fichiers (afin d'éliminer le besoin d'archiver les documents papier), etc.
*  la publication d'un __site web__ pour l'association, simple mais suffisamment flexible pour pouvoir adapter le fonctionnement à la plupart des besoins
*  l'__autonomisation des adhérents__ : possibilité de mettre à jour leurs informations par eux-même, ou de s'inscrire seul depuis un ordinateur ou un smartphone
*  la possibilité d'adapter aux besoins spécifiques de chaque association via des [__extensions__](/wiki/?name=Extensions).

Tous ces objectifs ne sont pas encore réalisés, voir :

* [la liste des fonctionnalités disponibles](/wiki/?name=Fonctionnalités) pour ce qui est actuellement disponible ;
* [la feuille de route](/wiki/?name=Roadmap) pour la liste des fonctionnalités qu'il reste à implémenter.

Garradin est un logiciel libre disponible sous licence [AGPL v3](https://www.gnu.org/licenses/why-affero-gpl.fr.html).

Garradin signifie *argent* en *Wagiman*, un dialecte aborigène du nord de l'Australie.

## Documentation et entraide

* D'abord lire la [documentation](/wiki/?name=Documentation) et notamment la [foire aux questions](/wiki/?name=FAQ)
* Voir la page [Entraide](/wiki/?name=Entraide) pour accéder aux listes de discussion et au salon de discussion IRC


## Participer

Tout coup de main est le bienvenu, pas besoin d'avoir des connaissances techniques ! Nous avons un [guide de contribution](/wiki/?name=Contribuer) pour vous aider à voir comment vous pouvez participer à Garradin :)

### Développement

Garradin est un logiciel libre, développé en PHP, utilisant la base de données SQLite, et avec une interface utilisant HTML, CSS et un peu de Javascript.

Nous acceptons les contributions (plugins, patch, code, tickets, etc.) avec plaisir, consultez la [documentation développeur⋅euse](/wiki/?name=Documentation développeur) pour découvrir comment vous pouvez contribuer.

Modified src/VERSION from [07c940882d] to [23f241f8f4].

1
1.1.8
|
1
1.1.11

Modified src/config.dist.php from [d4d9a98eb8] to [731c136e13].

427
428
429
430
431
432
433















 * pas possible de stocker plus que cette valeur.
 * Tout envoi de fichier sera refusé.
 *
 * Défaut : null (dans ce cas c'est le stockage qui détermine la taille disponible, donc généralement l'espace dispo sur le disque dur !)
 */

//const FILE_STORAGE_QUOTA = 10000; // Forcer le quota alloué à 10 Mo, quel que soit le backend de stockage






















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
 * pas possible de stocker plus que cette valeur.
 * Tout envoi de fichier sera refusé.
 *
 * Défaut : null (dans ce cas c'est le stockage qui détermine la taille disponible, donc généralement l'espace dispo sur le disque dur !)
 */

//const FILE_STORAGE_QUOTA = 10000; // Forcer le quota alloué à 10 Mo, quel que soit le backend de stockage

/**
 * Commande de création de PDF
 *
 * Commande qui sera exécutée pour créer un fichier PDF à partir d'un HTML.
 * Si laissé non spécifié (ou NULL), Garradin essaiera de détecter une solution entre
 * PrinceXML, Chromium, wkhtmltopdf ou weasyprint.
 *
 * %1$s sera remplacé par le chemin du fichier HTML, et %2$s par le chemin du fichier PDF.
 *
 * Exemple : chromium --headless --print-to-pdf=%2$s %1$s
 *
 * Défaut : null
 */
//const PDF_COMMAND = 'wkhtmltopdf %2$s %1$s';

Added src/include/data/1.0.0_schema.sql version [292ae06778].













































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
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
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
(
    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;

--
-- 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 TRIGGER IF NOT EXISTS acc_years_delete BEFORE DELETE ON acc_years BEGIN
    UPDATE services_fees SET id_account = NULL, id_year = NULL WHERE id_year = OLD.id;
END;

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, id_year);
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_transaction ON acc_transactions_lines (id_transaction);
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,
    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 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) 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
(
    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 src/include/data/1.0.7_migration.sql from [607cda27c7] to [f53124e2db].

1
2
3
4
5
-- Add indexes
DROP INDEX acc_transactions_type;
CREATE INDEX IF NOT EXISTS acc_transactions_type ON acc_transactions (type, id_year);

CREATE INDEX IF NOT EXISTS acc_transactions_lines_transaction ON acc_transactions_lines (id_transaction);

|



1
2
3
4
5
-- Add indexes
DROP INDEX IF EXISTS acc_transactions_type;
CREATE INDEX IF NOT EXISTS acc_transactions_type ON acc_transactions (type, id_year);

CREATE INDEX IF NOT EXISTS acc_transactions_lines_transaction ON acc_transactions_lines (id_transaction);

Modified src/include/data/1.1.7_migration.sql from [40a52d8865] to [1dab4145d3].

1
2
3
4


5

6












7
8
9







ALTER TABLE services_reminders_sent RENAME TO srs_old;

-- Missing acc_years_delete trigger, again, because of missing symlink in previous release
-- Also add new column in services_reminders_sent




.read schema.sql













INSERT INTO services_reminders_sent SELECT id, id_user, id_service, id_reminder, date, date FROM srs_old;
DROP TABLE srs_old;









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



>
>
>
>
>
>
>
1
2

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ALTER TABLE services_reminders_sent RENAME TO srs_old;


-- Add new column in services_reminders_sent
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,

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

CREATE UNIQUE INDEX IF NOT EXISTS srs_index ON services_reminders_sent (id_user, id_service, id_reminder, due_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);

INSERT INTO services_reminders_sent SELECT id, id_user, id_service, id_reminder, date, date FROM srs_old;
DROP TABLE srs_old;

-- Missing acc_years_delete trigger, again, because of missing symlink in previous release
-- Make sure id_account is reset when a year is deleted
CREATE TRIGGER IF NOT EXISTS acc_years_delete BEFORE DELETE ON acc_years BEGIN
    UPDATE services_fees SET id_account = NULL, id_year = NULL WHERE id_year = OLD.id;
END;

Modified src/include/init.php from [aae3e95898] to [22b419591e].

172
173
174
175
176
177
178

179
180
181
182
183
184
185
	'ADMIN_COLOR1'          => '#9c4f15',
	'ADMIN_COLOR2'          => '#d98628',
	'FILE_STORAGE_BACKEND'  => 'SQLite',
	'FILE_STORAGE_CONFIG'   => null,
	'FILE_STORAGE_QUOTA'    => null,
	'API_USER'              => null,
	'API_PASSWORD'          => null,

];

foreach ($default_config as $const => $value)
{
	$const = sprintf('Garradin\\%s', $const);

	if (!defined($const))







>







172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
	'ADMIN_COLOR1'          => '#9c4f15',
	'ADMIN_COLOR2'          => '#d98628',
	'FILE_STORAGE_BACKEND'  => 'SQLite',
	'FILE_STORAGE_CONFIG'   => null,
	'FILE_STORAGE_QUOTA'    => null,
	'API_USER'              => null,
	'API_PASSWORD'          => null,
	'PDF_COMMAND'           => null,
];

foreach ($default_config as $const => $value)
{
	$const = sprintf('Garradin\\%s', $const);

	if (!defined($const))

Modified src/include/lib/Garradin/Accounting/Reports.php from [0f2d41f8c0] to [d37a6b0099].

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
		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 $by_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.description AS account_description, 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,
			(SELECT SUM(l2.credit - l2.debit) FROM acc_transactions_lines l2
				INNER JOIN acc_transactions t2 ON t2.id = l2.id_transaction
				INNER JOIN acc_accounts a2 ON a2.id = l2.id_account
				WHERE a2.position = %d AND l2.id_analytical = l.id_analytical AND t2.id_year = t.id_year) * -1 AS sum_expense,
			(SELECT SUM(l2.credit - l2.debit) FROM acc_transactions_lines l2
				INNER JOIN acc_transactions t2 ON t2.id = l2.id_transaction
				INNER JOIN acc_accounts a2 ON a2.id = l2.id_account







>
>
>
>
















|







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
		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 (!empty($criterias['analytical_only'])) {
			$where[] = 'l.id_analytical IS NOT NULL';
		}

		if (!count($where)) {
			throw new \LogicException('Unknown criteria');
		}

		return implode(' AND ', $where);
	}

	/**
	 * Return account sums per year or per account
	 * @param  bool $by_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.description AS account_description, 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, 0 AS total,
			(SELECT SUM(l2.credit - l2.debit) FROM acc_transactions_lines l2
				INNER JOIN acc_transactions t2 ON t2.id = l2.id_transaction
				INNER JOIN acc_accounts a2 ON a2.id = l2.id_account
				WHERE a2.position = %d AND l2.id_analytical = l.id_analytical AND t2.id_year = t.id_year) * -1 AS sum_expense,
			(SELECT SUM(l2.credit - l2.debit) FROM acc_transactions_lines l2
				INNER JOIN acc_transactions t2 ON t2.id = l2.id_transaction
				INNER JOIN acc_accounts a2 ON a2.id = l2.id_account
103
104
105
106
107
108
109

110
111
112
113
114
115
116

		$total = function (\stdClass $current, bool $by_year) use ($sums)
		{
			$out = (object) [
				'label' => 'Total',
				'id_account' => $by_year ? null : $current->id,
				'id_year' => $by_year ? $current->id : null,

			];

			foreach ($sums as $s) {
				$out->{$s} = $current->{$s};
			}

			return $out;







>







107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

		$total = function (\stdClass $current, bool $by_year) use ($sums)
		{
			$out = (object) [
				'label' => 'Total',
				'id_account' => $by_year ? null : $current->id,
				'id_year' => $by_year ? $current->id : null,
				'total' => 1,
			];

			foreach ($sums as $s) {
				$out->{$s} = $current->{$s};
			}

			return $out;
361
362
363
364
365
366
367
368







369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
	 * 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, t.id, t.date, t.reference,
			l.debit, l.credit, l.reference AS line_reference, t.label, l.label AS line_label,
			a.label AS account_label, a.code AS account_code
			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;

		foreach ($db->iterate($sql) as $row) {
			if (null !== $account && $account->id != $row->id_account) {
				yield $account;








>
>
>
>
>
>
>

|




|

|







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
	 * Grand livre
	 */
	static public function getGeneralLedger(array $criterias): \Generator
	{
		$where = self::getWhereClause($criterias);

		$db = DB::getInstance();

		if (!empty($criterias['analytical_only'])) {
			$join = 'acc_accounts a ON a.id = l.id_analytical';
		}
		else {
			$join = 'acc_accounts a ON a.id = l.id_account';
		}

		$sql = sprintf('SELECT
			t.id_year, a.id AS id_account, t.id, t.date, t.reference,
			l.debit, l.credit, l.reference AS line_reference, t.label, l.label AS line_label,
			a.label AS account_label, a.code AS account_code
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			INNER JOIN %s
			WHERE %s
			ORDER BY a.code COLLATE NOCASE, t.date, t.id;', $join, $where);

		$account = null;
		$debit = $credit = 0;

		foreach ($db->iterate($sql) as $row) {
			if (null !== $account && $account->id != $row->id_account) {
				yield $account;

Modified src/include/lib/Garradin/Accounting/Transactions.php from [b8d932894e] to [f117213851].

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
		'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)







|







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
		'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 = ['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)

Modified src/include/lib/Garradin/DB.php from [7ce0e3f7d3] to [ba3c284bd4].

208
209
210
211
212
213
214
215
216

217
218








219


220




221
222
223
224
225
226
227
228
     * @see https://www.sqlite.org/c3ref/strlike.html
     * @see https://sqlite.org/src/file?name=ext/icu/icu.c&ci=trunk
     */
    static public function unicodeLike($pattern, $value, $escape = null) {
        $id = md5($pattern . $escape);

        if (!array_key_exists($id, self::$unicode_patterns_cache)) {
            $pattern = Utils::unicodeCaseFold($pattern);
            $escape = $escape ? '(?!' . preg_quote($escape, '/') . ')' : '';

            $pattern = preg_quote($pattern, '/');
            $pattern = preg_replace('/' . $escape . '%/', '.*', $pattern);








            $pattern = preg_replace('/' . $escape . '_/', '.', $pattern);


            $pattern = '/' . $pattern . '/';




            self::$unicode_patterns_cache[$id] = $pattern;
        }

        $value = Utils::unicodeCaseFold($value);

        return (bool) preg_match(self::$unicode_patterns_cache[$id], $value);
    }
}







<

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








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
     * @see https://www.sqlite.org/c3ref/strlike.html
     * @see https://sqlite.org/src/file?name=ext/icu/icu.c&ci=trunk
     */
    static public function unicodeLike($pattern, $value, $escape = null) {
        $id = md5($pattern . $escape);

        if (!array_key_exists($id, self::$unicode_patterns_cache)) {

            $escape = $escape ? '(?!' . preg_quote($escape, '/') . ')' : '';
            preg_match_all('/('.$escape.'[%_])|(\w+)|(.+?)/iu', $pattern, $parts, PREG_SET_ORDER);
            $pattern = '';

            foreach ($parts as $part) {
                if (isset($part[3])) {
                    $pattern .= preg_quote($part[0], '/');
                }
                elseif (isset($part[2])) {
                    $pattern .= Utils::unicodeCaseFold($part[2]);
                }
                elseif ($part[1] == '%') {
                    $pattern .= '.*';
                }
                elseif ($part[1] == '_') {
                    $pattern .= '.';
                }
            }

            $pattern = '/^' . $pattern . '$/im';
            self::$unicode_patterns_cache[$id] = $pattern;
        }

        $value = Utils::unicodeCaseFold($value);

        return (bool) preg_match(self::$unicode_patterns_cache[$id], $value);
    }
}

Modified src/include/lib/Garradin/Entities/Files/File.php from [f33e63dac6] to [2f3a044bb4].

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
	public function selfCheck(): void
	{
		$this->assert($this->type === self::TYPE_DIRECTORY || $this->type === self::TYPE_FILE, 'Unknown file type');
		$this->assert($this->type === self::TYPE_DIRECTORY || $this->size !== null, 'File size must be set');
		$this->assert($this->image === 0 || $this->image === 1, 'Unknown image value');
		$this->assert(trim($this->name) !== '', 'Le nom de fichier ne peut rester vide');
		$this->assert(strlen($this->path), 'Le chemin ne peut rester vide');
		$this->assert(strlen($this->parent) || null === $this->parent, 'Le chemin ne peut rester vide');
	}

	public function context(): string
	{
		return strtok($this->path, '/');
	}








|







129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
	public function selfCheck(): void
	{
		$this->assert($this->type === self::TYPE_DIRECTORY || $this->type === self::TYPE_FILE, 'Unknown file type');
		$this->assert($this->type === self::TYPE_DIRECTORY || $this->size !== null, 'File size must be set');
		$this->assert($this->image === 0 || $this->image === 1, 'Unknown image value');
		$this->assert(trim($this->name) !== '', 'Le nom de fichier ne peut rester vide');
		$this->assert(strlen($this->path), 'Le chemin ne peut rester vide');
		$this->assert(strlen($this->parent) || '' === $this->parent, 'Le chemin ne peut rester vide');
	}

	public function context(): string
	{
		return strtok($this->path, '/');
	}

167
168
169
170
171
172
173


174
175
176
177
178
179
180
		Plugin::fireSignal('files.delete', ['file' => $this]);

		// clean up thumbnails
		foreach (self::ALLOWED_THUMB_SIZES as $size)
		{
			Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size));
		}



		if ($this->exists()) {
			return parent::delete();
		}

		return true;
	}







>
>







167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
		Plugin::fireSignal('files.delete', ['file' => $this]);

		// clean up thumbnails
		foreach (self::ALLOWED_THUMB_SIZES as $size)
		{
			Static_Cache::remove(sprintf(self::THUMB_CACHE_ID, $this->pathHash(), $size));
		}

		DB::getInstance()->delete('files_search', 'path = ? OR path LIKE ?', $this->path, $this->path . '/%');

		if ($this->exists()) {
			return parent::delete();
		}

		return true;
	}
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
	}

	public function fetch()
	{
		return Files::callStorage('fetch', $this);
	}

	public function render(array $options = [])
	{
		$editor_type = $this->renderFormat();

		if ($editor_type == 'text') {
			return sprintf('<pre>%s</pre>', htmlspecialchars($this->fetch()));
		}
		elseif (!$editor_type) {
			throw new \LogicException('Cannot render file of this type');
		}
		else {
			return Render::render($editor_type, $this, $this->fetch(), $options);
		}
	}

	public function checkReadAccess(?Session $session): bool
	{
		// Web pages and config files are always public
		if ($this->isPublic()) {







|










|







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
	}

	public function fetch()
	{
		return Files::callStorage('fetch', $this);
	}

	public function render(?string $user_prefix = null)
	{
		$editor_type = $this->renderFormat();

		if ($editor_type == 'text') {
			return sprintf('<pre>%s</pre>', htmlspecialchars($this->fetch()));
		}
		elseif (!$editor_type) {
			throw new \LogicException('Cannot render file of this type');
		}
		else {
			return Render::render($editor_type, $this, $this->fetch(), $user_prefix);
		}
	}

	public function checkReadAccess(?Session $session): bool
	{
		// Web pages and config files are always public
		if ($this->isPublic()) {

Modified src/include/lib/Garradin/Entities/Services/Fee.php from [0f3df37a82] to [7491493b33].

166
167
168
169
170
171
172





173
174
175
176
177
178
179
		$conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL)
			AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $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 AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id());







>
>
>
>
>







166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
		$conditions = sprintf('su.id_fee = %d AND su.paid = 1 AND (su.expiry_date >= date() OR su.expiry_date IS NULL)
			AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id());

		$list = new DynamicList($columns, $tables, $conditions);
		$list->groupBy('su.id_user');
		$list->orderBy('date', true);
		$list->setCount('COUNT(DISTINCT su.id_user)');

		$list->setExportCallback(function (&$row) {
			$row->paid_amount = $row->paid_amount ? Utils::money_format($row->paid_amount, '.', '', false) : null;
		});

		return $list;
	}

	public function unpaidUsersList(): DynamicList
	{
		$list = $this->paidUsersList();
		$conditions = sprintf('su.id_fee = %d AND su.paid = 0 AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id());

Modified src/include/lib/Garradin/Entities/Services/Service_User.php from [9cb86069a3] to [10de17a459].

111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
		if ($this->fee()->label != $label) {
			$label .= ' - ' . $this->fee()->label;
		}

		$label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user));

		$source['label'] = $label;
		$source['date'] = $this->date->format('d/m/Y');

		$transaction->importFromNewForm($source);
		$transaction->save();
		$transaction->linkToUser($this->id_user, $this->id());

		return $transaction;
	}







<







111
112
113
114
115
116
117

118
119
120
121
122
123
124
		if ($this->fee()->label != $label) {
			$label .= ' - ' . $this->fee()->label;
		}

		$label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user));

		$source['label'] = $label;


		$transaction->importFromNewForm($source);
		$transaction->save();
		$transaction->linkToUser($this->id_user, $this->id());

		return $transaction;
	}

Modified src/include/lib/Garradin/Entities/Web/Page.php from [7dc41152d2] to [a8ee380b83].

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
		return $this->_file;
	}

	public function load(array $data): void
	{
		parent::load($data);

		if ($this->file() && $this->file()->modified != $this->modified) {
			$this->loadFromFile($this->file());
			$this->save();
		}
	}

	public function url(): string
	{







|







109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
		return $this->_file;
	}

	public function load(array $data): void
	{
		parent::load($data);

		if ($this->file() && $this->file()->modified > $this->modified) {
			$this->loadFromFile($this->file());
			$this->save();
		}
	}

	public function url(): string
	{
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
	{
		$out = $this->asArray();
		$out['url'] = $this->url();
		$out['html'] = $this->render();
		return $out;
	}

	public function render(array $options = []): string
	{
		if (!$this->file()) {
			throw new \LogicException('File does not exist: '  . $this->file_path);
		}

		return Render::render($this->format, $this->file(), $this->content, $options);
	}

	public function preview(string $content): string
	{
		return Render::render($this->format, $this->file(), $content, ['prefix' => '#']);
	}

	public function filepath(bool $stored = true): string
	{
		return $stored && isset($this->file_path) ? $this->file_path : File::CONTEXT_WEB . '/' . $this->path . '/' . $this->_name;
	}








|





|




|







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
	{
		$out = $this->asArray();
		$out['url'] = $this->url();
		$out['html'] = $this->render();
		return $out;
	}

	public function render(?string $user_prefix = null): string
	{
		if (!$this->file()) {
			throw new \LogicException('File does not exist: '  . $this->file_path);
		}

		return Render::render($this->format, $this->file(), $this->content, $user_prefix);
	}

	public function preview(string $content): string
	{
		return Render::render($this->format, $this->file(), $content, '#');
	}

	public function filepath(bool $stored = true): string
	{
		return $stored && isset($this->file_path) ? $this->file_path : File::CONTEXT_WEB . '/' . $this->path . '/' . $this->_name;
	}

181
182
183
184
185
186
187

188
189
190
191
192
193
194
				$this->_file = null;
			}

			$file = $this->file();

			// Or update file
			if ($file->fetch() !== $export) {

				$file->store(null, $export);
			}
		}

		$this->syncSearch();
	}








>







181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
				$this->_file = null;
			}

			$file = $this->file();

			// Or update file
			if ($file->fetch() !== $export) {
				$file->set('modified', $this->modified);
				$file->store(null, $export);
			}
		}

		$this->syncSearch();
	}

202
203
204
205
206
207
208





209
210
211
212
213
214
215
	{
		$change_parent = null;

		if (isset($this->_modified['uri']) || isset($this->_modified['path'])) {
			$this->set('file_path', $this->filepath(false));
			$change_parent = $this->_modified['path'];
		}






		$current_path = $this->_modified['file_path'] ?? $this->file_path;
		parent::save();
		$this->syncFile($current_path);

		// Rename/move children
		if ($change_parent) {







>
>
>
>
>







203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
	{
		$change_parent = null;

		if (isset($this->_modified['uri']) || isset($this->_modified['path'])) {
			$this->set('file_path', $this->filepath(false));
			$change_parent = $this->_modified['path'];
		}

		// Update modified date if required
		if (count($this->_modified) && !isset($this->_modified['modified'])) {
			$this->set('modified', new \DateTime);
		}

		$current_path = $this->_modified['file_path'] ?? $this->file_path;
		parent::save();
		$this->syncFile($current_path);

		// Rename/move children
		if ($change_parent) {

Modified src/include/lib/Garradin/Entity.php from [722d6cf126] to [46fdcfd8d1].

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
	// Add plugin signals to save/delete
	public function save(): bool
	{
		$name = get_class($this);
		$name = str_replace('Garradin\Entities', '', $name);
		$name = 'entity.' . $name . '.save';


		if (Plugin::fireSignal($name . '.before', ['entity' => $this])) {
			return true;
		}






		$return = parent::save();
		Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]);



		return $return;
	}

	public function delete(): bool
	{
		$name = get_class($this);
		$name = str_replace('Garradin\Entities', '', $name);
		$name = 'entity.' . $name . '.delete';

		if (Plugin::fireSignal($name . '.before', ['entity' => $this])) {
			return true;
		}






		$return = parent::delete();
		Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]);


		return $return;
	}
}







>



>
>
>
>
>



>
>













>
>
>
>
>



>




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
	// Add plugin signals to save/delete
	public function save(): bool
	{
		$name = get_class($this);
		$name = str_replace('Garradin\Entities', '', $name);
		$name = 'entity.' . $name . '.save';

		// Specific entity signal
		if (Plugin::fireSignal($name . '.before', ['entity' => $this])) {
			return true;
		}

		// Generic entity signal
		if (Plugin::fireSignal('entity.save.before', ['entity' => $this])) {
			return true;
		}

		$return = parent::save();
		Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]);

		Plugin::fireSignal('entity.save.after', ['entity' => $this, 'success' => $return]);

		return $return;
	}

	public function delete(): bool
	{
		$name = get_class($this);
		$name = str_replace('Garradin\Entities', '', $name);
		$name = 'entity.' . $name . '.delete';

		if (Plugin::fireSignal($name . '.before', ['entity' => $this])) {
			return true;
		}

		// Generic entity signal
		if (Plugin::fireSignal('entity.delete.before', ['entity' => $this])) {
			return true;
		}

		$return = parent::delete();
		Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]);
		Plugin::fireSignal('entity.delete.after', ['entity' => $this, 'success' => $return]);

		return $return;
	}
}

Modified src/include/lib/Garradin/Files/Files.php from [893a79714f] to [9196c1dfe3].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
<?php

namespace Garradin\Files;

use Garradin\Static_Cache;
use Garradin\DB;
use Garradin\Utils;

use Garradin\ValidationException;
use Garradin\Membres\Session;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;

use KD2\DB\EntityManager as EM;
use KD2\ZipWriter;







>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Garradin\Files;

use Garradin\Static_Cache;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Membres\Session;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;

use KD2\DB\EntityManager as EM;
use KD2\ZipWriter;
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
			call_user_func([$to, 'lock']);

			$db = DB::getInstance();
			$db->begin();
			$i = 0;

			self::migrateDirectory($from, $to, '', $i, $callback);

			$db->commit();

		}
		finally {

			call_user_func([$from, 'unlock']);
			call_user_func([$to, 'unlock']);
		}
	}

	static protected function migrateDirectory(string $from, string $to, string $path, int &$i, ?callable $callback)
	{
		$db = DB::getInstance();

		foreach (call_user_func([$from, 'list'], $path) as $file) {





			if (++$i >= 100) {
				$db->commit();
				$db->begin();
				$i = 0;
			}

			if ($file->type == File::TYPE_DIRECTORY) {







|
|
>


>










>
>
>
>
>







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
			call_user_func([$to, 'lock']);

			$db = DB::getInstance();
			$db->begin();
			$i = 0;

			self::migrateDirectory($from, $to, '', $i, $callback);
		}
		catch (UserException $e) {
			throw new \RuntimeException('Migration failed', 0, $e);
		}
		finally {
			$db->commit();
			call_user_func([$from, 'unlock']);
			call_user_func([$to, 'unlock']);
		}
	}

	static protected function migrateDirectory(string $from, string $to, string $path, int &$i, ?callable $callback)
	{
		$db = DB::getInstance();

		foreach (call_user_func([$from, 'list'], $path) as $file) {
			if (!$file->parent && $file->name == '.lock') {
				// Ignore lock file
				continue;
			}

			if (++$i >= 100) {
				$db->commit();
				$db->begin();
				$i = 0;
			}

			if ($file->type == File::TYPE_DIRECTORY) {
282
283
284
285
286
287
288
289
290


291
292

293
294
295
296
297
298
299

		return $quota;
	}

	static public function getRemainingQuota(bool $force_refresh = false): float
	{
		if (FILE_STORAGE_QUOTA !== null) {
			return FILE_STORAGE_QUOTA - self::getUsedQuota($force_refresh);
		}



		return self::callStorage('getRemainingQuota');

	}

	static public function checkQuota(int $size = 0): void
	{
		if (!self::$quota) {
			return;
		}







|

>
>
|
|
>







290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310

		return $quota;
	}

	static public function getRemainingQuota(bool $force_refresh = false): float
	{
		if (FILE_STORAGE_QUOTA !== null) {
			$quota = FILE_STORAGE_QUOTA - self::getUsedQuota($force_refresh);
		}
		else {
			$quota = self::callStorage('getRemainingQuota');
		}

		return max(0, $quota);
	}

	static public function checkQuota(int $size = 0): void
	{
		if (!self::$quota) {
			return;
		}
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
		if (FILE_STORAGE_BACKEND == 'SQLite') {
			return 'files';
		}

		return 'tmp_files';
	}

	static public function syncVirtualTable(string $parent = '')
	{
		if (FILE_STORAGE_BACKEND == 'SQLite') {
			// No need to create a virtual table, use the real one
			return;
		}

		$db = DB::getInstance();
		$db->begin();

		$db->exec('CREATE TEMP TABLE IF NOT EXISTS tmp_files AS SELECT * FROM files WHERE 0;');

		foreach (Files::list($parent) as $file) {





			$db->insert('tmp_files', $file->asArray(true));




		}

		$db->commit();
	}
}







|








>



>
>
>
>
>

>
>
>
>





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
		if (FILE_STORAGE_BACKEND == 'SQLite') {
			return 'files';
		}

		return 'tmp_files';
	}

	static public function syncVirtualTable(string $parent = '', bool $recursive = false)
	{
		if (FILE_STORAGE_BACKEND == 'SQLite') {
			// No need to create a virtual table, use the real one
			return;
		}

		$db = DB::getInstance();
		$db->begin();

		$db->exec('CREATE TEMP TABLE IF NOT EXISTS tmp_files AS SELECT * FROM files WHERE 0;');

		foreach (Files::list($parent) as $file) {
			// Ignore additional directories
			if ($parent == '' && !array_key_exists($file->name, File::CONTEXTS_NAMES)) {
				continue;
			}

			$db->insert('tmp_files', $file->asArray(true));

			if ($recursive && $file->type === $file::TYPE_DIRECTORY) {
				self::syncVirtualTable($file->path, $recursive);
			}
		}

		$db->commit();
	}
}

Modified src/include/lib/Garradin/Files/Storage/FileSystem.php from [33ec3a8fb7] to [286067b967].

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

		$data['modified']->setTimeZone(new \DateTimeZone(date_default_timezone_get()));

		$data['image'] = (int) in_array($data['mime'], File::IMAGE_TYPES);

		$file = new File;
		$file->load($data);


		return $file;
	}

	static public function list(string $path): array
	{
		$fullpath = self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);
		$fullpath = rtrim($fullpath, DIRECTORY_SEPARATOR);

		if (!file_exists($fullpath)) {
			return [];
		}

		$files = [];

		foreach (new \FilesystemIterator($fullpath, \FilesystemIterator::SKIP_DOTS) as $file) {





			// Used to make sorting easier
			// directory_blabla
			// file_image.jpeg
			$files[$file->getType() . '_' .$file->getFilename()] = self::_SplToFile($file);
		}

		return Utils::knatcasesort($files);







>
















>
>
>
>
>







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

		$data['modified']->setTimeZone(new \DateTimeZone(date_default_timezone_get()));

		$data['image'] = (int) in_array($data['mime'], File::IMAGE_TYPES);

		$file = new File;
		$file->load($data);
		$file->parent = $parent; // Force empty parent to be empty, not null

		return $file;
	}

	static public function list(string $path): array
	{
		$fullpath = self::_getRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);
		$fullpath = rtrim($fullpath, DIRECTORY_SEPARATOR);

		if (!file_exists($fullpath)) {
			return [];
		}

		$files = [];

		foreach (new \FilesystemIterator($fullpath, \FilesystemIterator::SKIP_DOTS) as $file) {
			// Seems that SKIP_DOTS does not work all the time?
			if ($file->getFilename()[0] == '.') {
				continue;
			}

			// Used to make sorting easier
			// directory_blabla
			// file_image.jpeg
			$files[$file->getType() . '_' .$file->getFilename()] = self::_SplToFile($file);
		}

		return Utils::knatcasesort($files);
301
302
303
304
305
306
307
308
309
310
311
	}

	static public function checkLock(): void
	{
		$lock = file_exists(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock');

		if ($lock) {
			throw new \RuntimeException('File storage is locked');
		}
	}
}







|



307
308
309
310
311
312
313
314
315
316
317
	}

	static public function checkLock(): void
	{
		$lock = file_exists(self::_getRoot() . DIRECTORY_SEPARATOR . '.lock');

		if ($lock) {
			throw new \RuntimeException('FileSystem storage is locked');
		}
	}
}

Modified src/include/lib/Garradin/Files/Storage/SQLite.php from [ce000d8bf2] to [69084d5de4].

127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

	static public function listDirectoriesRecursively(string $path): array
	{
		$files = [];
		$it = DB::getInstance()->iterate('SELECT path FROM files WHERE parent LIKE ? ORDER BY path;', $path . '/%');

		foreach ($it as $file) {
			$files[] = $file->path;
		}

		return $files;
	}

	static public function exists(string $path): bool
	{







|







127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

	static public function listDirectoriesRecursively(string $path): array
	{
		$files = [];
		$it = DB::getInstance()->iterate('SELECT path FROM files WHERE parent LIKE ? ORDER BY path;', $path . '/%');

		foreach ($it as $file) {
			$files[] = substr($file->path, strlen($path) + 1);
		}

		return $files;
	}

	static public function exists(string $path): bool
	{

Modified src/include/lib/Garradin/Plugin.php from [2c3e05e869] to [d9b7c0cb75].

725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
			{
				throw new \RuntimeException('Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".');
			}
		}

		if (!empty($infos->min_version) && !version_compare(garradin_version(), $infos->min_version, '>='))
		{
			throw new \RuntimeException('Le plugin '.$id.' nécessite Garradin version '.$infos->min_version.' ou supérieure.');
		}

		if (!empty($infos->max_version) && !version_compare(garradin_version(), $infos->max_version, '>'))
		{
			throw new \RuntimeException('Le plugin '.$id.' nécessite Garradin version '.$infos->max_version.' ou inférieure.');
		}

		if (!empty($infos->menu) && !file_exists($path . '/www/admin/index.php'))
		{
			throw new \RuntimeException('Le plugin '.$id.' ne comporte pas de fichier www/admin/index.php alors qu\'il demande à figurer au menu.');
		}








|




|







725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
			{
				throw new \RuntimeException('Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".');
			}
		}

		if (!empty($infos->min_version) && !version_compare(garradin_version(), $infos->min_version, '>='))
		{
			throw new UserException('Le plugin '.$id.' nécessite Garradin version '.$infos->min_version.' ou supérieure.');
		}

		if (!empty($infos->max_version) && !version_compare(garradin_version(), $infos->max_version, '>'))
		{
			throw new UserException('Le plugin '.$id.' nécessite Garradin version '.$infos->max_version.' ou inférieure.');
		}

		if (!empty($infos->menu) && !file_exists($path . '/www/admin/index.php'))
		{
			throw new \RuntimeException('Le plugin '.$id.' ne comporte pas de fichier www/admin/index.php alors qu\'il demande à figurer au menu.');
		}

Modified src/include/lib/Garradin/Recherche.php from [ee18756339] to [5e796b3eb4].

444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
				}

				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']];

				$query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']);








|







444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
				}

				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
					continue;
				}

				$query_columns[] = $condition['column'];
				$column = $target_columns[$condition['column']];

				$query = sprintf('%s %s', $db->quoteIdentifier($condition['column']), $condition['operator']);

518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
			{
				$query_groups[] = implode(' ' . $group['operator'] . ' ', $query_group_conditions);
			}
		}

		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([$config->get('champ_identite')], $query_columns);
		}







|







518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
			{
				$query_groups[] = implode(' ' . $group['operator'] . ' ', $query_group_conditions);
			}
		}

		if (!count($query_groups))
		{
			throw new UserException('Aucune clause trouvée dans la recherche : elle contenait peut-être des clauses qui correspondent à des champs qui ont été supprimés ?');
		}

		// Ajout du champ identité si pas présent
		if ($target == 'membres')
		{
			$query_columns = array_merge([$config->get('champ_identite')], $query_columns);
		}

Modified src/include/lib/Garradin/Template.php from [7374ce76d8] to [426892fe1f].

16
17
18
19
20
21
22




















23
24
25
26
27
28
29
{
	static protected $_instance = null;

	static public function getInstance()
	{
		return self::$_instance ?: self::$_instance = new Template;
	}





















	private function __clone()
	{
	}

	public function __construct()
	{







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







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
{
	static protected $_instance = null;

	static public function getInstance()
	{
		return self::$_instance ?: self::$_instance = new Template;
	}

	public function display($template = null)
	{
		if (isset($_GET['_pdf'])) {
			$out = $this->fetch($template);

			$filename = 'Print.pdf';

			if (preg_match('!<title>(.*)</title>!U', $out, $match)) {
				$filename = trim($match[1]) . '.pdf';
			}

			header('Content-type: application/pdf');
			header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($filename)));
			Utils::streamPDF($out);
			return $this;
		}

		return parent::display($template);
	}

	private function __clone()
	{
	}

	public function __construct()
	{
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
		$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_skriv', function ($str) {
			$skriv = new Skriv(null);
			return $skriv->render((string) $str);
		});

		foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) {
			$this->register_modifier(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name);
		}








|







114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
		$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_skriv', function ($str) {
			$skriv = new Skriv;
			return $skriv->render((string) $str);
		});

		foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) {
			$this->register_modifier(is_int($key) ? $name : $key, is_int($key) ? [CommonModifiers::class, $name] : $name);
		}

Modified src/include/lib/Garradin/Upgrade.php from [0b2351ebf8] to [a3d7e4ed57].

55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

		try {
			if (version_compare($v, '1.0.0-rc1', '<'))
			{
				$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-rc10', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-rc10_migration.sql');







<
<
<
<
<
<
<
<







55
56
57
58
59
60
61








62
63
64
65
66
67
68

		try {
			if (version_compare($v, '1.0.0-rc1', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0_migration.sql');
				$db->commitSchemaUpdate();








			}


			if (version_compare($v, '1.0.0-rc10', '<'))
			{
				$db->beginSchemaUpdate();
				$db->import(ROOT . '/include/data/1.0.0-rc10_migration.sql');
318
319
320
321
322
323
324














325
326
327
328
329
330
331
				\Garradin\Web\Web::sync();

				// Add UNIQUE index
				$db->import(ROOT . '/include/data/1.1.8_migration.sql');

				$db->commit();
			}















			// Vérification de la cohérence des clés étrangères
			$db->foreignKeyCheck();

			// Delete local cached files
			Utils::resetCache(USER_TEMPLATES_CACHE_ROOT);
			Utils::resetCache(STATIC_CACHE_ROOT);







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







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
				\Garradin\Web\Web::sync();

				// Add UNIQUE index
				$db->import(ROOT . '/include/data/1.1.8_migration.sql');

				$db->commit();
			}

			if (version_compare($v, '1.1.8', '==')) {
				// Force sync to add missing pages if you had the buggy 1.1.8 version
				\Garradin\Web\Web::sync(true);
			}

			if (version_compare($v, '1.1.10', '<')) {
				\Garradin\Web\Web::sync(true); // Force sync of web pages
				Files::syncVirtualTable('', true);

				$db->begin();
				$db->exec(sprintf('DELETE FROM files_search WHERE path NOT IN (SELECT path FROM %s);', Files::getVirtualTableName()));
				$db->commit();
			}

			// Vérification de la cohérence des clés étrangères
			$db->foreignKeyCheck();

			// Delete local cached files
			Utils::resetCache(USER_TEMPLATES_CACHE_ROOT);
			Utils::resetCache(STATIC_CACHE_ROOT);

Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [e9e89801c5] to [e398fea153].

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		else {
			return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>';
		}
	}

	static public function atom_date($date)
	{
		return Utils::date_fr(DATE_ATOM, $date);
	}

	static public function xml_escape($str)
	{
		return htmlspecialchars($str, ENT_XML1);
	}
}







|







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		else {
			return '<a href="'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'">'.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').'</a>';
		}
	}

	static public function atom_date($date)
	{
		return Utils::date_fr($date, DATE_ATOM);
	}

	static public function xml_escape($str)
	{
		return htmlspecialchars($str, ENT_XML1);
	}
}

Modified src/include/lib/Garradin/UserTemplate/Sections.php from [9a0667f343] to [b168f12ea3].

291
292
293
294
295
296
297




298
299
300
301
302
303
304
		}

		// Allow for count=true, count=1 and also count="DISTINCT user_id" count="id"
		if (!empty($params['count'])) {
			$params['select'] = sprintf('COUNT(%s) AS count', $params['count'] == 1 ? '*' : $params['count']);
			$params['order'] = '1';
		}





		$sql = sprintf('SELECT %s FROM %s WHERE 1 %s %s ORDER BY %s LIMIT %d,%d;',
			$params['select'],
			$params['tables'],
			$params['where'] ?? '',
			isset($params['group']) ? 'GROUP BY ' . $params['group'] : '',
			$params['order'],







>
>
>
>







291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
		}

		// Allow for count=true, count=1 and also count="DISTINCT user_id" count="id"
		if (!empty($params['count'])) {
			$params['select'] = sprintf('COUNT(%s) AS count', $params['count'] == 1 ? '*' : $params['count']);
			$params['order'] = '1';
		}

		if (!empty($params['where']) && !preg_match('/^\s*AND\s+/i', $params['where'])) {
			$params['where'] = ' AND ' . $params['where'];
		}

		$sql = sprintf('SELECT %s FROM %s WHERE 1 %s %s ORDER BY %s LIMIT %d,%d;',
			$params['select'],
			$params['tables'],
			$params['where'] ?? '',
			isset($params['group']) ? 'GROUP BY ' . $params['group'] : '',
			$params['order'],

Modified src/include/lib/Garradin/Utils.php from [87963c7e33] to [2a526310a9].

14
15
16
17
18
19
20

21






22





23
24




25
26

















27
28
29
30
31
32
33
    const EMAIL_CONTEXT_PRIVATE = 'private';
    const EMAIL_CONTEXT_SYSTEM = 'system';

    static protected $collator;
    static protected $transliterator;

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







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







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
    const EMAIL_CONTEXT_PRIVATE = 'private';
    const EMAIL_CONTEXT_SYSTEM = 'system';

    static protected $collator;
    static protected $transliterator;

    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',
        'Jan' => 'jan',
        'Feb' => 'fév',

        'Mar' => 'mar',
        'Apr' => 'avr',
        'Jun' => 'juin',
        'Jul' => 'juil',
        'Aug' => 'août',
        'Sep' => 'sep',
        'Oct' => 'oct',
        'Nov' => 'nov',
        '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)) {
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 (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)







<



















<







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
        if (null === $ts) {
            return $ts;
        }

        $date = strftime($format, $ts->getTimestamp());

        $date = strtr($date, self::FRENCH_DATE_NAMES);

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

        return $date;
    }

    /**
     * @deprecated
     */
    static public function checkDate($str)
862
863
864
865
866
867
868









869
870
871
872
873
874
875

        $str = preg_replace('![^\w\d_-]!i', '-', $str);
        $str = preg_replace('!-{2,}!', '-', $str);
        $str = trim($str, '-');

        return $str;
    }










    /**
     * dirname may have undefined behaviour depending on the locale!
     */
    static public function dirname(string $str): string
    {
        $str = str_replace(DIRECTORY_SEPARATOR, '/', $str);







>
>
>
>
>
>
>
>
>







892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914

        $str = preg_replace('![^\w\d_-]!i', '-', $str);
        $str = preg_replace('!-{2,}!', '-', $str);
        $str = trim($str, '-');

        return $str;
    }

    static public function safeFileName(string $str): string
    {
        $str = Utils::transliterateToAscii($str);
        $str = preg_replace('![^\w\d_ -]!i', '.', $str);
        $str = preg_replace('!\.{2,}!', '.', $str);
        $str = trim($str, '.');
        return $str;
    }

    /**
     * dirname may have undefined behaviour depending on the locale!
     */
    static public function dirname(string $str): string
    {
        $str = str_replace(DIRECTORY_SEPARATOR, '/', $str);
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942


























































































































    static public function unicodeCaseFold(?string $str): string
    {
        if (null === $str || trim($str) === '') {
            return '';
        }

        if (!isset(self::$transliterator) && function_exists('transliterator_create')) {
            self::$transliterator = \Transliterator::create('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove; Lower();');
        }

        if (isset(self::$transliterator)) {
            return self::$transliterator->transliterate($str);
        }

        return strtoupper(self::transliterateToAscii($str));
    }

    static public function knatcasesort(array $array)
    {
        uksort($array, [self::class, 'unicodeCaseComparison']);
        return $array;
    }
}

































































































































|














|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
    static public function unicodeCaseFold(?string $str): string
    {
        if (null === $str || trim($str) === '') {
            return '';
        }

        if (!isset(self::$transliterator) && function_exists('transliterator_create')) {
            self::$transliterator = \Transliterator::create('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; Lower();');
        }

        if (isset(self::$transliterator)) {
            return self::$transliterator->transliterate($str);
        }

        return strtoupper(self::transliterateToAscii($str));
    }

    static public function knatcasesort(array $array)
    {
        uksort($array, [self::class, 'unicodeCaseComparison']);
        return $array;
    }

    /**
     * Displays a PDF from a string, only works when PDF_COMMAND constant is set to "prince"
     * @param  string $str HTML string
     * @return void
     */
    static public function streamPDF(string $str): void
    {
        if (!PDF_COMMAND) {
            // Try to see if there's a plugin
            $in = ['string' => $str];

            if (Plugin::fireSignal('pdf.stream', $in)) {
                return;
            }

            unset($in);
        }

        // Only Prince handles using STDIN and STDOUT
        if (PDF_COMMAND != 'prince') {
            $file = self::filePDF($str);
            readfile($file);
            unlink($file);
            return;
        }

        $descriptorspec = [
            0 => ["pipe", "r"], // stdin is a pipe that the child will read from
            1 => ["pipe", "w"], // stdout is a pipe that the child will write to
            2 => ['pipe', 'w'], // stderr
        ];

        $cmd = 'prince -o - -';
        $process = proc_open($cmd, $descriptorspec, $pipes);

        if (!is_resource($process)) {
            throw new \RuntimeException('Cannot execute Prince XML');
        }

        // $pipes now looks like this:
        // 0 => writeable handle connected to child stdin
        // 1 => readable handle connected to child stdout

        fwrite($pipes[0], $str);
        fclose($pipes[0]);

        echo stream_get_contents($pipes[1]);
        fclose($pipes[1]);

        // It is important that you close any pipes before calling
        // proc_close in order to avoid a deadlock
        proc_close($process);
    }

    /**
     * Creates a PDF file from a HTML string
     * @param  string $str HTML string
     * @return string File path of the PDF file (temporary), you must delete or move it
     */
    static public function filePDF(string $str): ?string
    {
        $source = sprintf('%s/print-%s.html', CACHE_ROOT, md5(random_bytes(16)));
        $target = str_replace('.html', '.pdf', $source);

        file_put_contents($source, $str);

        $cmd = PDF_COMMAND;

        if (!$cmd) {
            // Try to see if there's a plugin
            $in = ['source' => $source, 'target' => $target];

            if (Plugin::fireSignal('pdf.create', $in)) {
                return $target;
            }

            unset($in);

            // Try to find a local executable
            $list = ['prince', 'chromium', 'wkhtmltopdf', 'weasyprint'];

            foreach ($list as $program) {
                if (shell_exec('which ' . $program)) {
                    $cmd = $program;
                    break;
                }
            }

            // We still haven't found anything
            if (!$cmd) {
                throw new \LogicException('No PDF creation executable found. Please install or configure one.');
            }
        }

        switch ($cmd) {
            case 'prince':
                $cmd = 'prince -o %2$s %1$s';
                break;
            case 'chromium':
                $cmd = 'chromium --headless --disable-gpu --run-all-compositor-stages-before-draw --print-to-pdf-no-header --print-to-pdf=%s %s';
                break;
            case 'wkhtmltopdf':
                $cmd = 'wkhtmltopdf %1$s %2$s';
                break;
            case 'weasyprint':
                $cmd = 'weasyprint %1$s %2$s';
                break;
            default:
                break;
        }

        exec(sprintf($cmd, escapeshellarg($source), escapeshellarg($target)));

        if (!file_exists($target)) {
            throw new \RuntimeException('PDF command failed');
        }

        unlink($source);

        return $target;
    }
}

Modified src/include/lib/Garradin/Web/Render/AbstractRender.php from [c4980b1d31] to [5c2f7cfe96].

9
10
11
12
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

abstract class AbstractRender
{
	protected $current_path;
	protected $context;
	protected $link_prefix;
	protected $link_suffix;


	protected $file;

	public function __construct(?File $file)
	{
		$this->file = $file;

		if ($file) {
			$this->isRelativeTo($file);
		}
	}



	abstract public function render(?string $content = null, array $options = []): string;

	protected function resolveAttachment(string $uri) {
		$prefix = $this->current_path;
		$pos = strpos($uri, '/');

		// "Image.jpg"
		if ($pos === false) {
			return WWW_URL . $prefix . '/' . $uri;
		}
		// "bla/Image.jpg" outside of web context
		elseif ($this->context !== File::CONTEXT_WEB && $pos !== 0) {
			return WWW_URL . $this->context . '/' . $uri;
		}
		// "bla/Image.jpg" in web context or absolute link, eg. "/transactions/2442/42.jpg"
		else {
			return WWW_URL . ltrim($uri, '/');
		}
	}

	protected function resolveLink(string $uri) {
		$first = substr($uri, 0, 1);
		if ($first == '/' || $first == '!') {
			return Utils::getLocalURL($uri);
		}

		if (strpos(Utils::basename($uri), '.') === false) {
			$uri .= $this->link_suffix;
		}

		return $this->link_prefix . $uri;
	}

	public function isRelativeTo(File $file) {
		$this->current_path = Utils::dirname($file->path);
		$this->context = strtok($this->current_path, '/');
		$this->link_suffix = '';

		if ($this->context === File::CONTEXT_WEB) {
			$this->link_prefix = WWW_URL;
			$this->current_path = Utils::basename(Utils::dirname($file->path));
		}
		else {
			$this->link_prefix = $options['prefix'] ?? sprintf(ADMIN_URL . 'common/files/preview.php?p=%s/', $this->context);
			$this->link_suffix = '.skriv';
		}
	}
}







>



|






|
>
|
>
|





|
|
|

<
<
<
|
<
|
|
<
















|
|



|



|




9
10
11
12
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

abstract class AbstractRender
{
	protected $current_path;
	protected $context;
	protected $link_prefix;
	protected $link_suffix;
	protected $user_prefix;

	protected $file;

	public function __construct(?File $file = null, ?string $user_prefix = null)
	{
		$this->file = $file;

		if ($file) {
			$this->isRelativeTo($file);
		}

		$this->user_prefix = $user_prefix;
	}

	abstract public function render(?string $content = null): string;

	protected function resolveAttachment(string $uri) {
		$prefix = $this->current_path;
		$pos = strpos($uri, '/');

		// Absolute URL: treat it as absolute!
		if ($pos === 0) {
			return WWW_URL . ltrim($uri, '/');
		}





		// Handle relative URIs
		return WWW_URL . $prefix . '/' . $uri;

	}

	protected function resolveLink(string $uri) {
		$first = substr($uri, 0, 1);
		if ($first == '/' || $first == '!') {
			return Utils::getLocalURL($uri);
		}

		if (strpos(Utils::basename($uri), '.') === false) {
			$uri .= $this->link_suffix;
		}

		return $this->link_prefix . $uri;
	}

	public function isRelativeTo(File $file) {
		$this->current_path = $file->parent;
		$this->context = $file->context();
		$this->link_suffix = '';

		if ($this->context === File::CONTEXT_WEB) {
			$this->link_prefix = $this->user_prefix ?? WWW_URL;
			$this->current_path = Utils::basename(Utils::dirname($file->path));
		}
		else {
			$this->link_prefix = $this->user_prefix ?? sprintf(ADMIN_URL . 'common/files/preview.php?p=%s/', $this->context);
			$this->link_suffix = '.skriv';
		}
	}
}

Modified src/include/lib/Garradin/Web/Render/Markdown.php from [7e10d16363] to [1e59334a43].

1
2
3
4
5
6
7
8
9
10
11
12
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\Web\Render;

use Garradin\Entities\Files\File;

use Garradin\Squelette_Filtres;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\Files\Files;
use Garradin\UserTemplate\CommonModifiers;

use Parsedown;
use Parsedown_Extra;

use const Garradin\{ADMIN_URL, WWW_URL};

class Markdown
{
	static protected $parsedown;

	public function render(?File $file, ?string $content = null, array $options = []): string
	{
		if (!self::$parsedown)
		{
			self::$parsedown = new ParsedownExtra;
			self::$parsedown->setBreaksEnabled(true);
			self::$parsedown->setUrlsLinked(true);
			self::$parsedown->setSafeMode(true);
		}

		$str = $content ?? $file->fetch();

		$str = self::$parsedown->text($str);

		$str = CommonModifiers::typo($str);

		$str = preg_replace_callback(';<a href="((?!https?://|\w+:).+)">;i', function ($matches) {
			return sprintf('<a href="%s" target="_parent">', $this->resolveLink($matches[1]));
		}, $str);

		return sprintf('<div class="web-content">%s</div>', $str);
	}
}






<





<
<
<


|

<
|
<

<
<
|
|
|
|
|
<
|

|



|






1
2
3
4
5
6

7
8
9
10
11



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\Web\Render;

use Garradin\Entities\Files\File;


use Garradin\Plugin;
use Garradin\Utils;
use Garradin\Files\Files;
use Garradin\UserTemplate\CommonModifiers;




use const Garradin\{ADMIN_URL, WWW_URL};

class Markdown extends AbstractRender
{

	public function render(?string $content = null): string

	{


		$parsedown = new Parsedown($this->file, $this->user_prefix);
		$parsedown->setBreaksEnabled(true);
		$parsedown->setUrlsLinked(true);
		$parsedown->setSafeMode(true);


		$str = $content ?? $this->file->fetch();

		$str = $parsedown->text($str);

		$str = CommonModifiers::typo($str);

		$str = preg_replace_callback(';<a href="((?!https?://|\w+:|#).+)">;i', function ($matches) {
			return sprintf('<a href="%s" target="_parent">', $this->resolveLink($matches[1]));
		}, $str);

		return sprintf('<div class="web-content">%s</div>', $str);
	}
}

Modified src/include/lib/Garradin/Web/Render/Parsedown.php from [af58530681] to [8582efeb8b].

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
 * @see https://github.com/erusev/parsedown/wiki/Tutorial:-Create-Extensions
 */
class Parsedown extends Parent_Parsedown
{
	protected $skriv;
	protected $toc = [];

	function __construct(?File $file)
	{
		$this->BlockTypes['<'][] = 'SkrivExtension';
		$this->BlockTypes['['][]= 'TOC';

		# identify footnote definitions before reference definitions
		array_unshift($this->BlockTypes['['], 'Footnote');

		# identify footnote markers before before links
		array_unshift($this->InlineTypes['['], 'FootnoteMarker');

		$this->skriv = new Skriv($file);
	}

	protected function blockSkrivExtension(array $line): ?array
	{
		$line = $line['text'];

		if (strpos($line, '<<') === 0 && preg_match('/^<<<?([a-z_]+)((?:(?!>>>?).)*?)(>>>?$|$)/i', trim($line), $match)) {







|










|







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
 * @see https://github.com/erusev/parsedown/wiki/Tutorial:-Create-Extensions
 */
class Parsedown extends Parent_Parsedown
{
	protected $skriv;
	protected $toc = [];

	function __construct(?File $file, ?string $user_prefix)
	{
		$this->BlockTypes['<'][] = 'SkrivExtension';
		$this->BlockTypes['['][]= 'TOC';

		# identify footnote definitions before reference definitions
		array_unshift($this->BlockTypes['['], 'Footnote');

		# identify footnote markers before before links
		array_unshift($this->InlineTypes['['], 'FootnoteMarker');

		$this->skriv = new Skriv($file, $user_prefix);
	}

	protected function blockSkrivExtension(array $line): ?array
	{
		$line = $line['text'];

		if (strpos($line, '<<') === 0 && preg_match('/^<<<?([a-z_]+)((?:(?!>>>?).)*?)(>>>?$|$)/i', trim($line), $match)) {
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
			$block['footnotes'][$matches[1]] = $matches[2];
			return $block;
		}

		end($block['footnotes']);
		$last = key($block['footnotes']);

		if (isset($block['interrupted']))
		{
			if ($line['indent'] >= 4)
			{
				$block['footnotes'][$last] .= "\n\n" . $line['text'];

				return $block;
			}
		}
		else
		{
			$block['footnotes'][$last] .= "\n" . $line['text'];

			return $block;
		}







|

<
<
|

|
<







148
149
150
151
152
153
154
155
156


157
158
159

160
161
162
163
164
165
166
			$block['footnotes'][$matches[1]] = $matches[2];
			return $block;
		}

		end($block['footnotes']);
		$last = key($block['footnotes']);

		if (isset($block['interrupted']) && $line['indent'] >= 4)
		{


			$block['footnotes'][$last] .= "\n\n" . $line['text'];

			return $block;

		}
		else
		{
			$block['footnotes'][$last] .= "\n" . $line['text'];

			return $block;
		}

Modified src/include/lib/Garradin/Web/Render/Render.php from [a31683eb85] to [baee19544e].

1
2
3
4
5
6
7
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\Web\Render;

use Garradin\Entities\Files\File;

class Render
{

	const FORMAT_SKRIV = 'skriv';
	const FORMAT_ENCRYPTED = 'skriv/encrypted';
	const FORMAT_MARKDOWN = 'markdown';

	static public function render(string $format, File $file, string $content = null, array $options = [])
	{
		if ($format == self::FORMAT_SKRIV) {
			$r = new Skriv($file);
		}
		else if ($format == self::FORMAT_ENCRYPTED) {
			$r = new EncryptedSkriv($file);
		}
		else if ($format == self::FORMAT_MARKDOWN) {
			$r = new Markdown($file);
		}
		else {
			throw new \LogicException('Invalid format: ' . $format);
		}

		return $r->render($content, $options);
	}
}













|


|


|


|





|


1
2
3
4
5
6
7
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\Web\Render;

use Garradin\Entities\Files\File;

class Render
{

	const FORMAT_SKRIV = 'skriv';
	const FORMAT_ENCRYPTED = 'skriv/encrypted';
	const FORMAT_MARKDOWN = 'markdown';

	static public function render(string $format, File $file, string $content = null, string $link_prefix = null)
	{
		if ($format == self::FORMAT_SKRIV) {
			$r = new Skriv($file, $link_prefix);
		}
		else if ($format == self::FORMAT_ENCRYPTED) {
			$r = new EncryptedSkriv($file, $link_prefix);
		}
		else if ($format == self::FORMAT_MARKDOWN) {
			$r = new Markdown($file, $link_prefix);
		}
		else {
			throw new \LogicException('Invalid format: ' . $format);
		}

		return $r->render($content);
	}
}

Modified src/templates/acc/accounts/reconcile_assist.tpl from [80420a98b7] to [d7fc6d0db8].

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

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







|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

<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://garradin.eu/Rapprochement_assiste" 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}

Modified src/templates/acc/reports/_header.tpl from [8e4c7a5fd3] to [b23bfd50d9].

1
2
3
4
5
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} — {$title}</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>




|

|
>
>
>






>








|
|




>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<div class="year-header">

	<nav class="tabs noprint">
		<ul>
		{if isset($analytical) || $current == 'analytical_ledger'}
			<li><strong><a href="{$admin_url}acc/reports/projects.php">Projets</a></strong></li>
		{/if}
		{if $current == 'analytical_ledger'}
				<li class="current"><a href="{$admin_url}acc/reports/ledger.php?{$criterias_query}">Grand livre analytique</a></li>
		{else}
			<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>
		{/if}
		</ul>
	</nav>

	<h2>{$config.nom_asso} — {$title}</h2>
	{if isset($analytical)}
		<h3>Projet&nbsp;: {$analytical.label}</h3>
	{/if}
	{if isset($year)}
		<p>Exercice&nbsp;: {$year.label} ({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>
		{linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"}
	</p>
</div>

Modified src/templates/acc/reports/ledger.tpl from [2309772ddd] to [5c2ec98ec8].


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="Grand livre" current="acc/years"}



{include file="acc/reports/_header.tpl" current="ledger" title="Grand livre"}


<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">
		<thead>
			<tr>
				<td></td>
				<td>N° pièce</td>
				<td>Réf. ligne</td>
>
|
|
>
>
|
>









|
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{if !empty($criterias.analytical_only)}
	{include file="admin/_head.tpl" title="Grand livre analytique" current="acc/years"}
	{include file="acc/reports/_header.tpl" current="analytical_ledger" title="Grand livre analytique"}
{else}
	{include file="admin/_head.tpl" title="Grand livre" current="acc/years"}
	{include file="acc/reports/_header.tpl" current="ledger" title="Grand livre"}
{/if}

<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">
		{if !empty($criterias.analytical_only)}
			<?php $link = sprintf('%sacc/reports/trial_balance.php?analytical=%d&year=%d', $admin_url, $account->id, $account->id_year); ?>
		{else}
			<?php $link = sprintf('%sacc/reports/journal.php?id=%d&year=%d', $admin_url, $account->id, $account->id_year); ?>
		{/if}
			<a href="{$link}">{$account.code} — {$account.label}</a>
	</h2></summary>

	<table class="list">
		<thead>
			<tr>
				<td></td>
				<td>N° pièce</td>
				<td>Réf. ligne</td>

Modified src/templates/acc/reports/projects.tpl from [4800025ba6] to [c7e14dfc07].

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



66
67
68
69
70
71
72
					</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.sum_expense|raw|money}</td>
					<td class="money">{$item.sum_revenue|raw|money}</td>
					<td class="money">{$item.debit|raw|money:false}</td>
					<td class="money">{$item.credit|raw|money:false}</td>
					<td class="money">{$item.sum|raw|money:false}</td>







|
|
|
|
|
|
>
>
>







53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
					</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}&amp;year={$item.id_year}">Graphiques</a>
						| <a href="{$admin_url}acc/reports/trial_balance.php?analytical={$item.id_account}&amp;year={$item.id_year}">Balance générale</a>
						| <a href="{$admin_url}acc/reports/journal.php?analytical={$item.id_account}&amp;year={$item.id_year}">Journal général</a>
						| <a href="{$admin_url}acc/reports/ledger.php?analytical={$item.id_account}&amp;year={$item.id_year}">Grand livre</a>
						| <a href="{$admin_url}acc/reports/statement.php?analytical={$item.id_account}&amp;year={$item.id_year}">Compte de résultat</a>
						| <a href="{$admin_url}acc/reports/balance_sheet.php?analytical={$item.id_account}&amp;year={$item.id_year}">Bilan</a>
						{if $item.total && $by_year}
						| <a href="{$admin_url}acc/reports/ledger.php?analytical_only=1&amp;year={$item.id_year}">Grand livre analytique</a>
						{/if}
					</span>
					</td>
					<td class="money">{$item.sum_expense|raw|money}</td>
					<td class="money">{$item.sum_revenue|raw|money}</td>
					<td class="money">{$item.debit|raw|money:false}</td>
					<td class="money">{$item.credit|raw|money:false}</td>
					<td class="money">{$item.sum|raw|money:false}</td>

Modified src/templates/acc/transactions/details.tpl from [0098f837e0] to [b3d4a382f1].

1
2
3
4




5
6
7
8
9
10

11
12
13
14
15
16
17
{include file="admin/_head.tpl" title="Écriture n°%d"|args:$transaction.id current="acc"}

{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_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($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE) && $transaction.status & $transaction::STATUS_WAITING}
<div class="block alert">
	<form method="post" action="{$self_url}">
	{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}


<

>
>
>
>




<

>







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="Écriture n°%d"|args:$transaction.id current="acc"}


<nav class="tabs">
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
	<aside>{linkbutton href="new.php?copy=%d"|args:$transaction.id shape="plus" label="Dupliquer cette écriture"}</aside>
{/if}
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction->validated && !$tr_year->closed}
	<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>

{/if}
</nav>

{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE) && $transaction.status & $transaction::STATUS_WAITING}
<div class="block alert">
	<form method="post" action="{$self_url}">
	{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}

Modified src/templates/acc/transactions/new.tpl from [49e96a9c33] to [91b454b44d].

64
65
66
67
68
69
70

71
72
73
74
75
76
77
78
				<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}








>
|







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
				<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}
	{/if}

Modified src/templates/acc/transactions/service_user.tpl from [31416dfe77] to [51d9da9e41].

1
2
3
4

5
6
7
8
9
10
11
12
{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>


|

>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
{include file="admin/_head.tpl" title="Écritures liées à une inscription" current="acc/accounts"}

<nav class="tabs">
	{linkbutton href="!membres/fiche.php?id=%d"|args:$user_id label="Retour à la fiche membre" shape="user"}
	{linkbutton href="!services/payment.php?id=%d"|args:$service_user_id label="Nouveau règlement" shape="plus"}
</nav>

{include file="acc/reports/_journal.tpl"}

<h2 class="ruler">Solde des comptes</h2>

<table class="list">
	<thead>

Modified src/templates/acc/years/export.tpl from [f38c0c6bff] to [640274763b].

1
2
3
4
5
6
7
8
{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">
|







1
2
3
4
5
6
7
8
{include file="admin/_head.tpl" title="Export d'exercice" 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">

Modified src/templates/admin/login.tpl from [011baee06b] to [f3e8752670].

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
                        <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>







|







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
                        <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>

Modified src/templates/common/files/edit_web.tpl from [5e930c6a6c] to [c721ff089b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']}

<form method="post" action="{$self_url}">
	<p class="textEditor">
		{input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$file.parent data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$file->renderFormat()}
	</p>

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




|










1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="admin/_head.tpl" title="Édition de fichier" custom_js=['wiki_editor.js']}

<form method="post" action="{$self_url}">
	<p class="textEditor">
		{input type="textarea" name="content" cols="70" rows="30" default=$content data-preview-url="!common/files/_preview.php?f=%s"|local_url|args:$file.path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$file->renderFormat()}
	</p>

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

Modified src/templates/docs/index.tpl from [586034f95e] to [ce2d8a6b1b].

1
2
3
4
5
6
7
8
9
10
11
12
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
use Garradin\Entities\Files\File;
?>
{include file="admin/_head.tpl" title="Documents" current="docs"}

<nav class="tabs">
	<aside>
	{if $context == File::CONTEXT_DOCUMENTS}
		{linkbutton shape="search" label="Rechercher" href="search.php" target="_dialog"}
	{/if}
	{if $can_mkdir}
		{linkbutton shape="plus" label="Nouveau répertoire" target="_dialog" href="!docs/new_dir.php?p=%s"|args:$path}
	{/if}
	{if $can_upload}
		{linkbutton shape="plus" label="Nouveau fichier texte" target="_dialog" href="!docs/new_file.php?p=%s"|args:$path}
		{linkbutton shape="upload" label="Ajouter un fichier" target="_dialog" href="!common/files/upload.php?p=%s"|args:$path}
	{/if}
	</aside>
	<ul>
		<li{if $context == File::CONTEXT_DOCUMENTS} class="current"{/if}><a href="./">Documents</a></li>
		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)}
			<li{if $context == File::CONTEXT_TRANSACTION} class="current"{/if}><a href="./?p=<?=File::CONTEXT_TRANSACTION?>">Fichiers des écritures</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)}
			<li{if $context == File::CONTEXT_USER} class="current"{/if}><a href="./?p=<?=File::CONTEXT_USER?>">Fichiers des membres</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			<li{if $context == File::CONTEXT_SKELETON} class="current"{/if}><a href="./?p=<?=File::CONTEXT_SKELETON?>">Squelettes du site web</a></li>
		{/if}
	</ul>

</nav>

<nav class="breadcrumbs">
	{if count($breadcrumbs) > 1}
		{linkbutton href="?p=%s"|args:$parent_path label="Retour au répertoire parent" shape="left"}
	{/if}

{if $context == File::CONTEXT_TRANSACTION}
	{if $context_ref}
		{linkbutton href="!acc/transactions/details.php?id=%d"|args:$context_ref|local_url label="Détails de l'écriture" shape="menu"}
	{/if}
{elseif $context == File::CONTEXT_USER}
	{if $context_ref}
		{linkbutton href="!membres/fiche.php?id=%d"|args:$context_ref|local_url label="Fiche du membre" shape="user"}
	{/if}
{else}
	<ul>
	{foreach from=$breadcrumbs item="name" key="bc_path"}
		<li><a href="?p={$bc_path}">{$name}</a></li>
	{/foreach}
	</ul>
{/if}

	<aside class="quota">
		<h4><b>{$quota_left|size_in_bytes}</b> libres sur <i>{$quota_max|size_in_bytes}</i></h4>
		<span class="meter"><span style="width: {$quota_percent}%"></span></span>











|


|






|


|


|







|













|







1
2
3
4
5
6
7
8
9
10
11
12
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
use Garradin\Entities\Files\File;
?>
{include file="admin/_head.tpl" title="Documents" current="docs"}

<nav class="tabs">
	<aside>
	{if $context == File::CONTEXT_DOCUMENTS}
		{linkbutton shape="search" label="Rechercher" href="search.php" target="_dialog"}
	{/if}
	{if $can_mkdir}
		{linkbutton shape="plus" label="Nouveau répertoire" target="_dialog" href="!docs/new_dir.php?path=%s"|args:$path}
	{/if}
	{if $can_upload}
		{linkbutton shape="plus" label="Nouveau fichier texte" target="_dialog" href="!docs/new_file.php?path=%s"|args:$path}
		{linkbutton shape="upload" label="Ajouter un fichier" target="_dialog" href="!common/files/upload.php?p=%s"|args:$path}
	{/if}
	</aside>
	<ul>
		<li{if $context == File::CONTEXT_DOCUMENTS} class="current"{/if}><a href="./">Documents</a></li>
		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)}
			<li{if $context == File::CONTEXT_TRANSACTION} class="current"{/if}><a href="./?path=<?=File::CONTEXT_TRANSACTION?>">Fichiers des écritures</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)}
			<li{if $context == File::CONTEXT_USER} class="current"{/if}><a href="./?path=<?=File::CONTEXT_USER?>">Fichiers des membres</a></li>
		{/if}
		{if $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)}
			<li{if $context == File::CONTEXT_SKELETON} class="current"{/if}><a href="./?path=<?=File::CONTEXT_SKELETON?>">Squelettes du site web</a></li>
		{/if}
	</ul>

</nav>

<nav class="breadcrumbs">
	{if count($breadcrumbs) > 1}
		{linkbutton href="?path=%s"|args:$parent_path label="Retour au répertoire parent" shape="left"}
	{/if}

{if $context == File::CONTEXT_TRANSACTION}
	{if $context_ref}
		{linkbutton href="!acc/transactions/details.php?id=%d"|args:$context_ref|local_url label="Détails de l'écriture" shape="menu"}
	{/if}
{elseif $context == File::CONTEXT_USER}
	{if $context_ref}
		{linkbutton href="!membres/fiche.php?id=%d"|args:$context_ref|local_url label="Fiche du membre" shape="user"}
	{/if}
{else}
	<ul>
	{foreach from=$breadcrumbs item="name" key="bc_path"}
		<li><a href="?path={$bc_path}">{$name}</a></li>
	{/foreach}
	</ul>
{/if}

	<aside class="quota">
		<h4><b>{$quota_left|size_in_bytes}</b> libres sur <i>{$quota_max|size_in_bytes}</i></h4>
		<span class="meter"><span style="width: {$quota_percent}%"></span></span>
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
				{if $file.type == $file::TYPE_DIRECTORY}
				<tr>
					{if $can_delete}
					<td class="check">
						{input type="checkbox" name="check[]" value=$file.path}
					</td>
					{/if}
					<th><a href="?p={$file.path}">{$file.name}</a></th>
					<td></td>
					<td>Répertoire</td>
					<td></td>
					<td class="actions">
					{if $can_write && ($context == File::CONTEXT_SKELETON || $context == File::CONTEXT_DOCUMENTS)}
						{linkbutton href="!common/files/rename.php?p=%s"|args:$file.path label="Renommer" shape="minus" target="_dialog"}
					{/if}







|







94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
				{if $file.type == $file::TYPE_DIRECTORY}
				<tr>
					{if $can_delete}
					<td class="check">
						{input type="checkbox" name="check[]" value=$file.path}
					</td>
					{/if}
					<th><a href="?path={$file.path}">{$file.name}</a></th>
					<td></td>
					<td>Répertoire</td>
					<td></td>
					<td class="actions">
					{if $can_write && ($context == File::CONTEXT_SKELETON || $context == File::CONTEXT_DOCUMENTS)}
						{linkbutton href="!common/files/rename.php?p=%s"|args:$file.path label="Renommer" shape="minus" target="_dialog"}
					{/if}
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
		{include file="common/dynamic_list_head.tpl" check=false}


		{foreach from=$list->iterate() item="item"}
			<tr>
				{if $context == File::CONTEXT_TRANSACTION}
					<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$item.id}">#{$item.id}</a></td>
					<th><a href="?p={$item.path}">{$item.label}</a></th>
					<td>{$item.date|date_short}</td>
					<td>{$item.reference}</td>
					<td>{$item.year}</td>
					<td class="actions">
						{linkbutton href="!docs/?p=%s"|args:$item.path label="Fichiers" shape="menu"}
						{linkbutton href="!acc/transactions/details.php?id=%d"|args:$item.id label="Écriture" shape="search"}
					</td>
				{else}
					<td class="num"><a href="{$admin_url}membres/fiche.php?id={$item.id}">#{$item.number}</a></td>
					<th><a href="?p={$item.path}">{$item.identity}</a></th>
					<td class="actions">
						{linkbutton href="!docs/?p=%s"|args:$item.path label="Fichiers" shape="menu"}
						{linkbutton href="!membres/fiche.php?id=%d"|args:$item.id label="Fiche membre" shape="user"}
					</td>
				{/if}
			</tr>
		{/foreach}
		</tbody>
		</table>



	{/if}

	<p class="actions">
		{linkbutton href="!docs/zip.php?p=%s"|args:$path label="Télécharger ce répertoire (ZIP)" shape="download"}
	</p>

</form>
{else}
	<p class="alert block">Il n'y a aucun fichier dans ce répertoire.</p>
{/if}

{include file="admin/_foot.tpl"}







|




|




|

|








>
>



|








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
		{include file="common/dynamic_list_head.tpl" check=false}


		{foreach from=$list->iterate() item="item"}
			<tr>
				{if $context == File::CONTEXT_TRANSACTION}
					<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$item.id}">#{$item.id}</a></td>
					<th><a href="?path={$item.path}">{$item.label}</a></th>
					<td>{$item.date|date_short}</td>
					<td>{$item.reference}</td>
					<td>{$item.year}</td>
					<td class="actions">
						{linkbutton href="!docs/?path=%s"|args:$item.path label="Fichiers" shape="menu"}
						{linkbutton href="!acc/transactions/details.php?id=%d"|args:$item.id label="Écriture" shape="search"}
					</td>
				{else}
					<td class="num"><a href="{$admin_url}membres/fiche.php?id={$item.id}">#{$item.number}</a></td>
					<th><a href="?path={$item.path}">{$item.identity}</a></th>
					<td class="actions">
						{linkbutton href="!docs/?path=%s"|args:$item.path label="Fichiers" shape="menu"}
						{linkbutton href="!membres/fiche.php?id=%d"|args:$item.id label="Fiche membre" shape="user"}
					</td>
				{/if}
			</tr>
		{/foreach}
		</tbody>
		</table>

		{pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()}

	{/if}

	<p class="actions">
		{linkbutton href="!docs/zip.php?path=%s"|args:$path label="Télécharger ce répertoire (ZIP)" shape="download"}
	</p>

</form>
{else}
	<p class="alert block">Il n'y a aucun fichier dans ce répertoire.</p>
{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/services/fees/_fee_form.tpl from [60289da098] to [9d5c15a2e4].

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
				</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>







|







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
				</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://garradin.eu/Formule-calcul-activite">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>

Modified src/templates/services/payment.tpl from [10d2082879] to [c4cd0c545e].

8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24

25
26
27
28
29
		<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="reference" label="Numéro de pièce comptable" help="Numéro de facture, de note de frais, etc."}
			{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"}







>










>





8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
		<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="date" name="date" label="Date" required=1 source=$su}
			{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="reference" label="Numéro de pièce comptable" help="Numéro de facture, de note de frais, etc."}
			{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"}
		{button type="submit" name="save_and_add_payment" label="Enregistrer et ajouter un autre règlement" shape="plus"}
	</p>

</form>

{include file="admin/_foot.tpl"}

Modified src/templates/services/save.tpl from [c8f34251fe] to [b1114e7276].

111
112
113
114
115
116
117

118
119
120
121
122
123
124
{/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>








>







111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
{/if}
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{if $user_id}
			{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
			{button type="submit" name="save_and_add_payment" class="accounting" label="Enregistrer et ajouter un autre règlement" shape="plus"}
		{else}
			{button type="submit" name="next" label="Continuer" shape="right" class="main"}
		{/if}
	</p>

</form>

Modified src/templates/services/user.tpl from [d3b73b33b5] to [6397212801].

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
		Aucune inscription.
	</dd>
	{/foreach}
	<dt>Nombre d'inscriptions pour ce membre</dt>
	<dd>
		{$list->count()}
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_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"}







|
|







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
		Aucune inscription.
	</dd>
	{/foreach}
	<dt>Nombre d'inscriptions pour ce membre</dt>
	<dd>
		{$list->count()}
		{if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
			{linkbutton href="?id=%d&export=csv"|args:$user.id shape="export" label="Export CSV"}
			{linkbutton href="?id=%d&export=ods"|args:$user.id shape="export" label="Export tableur"}
		{/if}
	</dd>
</dl>

{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}

Modified src/templates/web/config.tpl from [cbe2f68864] to [6540f9cc0f].

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
		<p class="actions">
			Pour les squelettes sélectionnés&nbsp;:
			<input type="submit" name="reset" value="Réinitialiser" onclick="return confirm('Effacer toute modification locale et restaurer les squelettes d\'installation ?');" />
			{csrf_field key="squelettes"}
		</p>

		<p>
			{linkbutton href="!docs/?p=skel" label="Gérer les fichiers de squelettes" shape="folder"}
		</p>
	</fieldset>
	</form>

{/if}

{include file="admin/_foot.tpl"}







|







118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
		<p class="actions">
			Pour les squelettes sélectionnés&nbsp;:
			<input type="submit" name="reset" value="Réinitialiser" onclick="return confirm('Effacer toute modification locale et restaurer les squelettes d\'installation ?');" />
			{csrf_field key="squelettes"}
		</p>

		<p>
			{linkbutton href="!docs/?path=skel" label="Gérer les fichiers de squelettes" shape="folder"}
		</p>
	</fieldset>
	</form>

{/if}

{include file="admin/_foot.tpl"}

Modified src/templates/web/index.tpl from [387d7766db] to [38a56e5fed].

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
	</table>
{/if}

{if count($pages)}
	<h2 class="ruler">Pages</h2>
	<p>
		{if !$order_date}
			{linkbutton shape="down" label="Trier par date" href="?p=%s"|args:$current_path}
		{else}
			{linkbutton shape="up" label="Trier par titre" href="?p=%s&order_title"|args:$current_path}
		{/if}
	</p>
	<table class="list">
		<tbody>
			{foreach from=$pages item="p"}
			<tr>
				<th>{$p.title}</th>







|

|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
	</table>
{/if}

{if count($pages)}
	<h2 class="ruler">Pages</h2>
	<p>
		{if !$order_date}
			{linkbutton shape="down" label="Trier par date" href="?p=%s&order_date"|args:$current_path}
		{else}
			{linkbutton shape="up" label="Trier par titre" href="?p=%s"|args:$current_path}
		{/if}
	</p>
	<table class="list">
		<tbody>
			{foreach from=$pages item="p"}
			<tr>
				<th>{$p.title}</th>

Modified src/www/admin/acc/reports/_inc.php from [923715a8c5] to [c7c07f3287].

31
32
33
34
35
36
37




38
39
40
41
42
43

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







>
>
>
>






>

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
		throw new UserException('Exercice inconnu.');
	}

	$criterias['year'] = $year->id();
	$tpl->assign('year', $year);
	$tpl->assign('close_date', $year->closed ? $year->end_date : time());
}

if (qg('analytical_only')) {
	$criterias['analytical_only'] = true;
}

if (!count($criterias))
{
	throw new UserException('Critère de rapport inconnu.');
}

$tpl->assign('criterias', $criterias);
$tpl->assign('criterias_query', http_build_query($criterias));

Modified src/www/admin/acc/transactions/new.php from [d8191985b4] to [c93d48adea].

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


















24
25
26
27
28
29
30
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Files\File;

use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$chart = $current_year->chart();
$accounts = $chart->accounts();

$transaction = new Transaction;
$lines = [[], []];
$amount = 0;
$payoff_for = qg('payoff_for') ?: f('payoff_for');



















$date = new \DateTime;

if ($session->get('acc_last_date')) {
	$date = \DateTime::createFromFormat('!d/m/Y', $session->get('acc_last_date'));
}







>

















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







1
2
3
4
5
6
7
8
9
10
11
12
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;

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Files\File;
use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$chart = $current_year->chart();
$accounts = $chart->accounts();

$transaction = new Transaction;
$lines = [[], []];
$amount = 0;
$payoff_for = qg('payoff_for') ?: f('payoff_for');
$types_accounts = null;

// Duplicate transaction
if (qg('copy')) {
	$old = Transactions::get((int)qg('copy'));
	$transaction = $old->duplicate($current_year);
	$lines = $transaction->getLinesWithAccounts();
	$payoff_for = null;
	$amount = $transaction->getLinesCreditSum();
	$types_accounts = $transaction->getTypesAccounts();
	$transaction->resetLines();

	foreach ($lines as $k => &$line) {
		$line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_name)];
	}

	unset($line);
}

$date = new \DateTime;

if ($session->get('acc_last_date')) {
	$date = \DateTime::createFromFormat('!d/m/Y', $session->get('acc_last_date'));
}

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
		Utils::redirect(Utils::getSelfURI(false) . '?ok=' . $transaction->id());
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$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');







|








105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
		Utils::redirect(Utils::getSelfURI(false) . '?ok=' . $transaction->id());
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact('transaction', 'payoff_for', 'amount', 'lines', 'types_accounts'));
$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');

Modified src/www/admin/acc/transactions/service_user.php from [fb13cfe6c3] to [6aaa90de39].

9
10
11
12
13
14
15

16
17
$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ);

$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');







>


9
10
11
12
13
14
15
16
17
18
$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ);

$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->assign('service_user_id', qg('id'));

$tpl->display('acc/transactions/service_user.tpl');

Modified src/www/admin/common/files/_preview.php from [35c5f4da82] to [a08e5e24c0].

1
2
3
4
5
6
7
8
9
10
11
12





13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Web\Render\Render;
use Garradin\Web\Web;

require_once __DIR__ . '/../../_inc.php';

$page = null;






if ($path = qg('f')) {
	$file = Files::get($path);

	if (!$file || !$file->checkReadAccess($session)) {
		throw new UserException('Vous n\'avez pas le droit de lire ce fichier.');
	}












>
>
>
>
>







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\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Web\Render\Render;
use Garradin\Web\Web;

require_once __DIR__ . '/../../_inc.php';

$page = null;
$content = f('content');

if (null == $content) {
	throw new UserException('Aucun contenu à prévisualiser');
}

if ($path = qg('f')) {
	$file = Files::get($path);

	if (!$file || !$file->checkReadAccess($session)) {
		throw new UserException('Vous n\'avez pas le droit de lire ce fichier.');
	}
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

	$file = $page->file();
}
else {
	throw new UserException('Fichier inconnu');
}

$prefix = $page ? 'web/page.php?uri=' : 'common/files/_preview.php?p=' . File::CONTEXT_DOCUMENTS . '/';

$content = Render::render(f('format'), $file, f('content'), ['prefix' => ADMIN_URL . $prefix]);

$tpl->assign(compact('file', 'content'));

$tpl->assign('custom_css', ['!web/css.php']);

$tpl->display('common/files/_preview.tpl');







|

|






32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

	$file = $page->file();
}
else {
	throw new UserException('Fichier inconnu');
}

$prefix = $page ? 'web/page.php?uri=' : 'common/files/_preview.php?p=';

$content = Render::render(f('format'), $file, f('content'), ADMIN_URL . $prefix);

$tpl->assign(compact('file', 'content'));

$tpl->assign('custom_css', ['!web/css.php']);

$tpl->display('common/files/_preview.tpl');

Modified src/www/admin/common/files/preview.php from [a192851b45] to [5a776333ed].

13
14
15
16
17
18
19
20
21
22
23
24
25
26
}

if (!$file->checkReadAccess($session)) {
	throw new UserException('Vous n\'avez pas le droit de lire ce fichier.');
}

try {
	$tpl->assign('content', $file->render());
	$tpl->assign('file', $file);
	$tpl->display('common/files/_preview.tpl');
}
catch (\LogicException $e) {
	$file->serve($session);
}







|






13
14
15
16
17
18
19
20
21
22
23
24
25
26
}

if (!$file->checkReadAccess($session)) {
	throw new UserException('Vous n\'avez pas le droit de lire ce fichier.');
}

try {
	$tpl->assign('content', $file->render('common/files/_preview.php?p='));
	$tpl->assign('file', $file);
	$tpl->display('common/files/_preview.tpl');
}
catch (\LogicException $e) {
	$file->serve($session);
}

Modified src/www/admin/docs/action.php from [a63ecf9df6] to [1987800963].

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
	}

	unset($file);

	foreach ($check as $file) {
		$file->delete();
	}
}, $csrf_key, '!docs/?p=' . $parent);

$form->runIf(f('move') && f('select'), function () use ($check, $session) {
	foreach ($check as &$file) {
		$file = Files::get($file);

		if (!$file || !$file->checkWriteAccess($session) || $file->context() != File::CONTEXT_DOCUMENTS) {
			throw new UserException('Impossible de déplacer un fichier car vous n\'avez pas le droit de le modifier');
		}
	}

	$target = f('select');
	unset($file);

	foreach ($check as $file) {
		$file->move($target);
	}
}, $csrf_key, '!docs/?p=' . $parent);

$count = count($check);

$extra = compact('parent', 'action', 'check');
$tpl->assign(compact('csrf_key', 'extra', 'action', 'count'));

if ($action == 'delete') {







|
















|







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
	}

	unset($file);

	foreach ($check as $file) {
		$file->delete();
	}
}, $csrf_key, '!docs/?path=' . $parent);

$form->runIf(f('move') && f('select'), function () use ($check, $session) {
	foreach ($check as &$file) {
		$file = Files::get($file);

		if (!$file || !$file->checkWriteAccess($session) || $file->context() != File::CONTEXT_DOCUMENTS) {
			throw new UserException('Impossible de déplacer un fichier car vous n\'avez pas le droit de le modifier');
		}
	}

	$target = f('select');
	unset($file);

	foreach ($check as $file) {
		$file->move($target);
	}
}, $csrf_key, '!docs/?path=' . $parent);

$count = count($check);

$extra = compact('parent', 'action', 'check');
$tpl->assign(compact('csrf_key', 'extra', 'action', 'count'));

if ($action == 'delete') {

Modified src/www/admin/docs/index.php from [8becda408f] to [7fe6a0182d].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Files\Transactions;
use Garradin\Files\Users;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$path = trim(qg('p')) ?: File::CONTEXT_DOCUMENTS;

$context = Files::getContext($path);
$context_ref = Files::getContextRef($path);
$list = null;

// Specific lists for some contexts
if ($context == File::CONTEXT_TRANSACTION) {











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Files\Transactions;
use Garradin\Files\Users;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$path = trim(qg('path')) ?: File::CONTEXT_DOCUMENTS;

$context = Files::getContext($path);
$context_ref = Files::getContextRef($path);
$list = null;

// Specific lists for some contexts
if ($context == File::CONTEXT_TRANSACTION) {

Modified src/www/admin/docs/new_dir.php from [e00667d877] to [d2c27619fc].

1
2
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\Files\Files;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$parent = trim(qg('p'));

if (!File::checkCreateAccess(File::CONTEXT_DOCUMENTS, $session)) {
	throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.');
}

$csrf_key = 'create_dir';

$form->runIf('create', function () use ($parent) {
	$name = trim(f('name'));
	File::validatePath($parent . '/' . $name);
	File::createDirectory($parent, $name);
}, $csrf_key, '!docs/?p=' . $parent);

$tpl->assign(compact('csrf_key'));

$tpl->display('docs/new_dir.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\Files\Files;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$parent = trim(qg('path'));

if (!File::checkCreateAccess(File::CONTEXT_DOCUMENTS, $session)) {
	throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.');
}

$csrf_key = 'create_dir';

$form->runIf('create', function () use ($parent) {
	$name = trim(f('name'));
	File::validatePath($parent . '/' . $name);
	File::createDirectory($parent, $name);
}, $csrf_key, '!docs/?path=' . $parent);

$tpl->assign(compact('csrf_key'));

$tpl->display('docs/new_dir.tpl');

Modified src/www/admin/docs/new_file.php from [ad63719729] to [b548528de7].

1
2
3
4
5
6
7
8
9
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\Files\Files;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$parent = trim(qg('p'));

if (!File::checkCreateAccess(File::CONTEXT_DOCUMENTS, $session)) {
	throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.');
}

$csrf_key = 'create_file';

$form->runIf('create', function () use ($parent) {
	$name = trim(f('name'));

	if (!strpos($name, '.')) {
		$name .= '.skriv';
	}

	File::validatePath($parent . '/' . $name);
	$name = File::filterName($name);

	$file = File::createAndStore($parent, $name, null, '');
}, $csrf_key, '!docs/?p=' . $parent);

$tpl->assign(compact('csrf_key'));

$tpl->display('docs/new_file.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
<?php

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$parent = trim(qg('path'));

if (!File::checkCreateAccess(File::CONTEXT_DOCUMENTS, $session)) {
	throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.');
}

$csrf_key = 'create_file';

$form->runIf('create', function () use ($parent) {
	$name = trim(f('name'));

	if (!strpos($name, '.')) {
		$name .= '.skriv';
	}

	File::validatePath($parent . '/' . $name);
	$name = File::filterName($name);

	$file = File::createAndStore($parent, $name, null, '');
}, $csrf_key, '!docs/?path=' . $parent);

$tpl->assign(compact('csrf_key'));

$tpl->display('docs/new_file.tpl');

Modified src/www/admin/docs/zip.php from [a9aa6c6682] to [21577262c1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Files\Transactions;
use Garradin\Files\Users;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$path = trim(qg('p')) ?: File::CONTEXT_DOCUMENTS;

$name = preg_replace('/[^\p{L}_-]+/i', '_', $path);
$name = sprintf('%s - Fichiers - %s.zip', Config::getInstance()->get('nom_asso'), $name);
header('Content-type: application/zip');
header(sprintf('Content-Disposition: attachment; filename="%s"', $name));

Files::zip($path, $session);











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

use Garradin\Files\Files;
use Garradin\Files\Transactions;
use Garradin\Files\Users;
use Garradin\Entities\Files\File;

require_once __DIR__ . '/_inc.php';

$path = trim(qg('path')) ?: File::CONTEXT_DOCUMENTS;

$name = preg_replace('/[^\p{L}_-]+/i', '_', $path);
$name = sprintf('%s - Fichiers - %s.zip', Config::getInstance()->get('nom_asso'), $name);
header('Content-type: application/zip');
header(sprintf('Content-Disposition: attachment; filename="%s"', $name));

Files::zip($path, $session);

Modified src/www/admin/index.php from [bf52e33fdb] to [025ac1c529].

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

$homepage = Config::getInstance()->get('admin_homepage');

$banner = null;
Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner);

if ($homepage && ($file = Files::get($homepage))) {
	$homepage = $file->render(['prefix' => ADMIN_URL . 'common/files/preview.php?p=' . File::CONTEXT_DOCUMENTS . '/']);
}
else {
	$homepage = null;
}

$tpl->assign(compact('homepage', 'banner'));








|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

$homepage = Config::getInstance()->get('admin_homepage');

$banner = null;
Plugin::fireSignal('accueil.banniere', ['user' => $user, 'session' => $session], $banner);

if ($homepage && ($file = Files::get($homepage))) {
	$homepage = $file->render(ADMIN_URL . 'common/files/preview.php?p=' . File::CONTEXT_DOCUMENTS . '/');
}
else {
	$homepage = null;
}

$tpl->assign(compact('homepage', 'banner'));

Modified src/www/admin/membres/modifier.php from [171580f332] to [cbd131a783].

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (!$membre)
{
    throw new UserException("Ce membre n'existe pas.");
}

// Ne pas modifier le membre courant, on risque de se tirer une balle dans le pied
if ($membre->id == $user->id) {
    throw new UserException("Vous ne pouvez pas modifier votre propre profil, la modification doit être faite par un autre membre.");
}

$champs = $config->get('champs_membres');

// Protection contre la modification des admins par des membres moins puissants
$membre_cat = Categories::get($membre->id_category);








|







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (!$membre)
{
    throw new UserException("Ce membre n'existe pas.");
}

// Ne pas modifier le membre courant, on risque de se tirer une balle dans le pied
if ($membre->id == $user->id) {
    throw new UserException("Vous ne pouvez pas modifier votre propre profil, la modification doit être faite par un autre membre, pour éviter de vous empêcher de vous reconnecter.\nUtilisez la page 'Mes infos personnelles' pour modifier vos informations.");
}

$champs = $config->get('champs_membres');

// Protection contre la modification des admins par des membres moins puissants
$membre_cat = Categories::get($membre->id_category);

Modified src/www/admin/services/payment.php from [abbfbc0faf] to [6351db0e7d].

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29




30



31
32
33
34
35
36
37
38
	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');







|







>
>
>
>
|
>
>
>








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
	throw new UserException("Cette inscription n'existe pas");
}

$user_name = (new Membres)->getNom($su->id_user);

$csrf_key = 'service_pay';

$form->runIf(f('save') || f('save_and_add_payment'), function () use ($su, $session) {
	$su->addPayment($session->getUser()->id);

	if ($su->paid != (bool) f('paid')) {
		$su->paid = (bool) f('paid');
		$su->save();
	}

	if (f('save_and_add_payment')) {
		$url = ADMIN_URL . 'services/payment.php?id=' . $su->id;
	}
	else {
		$url = ADMIN_URL . 'services/user.php?id=' . $su->id_user;
	}

	Utils::redirect($url);
}, $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/save.php from [0acb3f484c] to [ebe0bb9e7c].

47
48
49
50
51
52
53
54
55
56




57



58
59
60
61
62
63
64
65
66
67
68
69
	Utils::redirect(Utils::getSelfURI(['user' => $user_id, 'past_services' => $current_only]));
}

$has_past_services = count($grouped_services) != $count_all;

$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', 'current_only', 'has_past_services'));

$tpl->display('services/save.tpl');







|


>
>
>
>
|
>
>
>












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
	Utils::redirect(Utils::getSelfURI(['user' => $user_id, 'past_services' => $current_only]));
}

$has_past_services = count($grouped_services) != $count_all;

$csrf_key = 'service_save';

$form->runIf(f('save') || f('save_and_add_payment'), function () use ($session) {
	$su = Service_User::saveFromForm($session->getUser()->id);

	if (f('save_and_add_payment')) {
		$url = ADMIN_URL . 'services/payment.php?id=' . $su->id;
	}
	else {
		$url = ADMIN_URL . 'services/user.php?id=' . $su->id_user;
	}

	Utils::redirect($url);
}, $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', 'current_only', 'has_past_services'));

$tpl->display('services/save.tpl');

Modified src/www/admin/static/print.css from [5d6f8ad8c8] to [6937e4b226].

1
2
3
4




5
6
7
8
9
10
11
@page {
    size: A4 landscape;
    margin: 0;
}





body {
    background: #fff;
    padding: 0;
    margin: 1cm;
    font-size: 10pt;
}




>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@page {
    size: A4 landscape;
    margin: 0;
}

html {
    height: auto;
}

body {
    background: #fff;
    padding: 0;
    margin: 1cm;
    font-size: 10pt;
}
97
98
99
100
101
102
103








    text-decoration: none;
}

/* Don't repeat the table footer on every printed page */
table tfoot{
    display:table-row-group;
}















>
>
>
>
>
>
>
>
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
    text-decoration: none;
}

/* Don't repeat the table footer on every printed page */
table tfoot{
    display:table-row-group;
}

details summary::after {
    display: none;
}

.ruler::after, .ruler::before {
    display: none;
}

Modified src/www/admin/static/scripts/global.js from [b78bac285f] to [0da4b43a86].

423
424
425
426
427
428
429

430
431
432
433
434
435
436
		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.dispatchEvent(new Event('submit'));
				this.form.submit();
			};
		}







>







423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
		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'))
				{
					this.selectedIndex = 0;
					return !window.alert("Aucune ligne sélectionnée !");
				}

				this.form.dispatchEvent(new Event('submit'));
				this.form.submit();
			};
		}

Modified src/www/admin/web/index.php from [030354055c] to [fbf5316a30].

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
}
else {
	foreach (Web::sync() as $error) {
		$form->addError($error);
	}
}

$order_date = qg('order_title') === null;

$categories = Web::listCategories($cat ? $cat->path : '');
$pages = Web::listPages($cat ? $cat->path : '', $order_date);
$title = $cat ? sprintf('Gestion du site web : %s', $cat->title) : 'Gestion du site web';
$type_page = Page::TYPE_PAGE;
$type_category = Page::TYPE_CATEGORY;
$breadcrumbs = $cat ? $cat->getBreadcrumbs() : [];

$parent = $cat ? $cat->parent : null;

$tpl->assign(compact('categories', 'pages', 'title', 'current_path', 'parent', 'type_page', 'type_category', 'order_date', 'breadcrumbs', 'cat'));

$tpl->display('web/index.tpl');







|













19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
}
else {
	foreach (Web::sync() as $error) {
		$form->addError($error);
	}
}

$order_date = qg('order_date') !== null;

$categories = Web::listCategories($cat ? $cat->path : '');
$pages = Web::listPages($cat ? $cat->path : '', $order_date);
$title = $cat ? sprintf('Gestion du site web : %s', $cat->title) : 'Gestion du site web';
$type_page = Page::TYPE_PAGE;
$type_category = Page::TYPE_CATEGORY;
$breadcrumbs = $cat ? $cat->getBreadcrumbs() : [];

$parent = $cat ? $cat->parent : null;

$tpl->assign(compact('categories', 'pages', 'title', 'current_path', 'parent', 'type_page', 'type_category', 'order_date', 'breadcrumbs', 'cat'));

$tpl->display('web/index.tpl');

Modified src/www/admin/web/page.php from [da153abd6b] to [7684223213].

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$membres = new Membres;

$tpl->assign('breadcrumbs', $page->getBreadcrumbs());

$images = $page->getImageGallery(true);
$files = $page->getAttachmentsGallery(true);

$content = $page->render(['prefix' => ADMIN_URL . 'web/page.php?p=']);

$type_page = Page::TYPE_PAGE;
$type_category = Page::TYPE_CATEGORY;

$tpl->assign(compact('page', 'images', 'files', 'content', 'type_page', 'type_category'));

$tpl->assign('custom_js', ['wiki_gallery.js']);
$tpl->assign('custom_css', ['wiki.css', '!web/css.php']);

$tpl->display('web/page.tpl');







|










22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$membres = new Membres;

$tpl->assign('breadcrumbs', $page->getBreadcrumbs());

$images = $page->getImageGallery(true);
$files = $page->getAttachmentsGallery(true);

$content = $page->render(ADMIN_URL . 'web/page.php?p=');

$type_page = Page::TYPE_PAGE;
$type_category = Page::TYPE_CATEGORY;

$tpl->assign(compact('page', 'images', 'files', 'content', 'type_page', 'type_category'));

$tpl->assign('custom_js', ['wiki_gallery.js']);
$tpl->assign('custom_css', ['wiki.css', '!web/css.php']);

$tpl->display('web/page.tpl');

Added src/www/skel-dist/robots.txt version [9d03a7f081].









>
>
>
>
1
2
3
4
User-agent: *
Disallow: /admin/

Sitemap: {{$root_url}}sitemap.xml

Added src/www/skel-dist/sitemap.xml version [923be64ae6].

































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{#pages limit=1 order="modified DESC"}}
	<url>
		<loc>{{$root_url}}</loc>
		<lastmod>{{$modified|atom_date}}</lastmod>
	</url>
{{/pages}}

{{#pages limit=10000}}
	<url>
		<loc>{{$url}}</loc>
		<lastmod>{{$modified|atom_date}}</lastmod>
	</url>
{{/pages}}
</urlset>