Overview
Comment:Merge dev branch
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | invoice_module
Files: files | file ages | folders
SHA3-256: e292d08439f78c6626cf5543d9b91a0062e6a9e744d562eb6be98ac64895eef4
User & Date: alinaar on 2023-02-19 18:00:26
Other Links: branch diff | manifest | tags
Context
2023-02-19
18:09
Invoice module: zero quantity allowed check-in: 44c55767f6 user: alinaar tags: invoice_module
18:00
Merge dev branch check-in: e292d08439 user: alinaar tags: invoice_module
15:01
Invoice module: quotation's full edition implemented check-in: 33da505a8c user: alinaar tags: invoice_module
03:13
Improve code editor: add save without closing, use dark theme, remove PNG icons check-in: 2b83852c0a user: bohwaz tags: dev
Changes

Name change from debian/config.debian.php to build/debian/config.debian.php.

Modified build/debian/makedeb.sh from [0add980db8] to [ed98f1ed48].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# Ripped from fossil makdedeb.sh

DEB_REV=${1-1} # .deb package build/revision number.
PACKAGE_DEBNAME=paheko
THISDIR=${PWD}

DEB_ARCH_NAME=all

PACKAGE_VERSION=`cat ../src/VERSION`

[ ! -f ../src/paheko-${PACKAGE_VERSION}.tar.gz ] && (cd ../src; make release)

tar xzvf ../src/paheko-${PACKAGE_VERSION}.tar.gz -C /tmp

SRCDIR="/tmp/paheko-${PACKAGE_VERSION}"

test -e ${SRCDIR} || {
    echo "This script must be run from a BUILT copy of the source tree."
    exit 1
}









|

|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# Ripped from fossil makdedeb.sh

DEB_REV=${1-1} # .deb package build/revision number.
PACKAGE_DEBNAME=paheko
THISDIR=${PWD}

DEB_ARCH_NAME=all

PACKAGE_VERSION=`cat ../../src/VERSION`

[ ! -f ../../src/paheko-${PACKAGE_VERSION}.tar.gz ] && (cd ../../src; make release)

tar xzvf ../../src/paheko-${PACKAGE_VERSION}.tar.gz -C /tmp

SRCDIR="/tmp/paheko-${PACKAGE_VERSION}"

test -e ${SRCDIR} || {
    echo "This script must be run from a BUILT copy of the source tree."
    exit 1
}
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
echo "Creating .deb package [${DEBFILE}]..."

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

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

true && {
    cat <<EOF > DEBIAN/postinst







|







64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
echo "Creating .deb package [${DEBFILE}]..."

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

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

true && {
    cat <<EOF > DEBIAN/postinst
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
}

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

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







|







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
}

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

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

Name change from debian/manpage.txt to build/debian/manpage.txt.

Name change from debian/paheko to build/debian/paheko.

Name change from debian/paheko.desktop to build/debian/paheko.desktop.

Name change from debian/paheko.menu to build/debian/paheko.menu.

Name change from debian/paheko.png to build/debian/paheko.png.

cannot compute difference between binary files

Added build/windows/Makefile version [151de88204].



























































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
.PHONY := php installer clean publish
PHP_ARCHIVE := https://windows.php.net/downloads/releases/php-8.0.27-nts-Win32-vs16-x64.zip

php.zip:
	wget ${PHP_ARCHIVE} -O php.zip

php: php.zip
	mkdir -p install_dir/php
	unzip -o php.zip -d install_dir/php > /dev/null

	# Remove unused files
	@cd install_dir/php && rm -rf \
		phpdbg.exe \
		php8phpdbg.dll \
		php8embed.lib \
		php-cgi.exe \
		php.ini-* \
		dev \
		phar* \
		nghttp2.dll \
		libpq.dll \
		libenchant

	# Remove unused extensions
	@cd install_dir/php/ext && rm -f \
		php_bz2.dll \
		php_com_dotnet.dll \
		php_curl.dll \
		php_dba.dll \
		php_dl_test.dll \
		php_enchant.dll \
		php_exif.dll \
		php_ffi.dll \
		php_ftp.dll \
		php_gmp.dll \
		php_imap.dll \
		php_ldap.dll \
		php_mysqli.dll \
		php_oci8_19.dll \
		php_odbc.dll \
		php_opcache.dll \
		php_pdo_firebird.dll \
		php_pdo_mysql.dll \
		php_pdo_oci.dll \
		php_pdo_odbc.dll \
		php_pdo_pgsql.dll \
		php_pdo_sqlite.dll \
		php_pgsql.dll \
		php_shmop.dll \
		php_snmp.dll \
		php_soap.dll \
		php_sysvshm.dll \
		php_xsl.dll \
		php_zend_test.dll

	du -hs install_dir/php

installer: clean php
	$(eval VERSION=$(shell cat ../../src/VERSION))
	mkdir -p install_dir
	cp ../../src/paheko-${VERSION}.tar.gz install_dir/
	cd install_dir && tar xzf paheko-${VERSION}.tar.gz && mv paheko-${VERSION} paheko
	cp config.local.php install_dir/paheko/
	cp php.ini install_dir/php
	cp launch.bat install_dir
	cp paheko.ico install_dir
	rm -f install_dir/paheko-${VERSION}.tar.gz
	makensis -V3 -DVERSION=${VERSION} paheko.nsis

clean:
	rm -rf install_dir

publish:
	$(eval VERSION=$(shell cat ../../src/VERSION))
	fossil uv ls | grep '^paheko-.*\.exe' | xargs fossil uv rm
	fossil uv add paheko-${VERSION}.exe
	fossil uv sync

Added build/windows/README.md version [5c56cc2aa0].











>
>
>
>
>
1
2
3
4
5
# Paheko Windows build

## Requirements

NSIS: `apt install nsis`

Added build/windows/config.local.php version [4f250a82d1].

























































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

namespace Garradin;

if (!empty(getenv('LOCALAPPDATA'))) {
	// Store data in user AppData directory
	define('Garradin\DATA_ROOT', trim(getenv('LOCALAPPDATA'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Paheko');
}

// Store secret key in user directory
if (!defined('Garradin\SECRET_KEY')) {
	if (file_exists(DATA_ROOT . '/key')) {
		define('Garradin\SECRET_KEY', trim(file_get_contents(DATA_ROOT . '/key')));
	}
	else {
		define('Garradin\SECRET_KEY', base64_encode(random_bytes(16)));
		file_put_contents(DATA_ROOT . '/key', SECRET_KEY);
	}
}

// Always log in as admin user
const LOCAL_LOGIN = -1;

// Disable PDF export
const PDF_COMMAND = null;

// Disable e-mails as Windows is not able to send e-mails
const DISABLE_EMAIL = true;

Added build/windows/launch.bat version [4318d01c86].































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@start "" http://127.0.0.1:8082/

@echo =================================================
@echo.
@echo  Demarrage du serveur PHP de Paheko.
@echo.
@echo  Paheko est disponible a l'adresse suivante :
@echo  http://127.0.0.1:8082/
@echo.
@echo  Fermer cette fenetre pour arreter le serveur.
@echo.
@echo =================================================
@echo.

php\php.exe -S 127.0.0.1:8082 -t paheko/www paheko/www/_route.php 2> NUL

Added build/windows/paheko.ico version [969b2f968c].

cannot compute difference between binary files

Added build/windows/paheko.nsis version [8c0f2b68e1].































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
# From https://www.conjur.org/blog/building-a-windows-installer-from-a-linux-ci-pipeline/
!define APP_NAME "Paheko"
!define COMP_NAME "Paheko.cloud"
#!define WEB_SITE "https://paheko.cloud/"
#!define VERSION "0.0.0.1"
!define COPYRIGHT "Paheko"
!define DESCRIPTION "Gestion d'association simple et efficace"
!define INSTALLER_NAME "paheko-${VERSION}.exe"
!define MAIN_APP_EXE "launch.bat"
!define ICON "paheko.ico"
#!define BANNER "[CHANGEME Installer Banner Filename .bmp]"
#!define LICENSE_TXT "[CHANGEME License Text Document]"

!define INSTALL_DIR "$PROGRAMFILES64\${APP_NAME}"
!define INSTALL_TYPE "SetShellVarContext all"
!define REG_ROOT "HKLM"
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}"
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
!define REG_START_MENU "Start Menu Folder"

var SM_Folder

######################################################################

VIProductVersion  "${VERSION}.0"
VIAddVersionKey "ProductName"  "${APP_NAME}"
VIAddVersionKey "CompanyName"  "${COMP_NAME}"
VIAddVersionKey "LegalCopyright"  "${COPYRIGHT}"
VIAddVersionKey "FileDescription"  "${DESCRIPTION}"
VIAddVersionKey "FileVersion"  "${VERSION}"

######################################################################

SetCompressor /SOLID Lzma
Name "${APP_NAME}"
Caption "${APP_NAME}"
OutFile "${INSTALLER_NAME}"
BrandingText "${APP_NAME}"
#InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
InstallDir "${INSTALL_DIR}"

######################################################################

!define MUI_ICON "${ICON}"
!define MUI_UNICON "${ICON}"
Icon "${ICON}"

!ifdef BANNER
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}"
!endif

######################################################################

!include "MUI2.nsh"

!define MUI_ABORTWARNING
!define MUI_UNABORTWARNING

!insertmacro MUI_PAGE_WELCOME

!ifdef LICENSE_TXT
!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}"
!endif

!insertmacro MUI_PAGE_DIRECTORY

!ifdef REG_START_MENU
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "${APP_NAME}"
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}"
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
!endif

!insertmacro MUI_PAGE_INSTFILES

!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_UNPAGE_CONFIRM

!insertmacro MUI_UNPAGE_INSTFILES

!insertmacro MUI_UNPAGE_FINISH

!insertmacro MUI_LANGUAGE "French"

######################################################################

Section -MainProgram
	${INSTALL_TYPE}

	SetOverwrite ifnewer
	SetOutPath "$INSTDIR"
	File /r "install_dir\\"

SectionEnd

######################################################################

Section -Icons_Reg
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\uninstall.exe"

!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
CreateDirectory "$SMPROGRAMS\$SM_Folder"
CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico"
CreateShortCut "$SMPROGRAMS\$SM_Folder\Desinstaller ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"

!ifdef WEB_SITE
WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}"
CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url"
!endif
!insertmacro MUI_STARTMENU_WRITE_END
!endif

!ifndef REG_START_MENU
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" "" "$INSTDIR\paheko.ico"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\Desinstaller ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"

!ifdef WEB_SITE
WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url"
!endif
!endif

WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "DisplayName" "${APP_NAME}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "DisplayIcon" "$INSTDIR\paheko.ico"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "DisplayVersion" "${VERSION}"
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "Publisher" "${COMP_NAME}"

!ifdef WEB_SITE
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}"  "URLInfoAbout" "${WEB_SITE}"
!endif
SectionEnd

######################################################################

Section Uninstall
${INSTALL_TYPE}

RmDir /r "$INSTDIR"

!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\$SM_Folder\Desinstaller ${APP_NAME}.lnk"
!ifdef WEB_SITE
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk"
!endif
Delete "$DESKTOP\${APP_NAME}.lnk"

RmDir "$SMPROGRAMS\$SM_Folder"
!endif

!ifndef REG_START_MENU
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk"
Delete "$SMPROGRAMS\${APP_NAME}\Desinstaller ${APP_NAME}.lnk"
!ifdef WEB_SITE
Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME} Website.lnk"
!endif
Delete "$DESKTOP\${APP_NAME}.lnk"

RmDir "$SMPROGRAMS\${APP_NAME}"
!endif

DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
SectionEnd

Added build/windows/php.ini version [799dda8484].































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
[PHP]
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions =
disable_classes =
zend.enable_gc = On
zend.exception_ignore_args = Off
zend.exception_string_param_max_len = 15
expose_php = On
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = On
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 8M
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
doc_root =
user_dir =
extension_dir = "ext"
enable_dl = Off
file_uploads = On
upload_max_filesize = 2M
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60

extension=fileinfo
extension=gd
extension=gettext
extension=intl
extension=mbstring
extension=openssl
extension=sodium
extension=sqlite3
extension=tidy

[CLI Server]
cli_server.color = On

[mail function]
SMTP = localhost
smtp_port = 25
mail.add_x_header = Off

[bcmath]
bcmath.scale = 0

[Session]
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.cookie_samesite =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.sid_length = 26
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5

[Assertion]
zend.assertions = -1

[Tidy]
tidy.clean_output = Off

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

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

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

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


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














































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Added doc/icon.png version [13439ec707].

cannot compute difference between binary files

Deleted doc/img/garradin.svg version [7d26df8f0d].

cannot compute difference between binary files

Deleted doc/img/garradin_gecko.svg version [e94b930821].

cannot compute difference between binary files

Modified doc/index.md from [db1e5dddfa] to [cd33124bbd].

1
2























3
4
5
6
7
8

9
10
11
12
13
14
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
# Paheko, le gestionnaire d'association libre et simple
























<nav id="gnav">

* [Guides d'installation](/wiki/?name=Installation)
* [Documentation](/wiki/?name=Documentation)
* <a href="https://paheko.cloud/" target="_blank">Essayer gratuitement</a>
* [Entraide](/wiki/?name=Entraide)


<ul id="download">
	<li><a href="$ROOT/wiki/?name=Changelog">Nouveautés</a></li>
	<li><a href="$ROOT/uvlist">Anciennes versions</a></li>
</ul>

</nav>

<p id="give"><a href="https://kd2.org/soutien.html" target="_blank">Soutenir Paheko en effectuant un don :-)</a></p>

<h3><a href="https://paheko.cloud/garradin-devient-paheko" target="_blank">Garradin devient Paheko !</a></h3>

<p>Garradin.eu est devenu Paheko.cloud !</p>

<form method="GET" action="$ROOT/wiki" onsubmit="var t = this.querySelector('[type=radio]:checked'); this.querySelector('[name=s]').name=t.dataset.name; this.action=t.dataset.action; this.target=t.dataset.target;">
<fieldset class="searchForm searchFormWiki">
	<legend>Rechercher</legend>
	<input type="search" name="s" size="40" value="" />
	<label><input type="radio" name="t" value="" data-name="s" data-action="/paheko/wiki" data-target="" checked="checked" /> Chercher dans la documentation technique</label>
	<label><input type="radio" name="t" value="1" data-action="https://paheko.cloud/search" data-name="search" data-target="_blank" /> Chercher dans l'aide utilisateur</label>
	<input type="submit" value="Rechercher" />
</fieldset>
</form>

<script type="text/javascript">
document.head.innerHTML += `<style type="text/css">







































#give {
	text-align: center;
	padding: 1em;
}

#give a {
	display: inline-block;
	padding: .5em;
	padding-left: 70px;
	border-radius: .5em;
	font-size: 1.5em;
	background: #ffc url("https://kd2.org/soutien/coins.png") no-repeat .5em .5em;
	border: 2px solid #990;
}

#gnav ul {
	display: flex;
	padding: 0;
	margin: 1em;
	margin-bottom: 1em;
	font-size: 1.2em;
	list-style: none;
	justify-content: center;
	align-items: center;
}

#gnav li {
	margin: 0;
|

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




<

>

|








<
<
<
<












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


|

















|







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

30
31
32
33
34
35
36
37
38
39
40
41




42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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
# La gestion d'association libre et simple

<div id="prez">
	<figure>
		<img src="./selfhost2.png" alt="Illustration d'une personne aidant une autre à installer Paheko sur un ordinateur" />
	</figure>

### Paheko — la gestion d'association simple</h3>

**Paheko** <small>(anciennement appelé *Garradin*)</small> est un logiciel de gestion d'association, libre, simple et efficace. Son but est de&nbsp;:

* **réduire le temps** passé sur les tâches administratives&nbsp;;
* re-**donner de l'autonomie aux adhérent⋅e⋅s** dans la gestion de leurs données&nbsp;;
* **simplifier la gestion** de l'association, pour inciter à participer à la gestion de l'association&nbsp;;
* intégrer les outils habituels, afin de réduire le nombre de logiciels à gérer.
	
Pour en savoir plus : [voir les principales fonctionnalités](#features).
</div>

<div id="warn">
	<p><strong>Attention&nbsp;: ce site est dédié au logiciel libre Paheko.</strong><br />
	Son installation, sur un serveur ou sur un ordinateur personnel, nécessite quelques compétences techniques.</p>
	<p>Si votre association n'a pas ces compétences, nous recommandons l'utilisation de notre service d'hébergement&nbsp;:<br /><strong class="cloud"><a href="https://paheko.cloud/" target="_blank"><img src="./icon.png" alt="" /> Paheko.cloud</a></strong> 
		<small>(<strong>Essai gratuit</strong>, puis contribution à prix libre, à partir de 5&nbsp;€ par an)</small>
</div>
<nav id="gnav">

* [Guides d'installation](/wiki/?name=Installation)
* [Documentation](/wiki/?name=Documentation)

* [Entraide](/wiki/?name=Entraide)
* <a href="https://paheko.cloud/" target="_blank">Essayer gratuitement sur &nbsp; <b><img src="./icon.png" alt="" /> Paheko.cloud</b></a>

<ul id="news">
	<li><a href="$ROOT/wiki/?name=Changelog">Nouveautés</a></li>
	<li><a href="$ROOT/uvlist">Anciennes versions</a></li>
</ul>

</nav>

<p id="give"><a href="https://kd2.org/soutien.html" target="_blank">Soutenir Paheko en effectuant un don :-)</a></p>





<form method="GET" action="$ROOT/wiki" onsubmit="var t = this.querySelector('[type=radio]:checked'); this.querySelector('[name=s]').name=t.dataset.name; this.action=t.dataset.action; this.target=t.dataset.target;">
<fieldset class="searchForm searchFormWiki">
	<legend>Rechercher</legend>
	<input type="search" name="s" size="40" value="" />
	<label><input type="radio" name="t" value="" data-name="s" data-action="/paheko/wiki" data-target="" checked="checked" /> Chercher dans la documentation technique</label>
	<label><input type="radio" name="t" value="1" data-action="https://paheko.cloud/search" data-name="search" data-target="_blank" /> Chercher dans l'aide utilisateur</label>
	<input type="submit" value="Rechercher" />
</fieldset>
</form>

<script type="text/javascript">
document.head.innerHTML += `<style type="text/css">
#prez {
}

#warn {
	border: 2px solid #990;
	padding: .5em;
	border-radius: .5em;
	background: #ffd;
	margin: 1em 0;
	clear: both;
}

#warn .cloud {
	font-size: 1.2em;
}

#prez figure {
	float: right;
}

.markdown img {
	display: inline-block;
	max-width: unset;
	vertical-align: middle;
	box-shadow: none;
	margin: 0;
}

/*
#info {
	text-align: center;
	margin: 1em auto;
	background: #ddd;
	padding: .5em;
	border-radius: .5em;
	max-width: 40em;
}
*/

#give {
	text-align: center;
	margin: 1em;
}

#give a {
	display: inline-block;
	padding: .5em;
	padding-left: 70px;
	border-radius: .5em;
	font-size: 1.5em;
	background: #ffc url("https://kd2.org/soutien/coins.png") no-repeat .5em .5em;
	border: 2px solid #990;
}

#gnav ul {
	display: flex;
	padding: 0;
	margin: 1em;
	margin-bottom: 1em;
	font-size: 1.1em;
	list-style: none;
	justify-content: center;
	align-items: center;
}

#gnav li {
	margin: 0;
86
87
88
89
90
91
92
93
94
95
96





97






























98

















99



100
101
102
103
104
105
106
107
}

#gnav li a:hover {
	text-decoration: underline;
	opacity: 0.7;
}

#download li {
	font-size: 1em;
}






#download li a {






























	border-color: #060;

















	background: #dfd;



}

.searchForm {
	border: 1px solid #ccc;
	border-radius: 5px;
	padding: .5em;
	margin: 1em auto;
	max-width: 30em;







|



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








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
}

#gnav li a:hover {
	text-decoration: underline;
	opacity: 0.7;
}

#news li {
	font-size: 1em;
}

#news li a {
	border-color: #060;
	background: #dfd;
}

#download > h2 {
	text-align: center;
}

#download nav {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
}

#download div, #download div h3 a, #download div h4 a {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
}

#download div {
	margin: 0 20px;
}

#download div h3 a, #download div h4 a {
	background: #eef;
	border: 2px solid #ccf;
	padding: 5px;
	border-radius: 8px;
}

#download a:hover {
	background: #fee;
	border-color: #fcc;
}

#download img {
	height: 124px;
	box-shadow: none;
	padding: 5px;
	margin: 0;
}

#download p, #download h3, #download h4 {
	margin: 0;
	margin-bottom: 8px;
	text-align: center;
}

#download p em {
	color: #333;
	background: #ddd;
	padding: 2px;
	border-radius: 4px;
	display: inline-block;
}

.searchForm {
	border: 1px solid #ccc;
	border-radius: 5px;
	padding: .5em;
	margin: 1em auto;
	max-width: 30em;
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
		if (a < b) return false
	}
	return false
}

fetch('/paheko/juvlist?'+(+(new Date))).then((r) => {
	r.json().then((list) => {
		let last;
		let selected;

		list.forEach((file) => {
			var v = file.name.match(/^paheko-(.*)\.tar\./);

			if (!v || v[1].match(/-(alpha|rc|beta)/)) {
				return;
			}




			if (!last || isNewerVersion(last, v[1])) {


				last = v[1];

				selected = file;

			}
		});

		let days = ((+new Date)/1000 - selected.mtime) / 3600 / 24;

		if (days < 31) {
			time = Math.ceil(days) + ' jours';
		}
		else if (days >= 31) {
			time = Math.round(days / 30.5) + ' mois';
		}



		let deb = selected.name.replace(/\.tar\..*$/, '.deb');





		document.querySelector('#download').innerHTML = `<li class="last"><strong>Dernière version : ${last}</strong><br />





			<em>il y a ${time}</em></li>


			<li><a href="$ROOT/uv/${selected.name}"><strong>Télécharger</strong> <small>(.tar.gz)</small></a></li>





			<li><a href="$ROOT/uv/${deb}">Debian</a></li>` + document.querySelector('#download').innerHTML;













	});
});
</script>

## C'est quoi ?

<a href="$ROOT/raw/7bb068963b9f6301b27b81fe925caae9e86a229b?m=image/png" target="_blank" style="float: right; margin: 1em;"><img src="/paheko/raw/7bb068963b9f6301b27b81fe925caae9e86a229b?m=image/png" alt="Liste des membres" width="400" /></a>


Paheko (anciennement appelé <em>Garradin</em>) est un logiciel de gestion d'association (loi 1901 / ASBL / etc.). Son but est de permettre :















*  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







|



|





>
>
>
|
>
>
|
>
|
>












>
>
|
>
>

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




|



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







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
		if (a < b) return false
	}
	return false
}

fetch('/paheko/juvlist?'+(+(new Date))).then((r) => {
	r.json().then((list) => {
		let last = {};
		let selected;

		list.forEach((file) => {
			var v = file.name.match(/^paheko-(\d+\.\d+\.\d+)\.(deb|exe|tar\.gz)$/);

			if (!v || v[1].match(/-(alpha|rc|beta)/)) {
				return;
			}

			file.type = v[2];
			file.version = v[1];
			file.human_size = (Math.round((file.size / 1024 / 1024) * 10) / 10 + ' Mo').replace(/\./, ',');

			if (!last.hasOwnProperty(file.type) || isNewerVersion(last[file.type].version, file.version)) {
				last[file.type] = file;

				if (file.type == 'tar.gz') {
					selected = file;
				}
			}
		});

		let days = ((+new Date)/1000 - selected.mtime) / 3600 / 24;

		if (days < 31) {
			time = Math.ceil(days) + ' jours';
		}
		else if (days >= 31) {
			time = Math.round(days / 30.5) + ' mois';
		}

		document.querySelector('#news').innerHTML = `<li class="last"><strong>Dernière version : ${last['tar.gz'].version}</strong></li>
			<li class="last"><em>il y a ${time}</em></li>` + document.querySelector('#news').innerHTML;

		document.querySelector('#news').insertAdjacentHTML('afterend', `<div id="download">
			<h2>Télécharger&nbsp;:</h2>

			<nav>
			<div>

				<h3><a href="$ROOT/uv/${last['tar.gz'].name}"><img src="data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMjAiIGhlaWdodD0iOCIgcng9IjIiIHJ5PSIyIiAvPgogIDxyZWN0IHg9IjIiIHk9IjE0IiB3aWR0aD0iMjAiIGhlaWdodD0iOCIgcng9IjIiIHJ5PSIyIiAvPgogIDxsaW5lIHgxPSI2IiB5MT0iNiIgeDI9IjYuMDEiIHkyPSI2IiAvPgogIDxsaW5lIHgxPSI2IiB5MT0iMTgiIHgyPSI2LjAxIiB5Mj0iMTgiIC8+Cjwvc3ZnPgo=" alt="" /><span>Serveur</span></a></h3>
				<p>pour auto-hébergement<br />
					<em>(.tar.gz, ${last['tar.gz'].human_size})</em><br />
					<small><a href="$ROOT/wiki/?name=Installation">Guides d'installation</a></small>
				</p>

			</div>
			<div>

				<h4><a href="$ROOT/uv/${last['deb'].name}"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGoAAAB8AgMAAAD8wM2CAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAAlwSFlzAAAF3gAABd4BiRluJgAAAAxQTFRFAAAAR3BMAAAAAAAAoOHXxQAAAAR0Uk5T/QBQozL7B+YAAAPySURBVEjHndc9aBxHFADgd7u64hTv4eYgxRVulijYpQURpDgIKVwILtbN7GAt9jquEl/gItUOl5gUhgjUbNKkMBhhkDE2rhOY2LhwYVvNgiEu0qhOwKiISSAzszPz3q7mhIlKfczuzJv3swds4d8Y3tEu/fNyb4FxCQBl2HJFkIRtpI0spGYIVkM2qS0JWV5bFLKiNnwhsZm1KmAja2nA5ta6AZMnmCXonWDJCdY5btxZ9D8tDtkpgD5fYKVUFzQKWqT+P9ARD5yh80UB1b/jkPHkh1346dTPwXXJ8gG8Wn6NF0hsuXMaXkXDsMVnYBifCxucgXMwxIsn9hz69z6G00F7LP+8MYXdoD355qvP2KNx0KTOaP4XqOAcz6XSZmnAbrrMT4/bt6Y8+WLbyrCSiH2it7IqMEFpzqtnTcsiaPNfGf9bZ3cSsN/fvinFa0zQxtm/3GPbE0wmas/VXnpqnxDIszslKyqO1YmWwWRtMzFlkbZNxEzGlSnRbtvGEbu0V5d20rZZx7eLqG2yh22mZdyFWPiNehNud5nf6BgPWmEddps2cinLfcl7k0DqN25YhiXpe5ozgS1Aus04y7GtzN1mnBVoM9ebnI2wVRWxrF/gbI7tL49tP3Amsb9P4IO6Pp3B+wNMgKSuCWscditMnGhuDuFtRufLzLzdWhbdZKThH5jma23S6TAyKIbmRNZEvzG/VkwkrOVLPTrrVkzkrY0/wvGl4n6e2myYMjLQlkxgrI0OBi0rvcnbeHR1z0vmdq3BrGzZwFkWj9gim9CwKDtPLI++a1vqrOg8JsbV2dFmnd8a66jNG8Z18/UmW+sOTOOqDaKdxrrbZB3E6411V6Q3VR73qSX7P0piA2pnISoGaF1qfwCs+LisTSNid1W1vOfuIVOdCu9BxHrouvublGyOObGhO0V8aE2oiOJDv9adIn5q7ZrOLX/x67rikwvGcujrHPFJuK4rfvWGse39Uh8jQlMVX101dst2B/fQW2aOf27syBbuqrdCveCisdQWT+KNv6nYZWJj33wu1BEgJvwMri2r8H2Zv4vaWBcNO4W1bWJ+o89syFNl37tKrl+cuQDtoOV2L8LZNWWf2qqzQSuc5Wg8bc4CJnb9M/1Z6LqjRjwKbxvKXPpdtd/RZJ+Z/3zQ+5z6dBSHYxA+v9T1X8dvzBcqLlfId7LEj2ihY30RS4R+YO8wapxaZmolL1sfoMY2zHXzfstS+0h9R9N+ecy469fX45KajkXuZ8CmQQ5v1bfNA6ltC2fHpn5ntob3cUTmynTNxqzOCdGYfw8PmWtpWzqPGjP1yS8u58WHm73WfN/3xfnQ7jv426l8h99V/wEOtd7r5KQmrQAAAABJRU5ErkJggg==" alt="" /><span>Linux</span></a></h4>
				<p>hors-ligne, pour ordinateur<br />
					<em>(.deb, ${last['deb'].human_size})</em><br />
					<small><a href="$ROOT/wiki/?name=Fonctionnement+hors-ligne">Guide d'installation</a></small>
				</p>

			</div>
			<div>

				<h4><a href="$ROOT/uv/${last['exe'].name}"><img src="data:image/svg+xml;base64,PHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0yMiAyLTEwLjggMS42djhsMTAuOC0uMXptLTExLjggMTAuNS04LjItLjF2Ni44bDguMSAxLjF6bS04LjItNy43djYuOGg4LjF2LTcuOXptOS4xIDcuN3Y3LjlsMTAuOSAxLjZ2LTkuNHoiLz48L3N2Zz4=" alt="" /><span>Windows</span></a></h4>
				<p>hors-ligne, pour ordinateur<br />
					<em>(.exe, ${last['exe'].human_size})</em><br />
					<small><a href="$ROOT/wiki/?name=Installation/Windows">Guide d'installation</a></small>
				</p>

			</div>
			</nav>

		</div>`);
	});
});
</script>

<a name="features"></a>

<a href="$ROOT/raw/7bb068963b9f6301b27b81fe925caae9e86a229b?m=image/png" target="_blank" style="float: right; margin: 1em;"><img src="/paheko/raw/7bb068963b9f6301b27b81fe925caae9e86a229b?m=image/png" alt="Liste des membres" width="400" /></a>

## C'est quoi ?

* **100% libre :** placé sous la licence [AGPL v3](https://www.gnu.org/licenses/why-affero-gpl.fr.html).
* Gestion des **adhérent⋅e⋅s** : fiches de membre personnalisables, recherches personnalisées…
* Gestion des **cotisations** et **activités** : suivi des adhérent⋅e⋅s à jour, des paiements en attente, **rappels automatiques** de cotisation par e-mail, etc.
* Envoi de **newsletters** avec suivi des adresses e-mail invalides
* **Comptabilité** puissante (à double entrée), **simple à utiliser par les débutant⋅e⋅s** : recettes, dépenses, suivi des dettes et créances, bilan et compte de résultat annuel, **comptabilité analytique**, export PDF, etc.
* Stockage et **partage** de **documents** : édition collaborative, synchronisation des fichiers sur un ordinateur, etc.
* Gestion du **site web** de l'association
* Comptabilisation du **temps bénévole** et sa **valorisation**
* Gestion de la **caisse informatisée** d'un atelier ou d'une boutique
* **Conforme au RGPD** : export des données de l'adhérent⋅e, désabonnement des e-mails, chiffrement des mots de passe…

## Dans quels buts ?

Le but est de permettre :

*  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

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

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

## Fonctionnement général

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

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














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

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



## Messages d'erreur

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

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

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

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

	PRAGMA schema.integrity_check;

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































Deleted doc/manuel/compta/Détails écriture.svg version [a9f0b60686].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   data-stacking-context="true"
   aria-owns="pjzavj1"
   width="356.51562"
   height="220.45375"
   viewBox="172 434 356.51562 220.45375"
   version="1.1"
   id="svg9072"
   sodipodi:docname="Modification dune écriture Screenshot.svg"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
  <metadata
     id="metadata9078">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs
     id="defs9076">
    <mask
       id="mask-for-svg21">
      <rect
         style="fill:#ffffff"
         id="rect9063"
         y="0"
         x="0"
         height="0"
         width="0" />
    </mask>
    <mask
       id="mask-for-svg11">
      <rect
         style="fill:#ffffff"
         id="rect9057"
         y="0"
         x="0"
         height="0"
         width="0" />
    </mask>
    <mask
       id="mask-for-menu11">
      <rect
         style="fill:#ffffff"
         id="rect8217"
         y="0"
         x="0"
         height="960"
         width="170" />
    </mask>
    <mask
       id="mask-for-f_notes1">
      <rect
         width="341.1875"
         height="102.78125"
         x="205"
         y="1270.625"
         id="rect9009"
         style="fill:#ffffff" />
    </mask>
    <mask
       id="mask-for-f_file1">
      <rect
         width="336.78125"
         height="35.375"
         x="213"
         y="1412.2969"
         id="rect9023"
         style="fill:#ffffff" />
    </mask>
  </defs>
  <sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1917"
     inkscape:window-height="1058"
     id="namedview9074"
     showgrid="false"
     inkscape:zoom="0.97925311"
     inkscape:cx="600.04959"
     inkscape:cy="29.820457"
     inkscape:window-x="0"
     inkscape:window-y="0"
     inkscape:window-maximized="0"
     inkscape:current-layer="svg9072"
     fit-margin-top="0"
     fit-margin-left="0"
     fit-margin-right="0"
     fit-margin-bottom="0" />
  <!-- Generated by dom-to-svg from https://kidideux.garradin.eu/admin/acc/transactions/edit.php?id=475 -->
  <style
     id="style8184">
        @font-face { font-family: gicon; src: url(&quot;https://kidideux.garradin.eu/admin/static/font/garradin.eot?2021#iefix&quot;) format(&quot;embedded-opentype&quot;), url(&quot;https://kidideux.garradin.eu/admin/static/font/garradin.woff?2021&quot;) format(&quot;woff&quot;), url(&quot;https://kidideux.garradin.eu/admin/static/font/garradin.woff2?2021&quot;) format(&quot;woff2&quot;), url(&quot;https://kidideux.garradin.eu/admin/static/font/garradin.ttf?2021&quot;) format(&quot;truetype&quot;), url(&quot;https://kidideux.garradin.eu/admin/static/font/garradin.svg?2021#garradin&quot;) format(&quot;svg&quot;); font-weight: normal; font-style: normal; }
    </style>
  <text
     dominant-baseline="text-after-edge"
     font-size="16px"
     font-stretch="100%"
     font-style="normal"
     font-variant="normal"
     font-weight="700"
     direction="ltr"
     letter-spacing="normal"
     text-decoration="none solid rgb(0, 0, 0)"
     unicode-bidi="normal"
     word-spacing="0px"
     user-select="auto"
     id="text8427"
     style="color:#000000;font-style:normal;font-variant:normal;font-weight:700;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#000000;text-rendering:auto"
     x="-18.171875"
     y="-44.337818">
    <tspan
       xml:space="preserve"
       x="170.82812"
       y="451.78717"
       lengthAdjust="spacingAndGlyphs"
       id="tspan8425">Date</tspan>  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="12px"
     font-stretch="100%"
     font-style="normal"
     font-variant="normal"
     font-weight="400"
     direction="ltr"
     letter-spacing="normal"
     text-decoration="none solid rgb(153, 0, 0)"
     unicode-bidi="normal"
     word-spacing="0px"
     user-select="auto"
     id="text8438"
     style="color:#990000;font-style:normal;font-variant:normal;font-weight:400;font-size:12px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#990000;text-rendering:auto"
     x="-18.171875"
     y="-44.337818">
    <tspan
       xml:space="preserve"
       x="209.82812"
       y="445.45905"
       lengthAdjust="spacingAndGlyphs"
       id="tspan8436">(obligatoire)</tspan>  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15;stroke-width:1px"
     id="rect8442"
     ry="4"
     rx="4"
     y="454.97467"
     x="186.82812"
     height="36.78125"
     width="117.1875" />
  <text
     style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:central;text-anchor:start;fill:#000000;text-rendering:auto"
     id="text8452"
     y="475.76532"
     x="196.42813"
     xml:space="preserve"
     user-select="auto"
     word-spacing="0px"
     unicode-bidi="normal"
     text-decoration="none solid rgb(0, 0, 0)"
     letter-spacing="normal"
     direction="ltr"
     font-weight="400"
     font-variant="normal"
     font-style="normal"
     font-stretch="100%"
     font-size="16px"
     dominant-baseline="central">15/12/2020</text>
  <text
     dominant-baseline="text-after-edge"
     font-size="16px"
     font-stretch="100%"
     font-style="normal"
     font-variant="normal"
     font-weight="700"
     direction="ltr"
     letter-spacing="normal"
     text-decoration="none solid rgb(0, 0, 0)"
     unicode-bidi="normal"
     word-spacing="0px"
     user-select="auto"
     id="text8475"
     style="color:#000000;font-style:normal;font-variant:normal;font-weight:700;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#000000;text-rendering:auto"
     x="-18.171875"
     y="-44.337818">
    <tspan
       xml:space="preserve"
       x="170.82812"
       y="519.27155"
       lengthAdjust="spacingAndGlyphs"
       id="tspan8473">Libellé</tspan>  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="12px"
     font-stretch="100%"
     font-style="normal"
     font-variant="normal"
     font-weight="400"
     direction="ltr"
     letter-spacing="normal"
     text-decoration="none solid rgb(153, 0, 0)"
     unicode-bidi="normal"
     word-spacing="0px"
     user-select="auto"
     id="text8486"
     style="color:#990000;font-style:normal;font-variant:normal;font-weight:400;font-size:12px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#990000;text-rendering:auto"
     x="-18.171875"
     y="-44.337818">
    <tspan
       xml:space="preserve"
       x="226.82812"
       y="512.94342"
       lengthAdjust="spacingAndGlyphs"
       id="tspan8484">(obligatoire)</tspan>  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15;stroke-width:1px"
     id="rect8490"
     ry="4"
     rx="4"
     y="522.45905"
     x="186.82812"
     height="36.78125"
     width="341.1875" />
  <text
     style="color:#000000;font-style:normal;font-variant:normal;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:central;text-anchor:start;fill:#000000;text-rendering:auto"
     id="text8500"
     y="542.27087"
     x="196.42813"
     xml:space="preserve"
     user-select="auto"
     word-spacing="0px"
     unicode-bidi="normal"
     text-decoration="none solid rgb(0, 0, 0)"
     letter-spacing="normal"
     direction="ltr"
     font-weight="400"
     font-variant="normal"
     font-style="normal"
     font-stretch="100%"
     font-size="16px"
     dominant-baseline="central">Dons reçus par chèque</text>
  <text
     dominant-baseline="text-after-edge"
     font-size="16px"
     font-stretch="100%"
     font-style="normal"
     font-variant="normal"
     font-weight="700"
     direction="ltr"
     letter-spacing="normal"
     text-decoration="none solid rgb(0, 0, 0)"
     unicode-bidi="normal"
     word-spacing="0px"
     user-select="auto"
     id="text8512"
     style="color:#000000;font-style:normal;font-variant:normal;font-weight:700;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#000000;text-rendering:auto"
     x="-18.171875"
     y="-44.337818">
    <tspan
       xml:space="preserve"
       x="170.82812"
       y="586.75592"
       lengthAdjust="spacingAndGlyphs"
       id="tspan8510">Numéro de pièce comptable</tspan>  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="12px"
     font-stretch="100%"
     font-style="italic"
     font-variant="normal"
     font-weight="400"
     direction="ltr"
     letter-spacing="normal"
     text-decoration="none solid rgb(153, 153, 153)"
     unicode-bidi="normal"
     word-spacing="0px"
     user-select="auto"
     id="text8521"
     style="color:#999999;font-style:italic;font-variant:normal;font-weight:400;font-size:12px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#999999;text-rendering:auto"
     x="-18.171875"
     y="-44.337818">
    <tspan
       xml:space="preserve"
       x="385.82812"
       y="580.4278"
       lengthAdjust="spacingAndGlyphs"
       id="tspan8519">(facultatif)</tspan>  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15;stroke-width:1px"
     id="rect8525"
     ry="4"
     rx="4"
     y="589.94342"
     x="186.82812"
     height="36.78125"
     width="341.1875" />
  <text
     style="color:#666666;font-style:normal;font-variant:normal;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:0px;writing-mode:lr-tb;direction:ltr;dominant-baseline:text-after-edge;text-anchor:start;fill:#666666;text-rendering:auto"
     id="text8545"
     user-select="auto"
     word-spacing="0px"
     unicode-bidi="normal"
     text-decoration="none solid rgb(102, 102, 102)"
     letter-spacing="normal"
     direction="ltr"
     font-weight="400"
     font-variant="normal"
     font-style="normal"
     font-stretch="100%"
     font-size="16px"
     dominant-baseline="text-after-edge"
     x="-18.171875"
     y="-44.337818">
    <tspan
       id="tspan8543"
       lengthAdjust="spacingAndGlyphs"
       y="655.09967"
       x="186.82812"
       xml:space="preserve">Numéro de facture, de note de frais, etc.</tspan>  </text>
</svg>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































































































































































































































































































































































































































































































































































































Deleted doc/manuel/compta/Lignes écriture chèques.svg version [a8247584d9].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   aria-owns="blxijiogc1"
   width="1142.8077"
   height="346.28101"
   viewBox="183 360 1142.8077 346.28101"
   version="1.1"
   id="svg547"
   sodipodi:docname="Modification dune écriture Screenshot.svg"
   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
  <metadata
     id="metadata551">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <sodipodi:namedview
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="1469"
     inkscape:window-height="787"
     id="namedview549"
     showgrid="false"
     inkscape:zoom="1.1005291"
     inkscape:cx="474.99106"
     inkscape:cy="176.6607"
     inkscape:window-x="0"
     inkscape:window-y="0"
     inkscape:window-maximized="0"
     inkscape:current-layer="svg547"
     fit-margin-top="0"
     fit-margin-left="0"
     fit-margin-right="0"
     fit-margin-bottom="0" />
  <defs
     id="defs2">
    <mask
       id="mask-for-menu11">
      <path
         style="fill:#ffffff"
         inkscape:connector-curvature="0"
         id="path6"
         d="M 0,0 H 170 V 960 H 0 Z" />
    </mask>
    <mask
       id="mask-for-f_notes1">
      <path
         d="M 205,879.625 H 546.188 V 982.406 H 205 Z"
         id="path531"
         inkscape:connector-curvature="0"
         style="fill:#ffffff" />
    </mask>
    <mask
       id="mask-for-f_file1">
      <path
         d="m 213,1021.297 h 336.781 v 35.375 H 213 Z"
         id="path536"
         inkscape:connector-curvature="0"
         style="fill:#ffffff" />
    </mask>
  </defs>
  <path
     stroke="rgba(217, 134, 40, 0.5)"
     d="m 183,360 h 1142.8077 v 29 H 183 Z"
     id="path56"
     inkscape:connector-curvature="0"
     style="fill:#cccccc;stroke-width:0.82281148" />
  <text
     dominant-baseline="text-after-edge"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     id="td1"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       x="192"
       y="386"
       lengthAdjust="spacingAndGlyphs"
       id="tspan58">Compte</tspan>
  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     id="td2"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     x="-166"
     y="-6.625">
    <tspan
       x="711"
       y="386"
       lengthAdjust="spacingAndGlyphs"
       id="tspan61">Débit</tspan>
  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     id="td3"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     x="-206"
     y="-6.625">
    <tspan
       x="809"
       y="386"
       lengthAdjust="spacingAndGlyphs"
       id="tspan64">Crédit</tspan>
  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     id="td4"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     x="-246"
     y="-6.625">
    <tspan
       x="907"
       y="386"
       lengthAdjust="spacingAndGlyphs"
       id="tspan67">Réf. ligne</tspan>
  </text>
  <text
     dominant-baseline="text-after-edge"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     id="td5"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     x="-286"
     y="-6.625">
    <tspan
       x="1024"
       y="386"
       lengthAdjust="spacingAndGlyphs"
       id="tspan70">Libellé ligne</tspan>
  </text>
  <rect
     width="354.96899"
     height="36.780998"
     x="192"
     y="394"
     rx="4"
     ry="4"
     id="rect80"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     id="rect82"
     ry="3.2"
     rx="3.2"
     y="395"
     x="193"
     height="34.780998"
     width="134.78101"
     style="fill:#cccccc" />
  <text
     style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="span18"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="19.2"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan84"
       lengthAdjust="spacingAndGlyphs"
       y="422.89099"
       x="202.59399">𝍢</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text89"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan87"
       lengthAdjust="spacingAndGlyphs"
       y="420.89099"
       x="228.188">Sélectionner</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text94"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan92"
       lengthAdjust="spacingAndGlyphs"
       y="423.39099"
       x="337.375">5112 — Chèques à encaisser</tspan>
  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15"
     id="rect99"
     ry="4"
     rx="4"
     y="394"
     x="711"
     height="36.780998"
     width="89.188004" />
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect106"
     ry="4"
     rx="4"
     y="394"
     x="809"
     height="36.780998"
     width="89.188004" />
  <rect
     width="103.188"
     height="36.780998"
     x="907"
     y="394"
     rx="4"
     ry="4"
     id="rect113"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="173.188"
     height="36.780998"
     x="1024"
     y="394"
     rx="4"
     ry="4"
     id="rect119"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="354.96899"
     height="36.780998"
     x="192"
     y="440"
     rx="4"
     ry="4"
     id="rect145"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     id="rect147"
     ry="3.2"
     rx="3.2"
     y="441"
     x="193"
     height="34.780998"
     width="134.78101"
     style="fill:#cccccc" />
  <text
     style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="span20"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="19.2"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan149"
       lengthAdjust="spacingAndGlyphs"
       y="468.89099"
       x="202.59399">𝍢</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text154"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan152"
       lengthAdjust="spacingAndGlyphs"
       y="466.89099"
       x="228.188">Sélectionner</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text159"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan157"
       lengthAdjust="spacingAndGlyphs"
       y="469.39099"
       x="337.375">5112 — Chèques à encaisser</tspan>
  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15"
     id="rect165"
     ry="4"
     rx="4"
     y="440"
     x="711"
     height="36.780998"
     width="89.188004" />
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect173"
     ry="4"
     rx="4"
     y="440"
     x="809"
     height="36.780998"
     width="89.188004" />
  <rect
     width="103.188"
     height="36.780998"
     x="907"
     y="440"
     rx="4"
     ry="4"
     id="rect181"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="173.188"
     height="36.780998"
     x="1024"
     y="440"
     rx="4"
     ry="4"
     id="rect188"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="354.96899"
     height="36.780998"
     x="192"
     y="486"
     rx="4"
     ry="4"
     id="rect216"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     id="rect218"
     ry="3.2"
     rx="3.2"
     y="487"
     x="193"
     height="34.780998"
     width="134.78101"
     style="fill:#cccccc" />
  <text
     style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="span22"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="19.2"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan220"
       lengthAdjust="spacingAndGlyphs"
       y="514.89099"
       x="202.59399">𝍢</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text225"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan223"
       lengthAdjust="spacingAndGlyphs"
       y="512.89099"
       x="228.188">Sélectionner</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text230"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan228"
       lengthAdjust="spacingAndGlyphs"
       y="515.39099"
       x="337.375">5112 — Chèques à encaisser</tspan>
  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15"
     id="rect236"
     ry="4"
     rx="4"
     y="486"
     x="711"
     height="36.780998"
     width="89.188004" />
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect244"
     ry="4"
     rx="4"
     y="486"
     x="809"
     height="36.780998"
     width="89.188004" />
  <rect
     width="103.188"
     height="36.780998"
     x="907"
     y="486"
     rx="4"
     ry="4"
     id="rect252"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="173.188"
     height="36.780998"
     x="1024"
     y="486"
     rx="4"
     ry="4"
     id="rect259"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="354.96899"
     height="36.780998"
     x="192"
     y="532"
     rx="4"
     ry="4"
     id="rect287"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     id="rect289"
     ry="3.2"
     rx="3.2"
     y="533"
     x="193"
     height="34.780998"
     width="134.78101"
     style="fill:#cccccc" />
  <text
     style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="span24"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="19.2"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan291"
       lengthAdjust="spacingAndGlyphs"
       y="560.89099"
       x="202.59399">𝍢</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text296"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan294"
       lengthAdjust="spacingAndGlyphs"
       y="558.89099"
       x="228.188">Sélectionner</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text301"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan299"
       lengthAdjust="spacingAndGlyphs"
       y="561.39099"
       x="337.375">5112 — Chèques à encaisser</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
     id="text309"
     y="556.79102"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">200,00</text>
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect315"
     ry="4"
     rx="4"
     y="532"
     x="809"
     height="36.780998"
     width="89.188004" />
  <rect
     width="103.188"
     height="36.780998"
     x="907"
     y="532"
     rx="4"
     ry="4"
     id="rect323"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="173.188"
     height="36.780998"
     x="1024"
     y="532"
     rx="4"
     ry="4"
     id="rect330"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="354.96899"
     height="36.780998"
     x="192"
     y="578"
     rx="4"
     ry="4"
     id="rect358"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     id="rect360"
     ry="3.2"
     rx="3.2"
     y="579"
     x="193"
     height="34.780998"
     width="134.78101"
     style="fill:#cccccc" />
  <text
     style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="span26"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="19.2"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan362"
       lengthAdjust="spacingAndGlyphs"
       y="606.89099"
       x="202.59399">𝍢</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text367"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan365"
       lengthAdjust="spacingAndGlyphs"
       y="604.89099"
       x="228.188">Sélectionner</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text372"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan370"
       lengthAdjust="spacingAndGlyphs"
       y="607.39099"
       x="337.375">5112 — Chèques à encaisser</tspan>
  </text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15"
     id="rect378"
     ry="4"
     rx="4"
     y="578"
     x="711"
     height="36.780998"
     width="89.188004" />
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect386"
     ry="4"
     rx="4"
     y="578"
     x="809"
     height="36.780998"
     width="89.188004" />
  <rect
     width="103.188"
     height="36.780998"
     x="907"
     y="578"
     rx="4"
     ry="4"
     id="rect394"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="173.188"
     height="36.780998"
     x="1024"
     y="578"
     rx="4"
     ry="4"
     id="rect401"
     style="fill:#ffffff;stroke:#9c4f15" />
  <text
     dominant-baseline="central"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     x="916.59998"
     y="414.79099"
     id="text115"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central">CM 43291</text>
  <text
     dominant-baseline="central"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     x="916.59998"
     y="460.79099"
     id="text183"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central">BP 00007</text>
  <text
     dominant-baseline="central"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     x="916.59998"
     y="506.79102"
     id="text254"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central">LBP 200674</text>
  <text
     dominant-baseline="central"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     x="916.59998"
     y="552.79102"
     id="text325"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central">CA 2662545</text>
  <text
     dominant-baseline="central"
     font-size="16"
     font-stretch="100%"
     font-weight="400"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     x="916.59998"
     y="598.79102"
     id="text396"
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central">LNP 206262</text>
  <g
     id="g3803"
     transform="translate(-46,-6.625)">
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
       id="text121"
       y="421.41599"
       x="1079.6"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="central">Annie</text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
       id="text190"
       y="467.41599"
       x="1079.6"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="central">Association</text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
       id="text261"
       y="513.41602"
       x="1079.6"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="central">Margot</text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
       id="text332"
       y="559.41602"
       x="1079.6"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="central">Jess</text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
       id="text403"
       y="605.41602"
       x="1079.6"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="central">David</text>
  </g>
  <rect
     width="502.96899"
     height="36.780998"
     x="192"
     y="624"
     rx="4"
     ry="4"
     id="rect429"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     id="rect431"
     ry="3.2"
     rx="3.2"
     y="625"
     x="193"
     height="34.780998"
     width="134.78101"
     style="fill:#cccccc" />
  <text
     style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="span28"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="19.2"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan433"
       lengthAdjust="spacingAndGlyphs"
       y="652.89099"
       x="202.59399">𝍢</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text438"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan436"
       lengthAdjust="spacingAndGlyphs"
       y="650.89099"
       x="228.188">Sélectionner</tspan>
  </text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     id="text443"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       id="tspan441"
       lengthAdjust="spacingAndGlyphs"
       y="653.39099"
       x="337.375">754 — Ressources liées à la générosité du public</tspan>
  </text>
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect449"
     ry="4"
     rx="4"
     y="624"
     x="711"
     height="36.780998"
     width="89.188004" />
  <rect
     style="fill:#ffffff;stroke:#9c4f15"
     id="rect457"
     ry="4"
     rx="4"
     y="624"
     x="809"
     height="36.780998"
     width="89.188004" />
  <rect
     width="103.188"
     height="36.780998"
     x="907"
     y="624"
     rx="4"
     ry="4"
     id="rect465"
     style="fill:#ffffff;stroke:#9c4f15" />
  <rect
     width="173.188"
     height="36.780998"
     x="1024"
     y="624"
     rx="4"
     ry="4"
     id="rect470"
     style="fill:#ffffff;stroke:#9c4f15" />
  <text
     dominant-baseline="text-after-edge"
     font-size="16"
     font-stretch="100%"
     font-weight="700"
     text-decoration="none solid rgb(0, 0, 0)"
     word-spacing="0"
     id="th1"
     style="color:#000000;font-weight:700;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
     x="-6"
     y="-6.625">
    <tspan
       x="192"
       y="698"
       lengthAdjust="spacingAndGlyphs"
       id="tspan497">Total</tspan>
  </text>
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect500"
     ry="4"
     rx="4"
     y="669"
     x="711"
     height="36.780998"
     width="89.188004" />
  <rect
     style="fill:#eeeeee;stroke:#999999"
     id="rect507"
     ry="4"
     rx="4"
     y="669"
     x="809"
     height="36.780998"
     width="89.188004" />
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
     id="text101"
     y="414.79099"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">12,00</text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
     id="text167"
     y="460.79099"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">50,00</text>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
     id="text238"
     y="506.79102"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">50,00</text>
  <rect
     style="fill:#ffffff;stroke:#9c4f15"
     id="rect307"
     ry="4"
     rx="4"
     y="532"
     x="711"
     height="36.780998"
     width="89.188004" />
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
     id="text380"
     y="598.79102"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">40,00</text>
  <text
     style="color:#999999;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#999999"
     id="text451"
     y="644.79102"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(153, 153, 153)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">0</text>
  <text
     style="color:#666666;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#666666"
     id="text502"
     y="689.79102"
     x="720.59998"
     word-spacing="0"
     text-decoration="none solid rgb(102, 102, 102)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">352,00</text>
  <g
     id="g3786"
     transform="translate(-46,-6.625)">
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(153, 153, 153)"
       word-spacing="0"
       x="864.59998"
       y="421.41599"
       id="text108"
       style="color:#999999;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#999999">0</text>
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(153, 153, 153)"
       word-spacing="0"
       x="864.59998"
       y="467.41599"
       id="text175"
       style="color:#999999;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#999999">0</text>
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(153, 153, 153)"
       word-spacing="0"
       x="864.59998"
       y="513.41602"
       id="text246"
       style="color:#999999;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#999999">0</text>
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(153, 153, 153)"
       word-spacing="0"
       x="864.59998"
       y="559.41602"
       id="text317"
       style="color:#999999;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#999999">0</text>
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(153, 153, 153)"
       word-spacing="0"
       x="864.59998"
       y="605.41602"
       id="text388"
       style="color:#999999;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#999999">0</text>
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(0, 0, 0)"
       word-spacing="0"
       x="864.59998"
       y="651.41602"
       id="text459"
       style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central">352,00</text>
    <text
       dominant-baseline="central"
       font-size="16"
       font-stretch="100%"
       font-variant="tabular-nums"
       font-weight="400"
       text-decoration="none solid rgb(102, 102, 102)"
       word-spacing="0"
       x="864.59998"
       y="696.41602"
       id="text509"
       style="color:#666666;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central;fill:#666666">352,00</text>
  </g>
  <g
     id="g3716"
     transform="translate(-506,-6.625)">
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect131"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="403.81299"
       x="1723"
       height="29.375"
       width="96.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span19"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan133"
         lengthAdjust="spacingAndGlyphs"
         y="429"
         x="1731.391">➖</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text138"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan136"
         lengthAdjust="spacingAndGlyphs"
         y="427"
         x="1755.984">Enlever</tspan>
    </text>
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect202"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="449.81299"
       x="1723"
       height="29.375"
       width="96.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span21"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan204"
         lengthAdjust="spacingAndGlyphs"
         y="475"
         x="1731.391">➖</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text209"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan207"
         lengthAdjust="spacingAndGlyphs"
         y="473"
         x="1755.984">Enlever</tspan>
    </text>
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect273"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="495.81299"
       x="1723"
       height="29.375"
       width="96.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span23"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan275"
         lengthAdjust="spacingAndGlyphs"
         y="521"
         x="1731.391">➖</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text280"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan278"
         lengthAdjust="spacingAndGlyphs"
         y="519"
         x="1755.984">Enlever</tspan>
    </text>
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect344"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="541.81299"
       x="1723"
       height="29.375"
       width="96.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span25"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan346"
         lengthAdjust="spacingAndGlyphs"
         y="567"
         x="1731.391">➖</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text351"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan349"
         lengthAdjust="spacingAndGlyphs"
         y="565"
         x="1755.984">Enlever</tspan>
    </text>
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect415"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="587.81299"
       x="1723"
       height="29.375"
       width="96.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span27"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan417"
         lengthAdjust="spacingAndGlyphs"
         y="613"
         x="1731.391">➖</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text422"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan420"
         lengthAdjust="spacingAndGlyphs"
         y="611"
         x="1755.984">Enlever</tspan>
    </text>
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect482"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="633.81299"
       x="1723"
       height="29.375"
       width="96.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span29"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan484"
         lengthAdjust="spacingAndGlyphs"
         y="659"
         x="1731.391">➖</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text489"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan487"
         lengthAdjust="spacingAndGlyphs"
         y="657"
         x="1755.984">Enlever</tspan>
    </text>
    <rect
       style="fill:#cccccc;stroke-width:2"
       id="rect516"
       ry="3.2"
       rx="3.2"
       stroke="rgba(156, 79, 21, 0.5)"
       y="678.81299"
       x="1723"
       height="29.375"
       width="92.375" />
    <text
       style="color:#000000;font-weight:400;font-size:19.20000076px;font-family:gicon, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="span30"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="19.2"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan518"
         lengthAdjust="spacingAndGlyphs"
         y="704"
         x="1731.391">➕</tspan>
    </text>
    <text
       style="color:#000000;font-weight:400;font-size:16px;font-family:Arial;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:text-after-edge"
       id="text523"
       word-spacing="0"
       text-decoration="none solid rgb(0, 0, 0)"
       font-weight="400"
       font-stretch="100%"
       font-size="16"
       dominant-baseline="text-after-edge">
      <tspan
         id="tspan521"
         lengthAdjust="spacingAndGlyphs"
         y="702"
         x="1755.984">Ajouter</tspan>
    </text>
  </g>
  <text
     style="color:#000000;font-weight:400;font-size:16px;font-family:'Trebuchet MS', Arial, Helvetica, sans-serif;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;word-spacing:0;writing-mode:lr-tb;dominant-baseline:central"
     id="text238-3"
     y="552.72943"
     x="719.62292"
     word-spacing="0"
     text-decoration="none solid rgb(0, 0, 0)"
     font-weight="400"
     font-variant="tabular-nums"
     font-stretch="100%"
     font-size="16"
     dominant-baseline="central">200,00</text>
</svg>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Added doc/selfhost2.png version [701da39174].

cannot compute difference between binary files

Modified src/.htaccess.www from [3ae6d113cf] to [1fa848cf6a].

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Objectif: supprimer le /www/ de l'URL
# Note: il est probable qu'il soit nécessaire d'adapter la configuration
# à votre hébergeur !

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

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







|
|
|





20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Objectif: supprimer le /www/ de l'URL
# Note: il est probable qu'il soit nécessaire d'adapter la configuration
# à votre hébergeur !

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

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

Modified src/Makefile from [9e2c4e5dbe] to [a6977744af].

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
	mv www/admin/static/mini.css /tmp/paheko-build/paheko/src/www/admin/static/admin.css
	cd /tmp/paheko-build/paheko/src/www/admin/static; \
		rm -f styles/[0-9]*.css; \
		rm -f font/*.css font/*.json
	cd /tmp/paheko-build/paheko/src; \
		rm -f Makefile include/lib/KD2/data/countries.en.json
	cd /tmp/paheko-build/paheko/src/data; mkdir plugins && cd plugins; \
		wget https://fossil.kd2.org/paheko-plugins/uv/welcome.tar.gz






	mv /tmp/paheko-build/paheko/src /tmp/paheko-build/paheko-${VERSION}
	@#cd /tmp/paheko-build/; zip -r -9 paheko-${VERSION}.zip paheko-${VERSION};
	@#mv -f /tmp/paheko-build/paheko-${VERSION}.zip ./
	tar czvfh paheko-${VERSION}.tar.gz --hard-dereference -C /tmp/paheko-build paheko-${VERSION}

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




publish: release deb
	$(eval VERSION=$(shell cat VERSION))
	gpg --armor --detach-sign paheko-${VERSION}.tar.gz
	fossil uv sync
	#fossil uv ls | fgrep -v 'paheko-0.8.5' | grep '^paheko-.*\.(tar\.bz2|deb)' | xargs fossil uv rm
	fossil uv add paheko-${VERSION}.tar.gz
	fossil uv add paheko-${VERSION}.tar.gz.asc
	cd ../debian && fossil uv add paheko-${VERSION}.deb
	cd ../tools && php make_installer.php > install.php && fossil uv add install.php && rm install.php
	fossil uv sync


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

minify:
	cat `ls www/admin/static/styles/[0-9]*.css` | sed 's/\.\.\///' > www/admin/static/mini.css
	@# Minify is only gaining 500 gzipped bytes (4kB uncompressed) but making things hard to read/hack
	@#yui-compressor --nomunge www/admin/static/mini.css -o www/admin/static/mini.css







|
>
>
>
>
>
>






|

>
>
>
|

|




|


>








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
	mv www/admin/static/mini.css /tmp/paheko-build/paheko/src/www/admin/static/admin.css
	cd /tmp/paheko-build/paheko/src/www/admin/static; \
		rm -f styles/[0-9]*.css; \
		rm -f font/*.css font/*.json
	cd /tmp/paheko-build/paheko/src; \
		rm -f Makefile include/lib/KD2/data/countries.en.json
	cd /tmp/paheko-build/paheko/src/data; mkdir plugins && cd plugins; \
		wget https://fossil.kd2.org/paheko-plugins/uv/welcome.tar.gz \
		&& wget https://fossil.kd2.org/paheko-plugins/uv/caisse.tar.gz \
		&& wget https://fossil.kd2.org/paheko-plugins/uv/taima.tar.gz \
		&& wget https://fossil.kd2.org/paheko-plugins/uv/dompdf.tar.gz \
		&& wget https://fossil.kd2.org/paheko-plugins/uv/reservations.tar.gz \
		&& wget https://fossil.kd2.org/paheko-plugins/uv/webstats.tar.gz \
		&& wget https://fossil.kd2.org/paheko-plugins/uv/stock_velos.tar.gz
	mv /tmp/paheko-build/paheko/src /tmp/paheko-build/paheko-${VERSION}
	@#cd /tmp/paheko-build/; zip -r -9 paheko-${VERSION}.zip paheko-${VERSION};
	@#mv -f /tmp/paheko-build/paheko-${VERSION}.zip ./
	tar czvfh paheko-${VERSION}.tar.gz --hard-dereference -C /tmp/paheko-build paheko-${VERSION}

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

windows:
	cd ../build/windows; make installer

publish: release deb windows
	$(eval VERSION=$(shell cat VERSION))
	gpg --armor -r dev@paheko.cloud --detach-sign paheko-${VERSION}.tar.gz
	fossil uv sync
	#fossil uv ls | fgrep -v 'paheko-0.8.5' | grep '^paheko-.*\.(tar\.bz2|deb)' | xargs fossil uv rm
	fossil uv add paheko-${VERSION}.tar.gz
	fossil uv add paheko-${VERSION}.tar.gz.asc
	cd ../build/debian && fossil uv add paheko-${VERSION}.deb
	cd ../tools && php make_installer.php > install.php && fossil uv add install.php && rm install.php
	fossil uv sync
	cd ../build/windows && make publish

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

minify:
	cat `ls www/admin/static/styles/[0-9]*.css` | sed 's/\.\.\///' > www/admin/static/mini.css
	@# Minify is only gaining 500 gzipped bytes (4kB uncompressed) but making things hard to read/hack
	@#yui-compressor --nomunge www/admin/static/mini.css -o www/admin/static/mini.css

Modified src/config.dist.php from [5e8ac1ce22] to [65125f1971].

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

/**
 * Ce fichier représente un exemple des constantes de configuration
 * disponibles pour Garradin.
 *
 * NE PAS MODIFIER CE FICHIER!
 *
 * Pour configurer Garradin, copiez ce fichier en 'config.local.php'
 * puis décommentez et modifiez ce dont vous avez besoin.
 */

// Nécessaire pour situer les constantes dans le bon namespace
namespace Garradin;

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

//const SECRET_KEY = '3xUhIgGwuovRKOjVsVPQ5yUMfXUSIOX2GKzcebsz5OINrYC50r';

/**
 * @var null|int|array
 *
 * Forcer la connexion locale
 *
 * Si un numéro est spécifié, alors le membre avec l'ID correspondant à ce
 * numéro sera connecté (sans besoin de mot de passe).
 *
 * Exemple: LOCAL_LOGIN = 42 connectera automatiquement le membre avec id = 42
 * Attention à ne pas utiliser en production !
 *
 * Si le nombre spécifié est -1, alors c'est le premier membre trouvé qui
 * peut gérer la configuration (et donc modifier les droits des membres)
 * qui sera connecté.
 *
 * Si un tableau est spécifié, alors Garradin considérera que l'utilisateur
 * connecté fourni dans le tableau n'est pas un membre.
 * Voir la documentation sur l'utilisation avec SSO et LDAP pour plus de détails.
 *
 * Exemple :
 * const LOCAL_LOGIN = [
 * 	'user' => ['_name' => 'bohwaz'],
 * 	'permissions' => ['users' => 9, 'config' => 9]




|



|







|







|




















|







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

/**
 * Ce fichier représente un exemple des constantes de configuration
 * disponibles pour Paheko.
 *
 * NE PAS MODIFIER CE FICHIER!
 *
 * Pour configurer Paheko, copiez ce fichier en 'config.local.php'
 * puis décommentez et modifiez ce dont vous avez besoin.
 */

// Nécessaire pour situer les constantes dans le bon namespace
namespace Garradin;

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

//const SECRET_KEY = '3xUhIgGwuovRKOjVsVPQ5yUMfXUSIOX2GKzcebsz5OINrYC50r';

/**
 * @var null|int|array
 *
 * Forcer la connexion locale
 *
 * Si un numéro est spécifié, alors le membre avec l'ID correspondant à ce
 * numéro sera connecté (sans besoin de mot de passe).
 *
 * Exemple: LOCAL_LOGIN = 42 connectera automatiquement le membre avec id = 42
 * Attention à ne pas utiliser en production !
 *
 * Si le nombre spécifié est -1, alors c'est le premier membre trouvé qui
 * peut gérer la configuration (et donc modifier les droits des membres)
 * qui sera connecté.
 *
 * Si un tableau est spécifié, alors Paheko considérera que l'utilisateur
 * connecté fourni dans le tableau n'est pas un membre.
 * Voir la documentation sur l'utilisation avec SSO et LDAP pour plus de détails.
 *
 * Exemple :
 * const LOCAL_LOGIN = [
 * 	'user' => ['_name' => 'bohwaz'],
 * 	'permissions' => ['users' => 9, 'config' => 9]
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
 *
 * Défaut : true
 */

//const ALLOW_MODIFIED_IMPORT = true;

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

//const ROOT = __DIR__;

/**
 * Répertoire où sont situées les données de Garradin
 * (incluant la base de données SQLite, les sauvegardes, le cache, les fichiers locaux et les plugins)
 *
 * Défaut : sous-répertoire "data" de la racine
 */

//const DATA_ROOT = ROOT . '/data';

/**
 * Répertoire où est situé le cache,
 * exemples : graphiques de statistiques, templates Brindille, etc.
 *
 * Défaut : sous-répertoire 'cache' de DATA_ROOT
 */

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

/**
 * Répertoire où est situé le cache partagé entre instances
 * Garradin utilisera ce répertoire pour stocker le cache susceptible d'être partagé entre instances, comme
 * le code PHP généré à partir des templates Smartyer.
 *
 * Défaut : sous-répertoire 'shared' de CACHE_ROOT
 */

//const SHARED_CACHE_ROOT = CACHE_ROOT . '/shared';








|

|





|


















|







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
 *
 * Défaut : true
 */

//const ALLOW_MODIFIED_IMPORT = true;

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

//const ROOT = __DIR__;

/**
 * Répertoire où sont situées les données de Paheko
 * (incluant la base de données SQLite, les sauvegardes, le cache, les fichiers locaux et les plugins)
 *
 * Défaut : sous-répertoire "data" de la racine
 */

//const DATA_ROOT = ROOT . '/data';

/**
 * Répertoire où est situé le cache,
 * exemples : graphiques de statistiques, templates Brindille, etc.
 *
 * Défaut : sous-répertoire 'cache' de DATA_ROOT
 */

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

/**
 * Répertoire où est situé le cache partagé entre instances
 * Paheko utilisera ce répertoire pour stocker le cache susceptible d'être partagé entre instances, comme
 * le code PHP généré à partir des templates Smartyer.
 *
 * Défaut : sous-répertoire 'shared' de CACHE_ROOT
 */

//const SHARED_CACHE_ROOT = CACHE_ROOT . '/shared';

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
 *
 * @var null|string
 */

//const WEB_CACHE_ROOT = CACHE_ROOT . '/web/%host%';

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

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

/**







|







128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
 *
 * @var null|string
 */

//const WEB_CACHE_ROOT = CACHE_ROOT . '/web/%host%';

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

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

/**
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
 * La clé est le nom du signal, et la valeur est la fonction.
 *
 * Défaut: [] (tableau vide)
 */
//const SYSTEM_SIGNALS = [['files.delete' => 'MyNamespace\Signals::deleteFile'], ['entity.Accounting\Transaction.save.before' => 'MyNamespace\Signals::saveTransaction']];

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

//const WWW_URI = '/asso/';

/**
 * Adresse URL HTTP(S) de Garradin
 *
 * Défaut : découverte à partir de HTTP_HOST ou SERVER_NAME + WWW_URI
 */

//const WWW_URL = 'http://garradin.chezmoi.tld' . WWW_URI;

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

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

/**
 * Affichage des erreurs
 * Si "true" alors un message expliquant l'erreur et comment rapporter le bug s'affiche
 * en cas d'erreur. Sinon rien ne sera affiché.
 *
 * Défaut : false







|








|




|


|




|







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
 * La clé est le nom du signal, et la valeur est la fonction.
 *
 * Défaut: [] (tableau vide)
 */
//const SYSTEM_SIGNALS = [['files.delete' => 'MyNamespace\Signals::deleteFile'], ['entity.Accounting\Transaction.save.before' => 'MyNamespace\Signals::saveTransaction']];

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

//const WWW_URI = '/asso/';

/**
 * Adresse URL HTTP(S) de Paheko
 *
 * Défaut : découverte à partir de HTTP_HOST ou SERVER_NAME + WWW_URI
 */

//const WWW_URL = 'http://paheko.chezmoi.tld' . WWW_URI;

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

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

/**
 * Affichage des erreurs
 * Si "true" alors un message expliquant l'erreur et comment rapporter le bug s'affiche
 * en cas d'erreur. Sinon rien ne sera affiché.
 *
 * Défaut : false
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
 *
 * Défaut : false
 */

//const MAIL_ERRORS = false;

/**
 * Envoi des erreurs à une API compatible AirBrake/Errbit/Garradin
 *
 * Si renseigné avec une URL HTTP(S) valide, chaque erreur système sera envoyée
 * automatiquement à cette URL.
 *
 * Si laissé à null, aucun rapport ne sera envoyé.
 *
 * Garradin accepte aussi les rapports d'erreur venant d'autres instances.
 *
 * Pour cela utiliser l'URL https://login:password@garradin.site.tld/api/errors/report
 * (voir aussi API_USER et API_PASSWORD)
 *
 * Les erreurs seront ensuite visibles dans
 * Configuration -> Fonctions avancées -> Journal d'erreurs
 *
 * Défaut : null
 */







|






|

|







208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
 *
 * Défaut : false
 */

//const MAIL_ERRORS = false;

/**
 * Envoi des erreurs à une API compatible AirBrake/Errbit/Paheko
 *
 * Si renseigné avec une URL HTTP(S) valide, chaque erreur système sera envoyée
 * automatiquement à cette URL.
 *
 * Si laissé à null, aucun rapport ne sera envoyé.
 *
 * Paheko accepte aussi les rapports d'erreur venant d'autres instances.
 *
 * Pour cela utiliser l'URL https://login:password@paheko.site.tld/api/errors/report
 * (voir aussi API_USER et API_PASSWORD)
 *
 * Les erreurs seront ensuite visibles dans
 * Configuration -> Fonctions avancées -> Journal d'erreurs
 *
 * Défaut : null
 */
239
240
241
242
243
244
245

















246
247
248
249
250
251
252
 * est affiché. Il est possible de personnaliser ce message avec cette constante.
 *
 * Voir include/init.php pour le template par défaut.
 */

// const ERRORS_TEMPLATE = null;


















/**
 * Activation des détails techniques (utile en auto-hébergement) :
 * - version de PHP
 * - page permettant de visualiser les erreurs présentes dans le error.log
 * - permettre de migrer d'un stockage de fichiers à l'autre
 * - vérification de nouvelle version (sur la page configuration)
 *







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







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
 * est affiché. Il est possible de personnaliser ce message avec cette constante.
 *
 * Voir include/init.php pour le template par défaut.
 */

// const ERRORS_TEMPLATE = null;

/**
 * Loguer / envoyer par mail les erreurs utilisateur ?
 *
 * Si positionné à 1, *toutes* les erreurs utilisateur (champ mal rempli dans un formulaire,
 * formulaire dont le token CSRF a expiré, etc.) seront loguées et/ou envoyées par mail
 * (selon le réglage choisit ci-dessus).
 *
 * Si positionné à 2, alors l'exception sera remontée dans la stack, *et* loguée/envoyée.
 *
 * Utile pour le développement.
 *
 * Défaut : 0 (ne rien faire)
 * @var int
 */

// const REPORT_USER_EXCEPTIONS = 0;

/**
 * Activation des détails techniques (utile en auto-hébergement) :
 * - version de PHP
 * - page permettant de visualiser les erreurs présentes dans le error.log
 * - permettre de migrer d'un stockage de fichiers à l'autre
 * - vérification de nouvelle version (sur la page configuration)
 *
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
 * pour faire une mise à jour en deux clics.
 *
 * Il est conseillé de désactiver cette fonctionnalité si vous ne voulez pas
 * permettre à un utilisateur de casser l'installation !
 *
 * Si cette constante est désactivée, mais que ENABLE_TECH_DETAILS est activé,
 * la vérification de nouvelle version se fera quand même, mais plutôt que de proposer
 * la mise à jour, Garradin proposera de se rendre sur le site officiel pour
 * télécharger la mise à jour.
 *
 * Défaut : true
 *
 * @var bool
 */








|







348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
 * pour faire une mise à jour en deux clics.
 *
 * Il est conseillé de désactiver cette fonctionnalité si vous ne voulez pas
 * permettre à un utilisateur de casser l'installation !
 *
 * Si cette constante est désactivée, mais que ENABLE_TECH_DETAILS est activé,
 * la vérification de nouvelle version se fera quand même, mais plutôt que de proposer
 * la mise à jour, Paheko proposera de se rendre sur le site officiel pour
 * télécharger la mise à jour.
 *
 * Défaut : true
 *
 * @var bool
 */

373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
 * - Lighttpd
 *
 * N'activer que si vous êtes sûr que le module est installé et activé (sinon
 * les fichiers ne pourront être vus ou téléchargés).
 * Nginx n'est PAS supporté, car X-Accel-Redirect ne peut gérer que des fichiers
 * qui sont *dans* le document root du vhost, ce qui n'est pas le cas ici.
 *
 * Pour activer X-SendFile mettre dans la config du virtualhost de Garradin:
 * XSendFile On
 * XSendFilePath /var/www/garradin
 *
 * (remplacer le chemin par le répertoire racine de Garradin)
 *
 * Détails : https://tn123.org/mod_xsendfile/
 *
 * Défaut : false
 */

//const ENABLE_XSENDFILE = false;







|

|

|







390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
 * - Lighttpd
 *
 * N'activer que si vous êtes sûr que le module est installé et activé (sinon
 * les fichiers ne pourront être vus ou téléchargés).
 * Nginx n'est PAS supporté, car X-Accel-Redirect ne peut gérer que des fichiers
 * qui sont *dans* le document root du vhost, ce qui n'est pas le cas ici.
 *
 * Pour activer X-SendFile mettre dans la config du virtualhost de Paheko:
 * XSendFile On
 * XSendFilePath /var/www/paheko
 *
 * (remplacer le chemin par le répertoire racine de Paheko)
 *
 * Détails : https://tn123.org/mod_xsendfile/
 *
 * Défaut : false
 */

//const ENABLE_XSENDFILE = false;
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
 * Login utilisateur pour le server SMTP
 *
 * mettre à null pour utiliser un serveur local ou anonyme
 *
 * Défaut : null
 */

//const SMTP_USER = 'garradin@monserveur.com';

/**
 * Mot de passe pour le serveur SMTP
 *
 * mettre à null pour utiliser un serveur local ou anonyme
 *
 * Défaut : null







|







459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
 * Login utilisateur pour le server SMTP
 *
 * mettre à null pour utiliser un serveur local ou anonyme
 *
 * Défaut : null
 */

//const SMTP_USER = 'paheko@monserveur.com';

/**
 * Mot de passe pour le serveur SMTP
 *
 * mettre à null pour utiliser un serveur local ou anonyme
 *
 * Défaut : null
551
552
553
554
555
556
557












558
559
560
561
562
563
564
565
566
567
568
569
 * Utile pour s'assurer qu'on est sur une instance de test par exemple.
 *
 * Défault : false
 * @var bool
 */
//const FORCE_CUSTOM_COLORS = false;













/**
 * Stockage des fichiers
 *
 * Indiquer ici le nom d'une classe de stockage de fichiers
 * (parmis celles disponibles dans lib/Garradin/Files/Backend)
 *
 * Indiquer NULL si vous souhaitez stocker les fichier dans la base
 * de données SQLite (valeur par défaut).
 *
 * Classes de stockage possibles :
 * - SQLite : enregistre dans la base de données (défaut)
 * - FileSystem : enregistrement des fichiers dans le système de fichier







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




|







568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
 * Utile pour s'assurer qu'on est sur une instance de test par exemple.
 *
 * Défault : false
 * @var bool
 */
//const FORCE_CUSTOM_COLORS = false;

/**
 * Désactiver le formulaire d'installation
 *
 * Si TRUE, alors le formulaire d'installation renverra une erreur.
 *
 * Utile pour une installation multi-associations.
 *
 * Défaut : false
 * @var bool
 */
//const DISABLE_INSTALL_FORM = false;

/**
 * Stockage des fichiers
 *
 * Indiquer ici le nom d'une classe de stockage de fichiers
 * (parmis celles disponibles dans lib/Paheko/Files/Backend)
 *
 * Indiquer NULL si vous souhaitez stocker les fichier dans la base
 * de données SQLite (valeur par défaut).
 *
 * Classes de stockage possibles :
 * - SQLite : enregistre dans la base de données (défaut)
 * - FileSystem : enregistrement des fichiers dans le système de fichier
619
620
621
622
623
624
625
626
627
628
629
630
631
632









633

634
635


636
637

638

639
640
641
642
643
644

645
646











647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
 * Défaut : null
 */

//const WOPI_DISCOVERY_URL = 'http://localhost:9980/hosting/discovery';

/**
 * PDF_COMMAND
 * 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. Si aucune solution n'est disponible,
 * une erreur sera levée.
 *









 * %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


 *
 * Il est aussi possible de simplement spécifier le nom du programme à utiliser,

 * et Garradin placera les arguments automatiquement : prince, chromium, wkhtmltopdf ou weasyprint.

 *
 * Si vous utilisez Prince, un message mentionnant l'utilisation de Prince
 * sera joint aux e-mails utilisant des fichiers PDF, conformément à la licence :
 * https://www.princexml.com/purchase/license_faq/#non-commercial
 *
 * Défaut : null

 */
//const PDF_COMMAND = 'wkhtmltopdf -q --print-media-type --enable-local-file-access %s %s';












/**
 * CALC_CONVERT_COMMAND
 * Outil de conversion de formats de tableur vers un format propriétaire
 *
 * Garradin gère nativement les exports en ODS (OpenDocument : LibreOffice)
 * et CSV, et imports en CSV.
 *
 * En indiquant ici le nom d'un outil, Garradin autorisera aussi
 * l'import en XLSX, XLS et ODS, et l'export en XLSX.
 *
 * Pour cela il procédera simplement à une conversion entre les formats natifs
 * ODS/CSV et XLSX ou XLS.
 *
 * Noter qu'installer ces commandes peut introduire des risques de sécurité sur le serveur.
 *







|

<
|
|
|

>
>
>
>
>
>
>
>
>
|
>

<
>
>

<
>
|
>





|
>

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





|


|







648
649
650
651
652
653
654
655
656

657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672

673
674
675

676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
 * Défaut : null
 */

//const WOPI_DISCOVERY_URL = 'http://localhost:9980/hosting/discovery';

/**
 * PDF_COMMAND
 * Commande qui sera exécutée pour créer un fichier PDF à partir d'un HTML.
 *

 * Si laissé sur 'auto', Paheko essaiera de détecter une solution entre
 * PrinceXML, Chromium, wkhtmltopdf ou weasyprint (dans cet ordre).
 * Si aucune solution n'est disponible, une erreur sera affichée.
 *
 * Il est possible d'indiquer NULL pour désactiver l'export en PDF.
 *
 * Il est possible d'indiquer uniquement le nom du programme :
 * 'chromium', 'prince', 'weasyprint', ou 'wkhtmltopdf'.
 * Dans ce cas, Paheko utilisera les paramètres par défaut de ce programme.
 *
 * Alternativement, il est possible d'indiquer la commande complète avec
 * les options, par exemple '/usr/bin/chromium --headless --print-to-pdf=%2$s %1$s'
 * Dans ce cas :
 * - %1$s sera remplacé par le chemin du fichier HTML existant,
 * - %2$s sera remplacé par le chemin du fichier PDF à créer.
 *

 * Si vous utilisez une extension pour générer les PDF (comme DomPDF), alors
 * laisser cette constante sur 'auto'.
 *

 * Exemples :
 * 'weasyprint'
 * 'wkhtmltopdf -q --print-media-type --enable-local-file-access %s %s'
 *
 * Si vous utilisez Prince, un message mentionnant l'utilisation de Prince
 * sera joint aux e-mails utilisant des fichiers PDF, conformément à la licence :
 * https://www.princexml.com/purchase/license_faq/#non-commercial
 *
 * Défaut : 'auto'
 * @var null|string
 */
//const PDF_COMMAND = 'auto';

/**
 * PDF_USAGE_LOG
 * Chemin vers le fichier où enregistrer la date de chaque export en PDF
 *
 * Ceci est utilisé notamment pour estimer le prix de la licence PrinceXML.
 *
 * Défaut : NULL
 * @var null|string
 */
//const PDF_USAGE_LOG = null;

/**
 * CALC_CONVERT_COMMAND
 * Outil de conversion de formats de tableur vers un format propriétaire
 *
 * Paheko gère nativement les exports en ODS (OpenDocument : LibreOffice)
 * et CSV, et imports en CSV.
 *
 * En indiquant ici le nom d'un outil, Paheko autorisera aussi
 * l'import en XLSX, XLS et ODS, et l'export en XLSX.
 *
 * Pour cela il procédera simplement à une conversion entre les formats natifs
 * ODS/CSV et XLSX ou XLS.
 *
 * Noter qu'installer ces commandes peut introduire des risques de sécurité sur le serveur.
 *
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
//const CALC_CONVERT_COMMAND = 'ssconvert';
//const CALC_CONVERT_COMMAND = 'unoconvert --interface localhost --port 2022';

/**
 * API_USER et API_PASSWORD
 * Login et mot de passe système de l'API
 *
 * Une API est disponible via l'URL https://login:password@garradin.association.tld/api/...
 * Voir https://fossil.kd2.org/garradin/wiki?name=API pour la documentation
 *
 * Ces deux constantes permettent d'indiquer un nom d'utilisateur
 * et un mot de passe pour accès à l'API.
 *
 * Cet utilisateur est distinct de ceux définis dans la page de gestion des
 * identifiants d'accès à l'API, et aura accès à TOUT en écriture/administration.
 *
 * Défaut: null
 */
//const API_USER = 'coraline';
//const API_PASSWORD = 'thisIsASecretPassword42';

/**
 * DISABLE_INSTALL_PING
 *
 * Lors de l'installation, ou d'une mise à jour, la version installée de Garradin,
 * ainsi que celle de PHP et de SQLite, sont envoyées à Paheko.cloud.
 *
 * Cela permet de savoir quelles sont les versions utilisées, et également de compter
 * le nombre d'installations effectuées.
 *
 * Aucune donnée personnelle n'est envoyée. Un identifiant anonyme est envoyé,
 * permettant d'identifier l'installation et éviter les doublons.







|
|















|







723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
//const CALC_CONVERT_COMMAND = 'ssconvert';
//const CALC_CONVERT_COMMAND = 'unoconvert --interface localhost --port 2022';

/**
 * API_USER et API_PASSWORD
 * Login et mot de passe système de l'API
 *
 * Une API est disponible via l'URL https://login:password@paheko.association.tld/api/...
 * Voir https://fossil.kd2.org/paheko/wiki?name=API pour la documentation
 *
 * Ces deux constantes permettent d'indiquer un nom d'utilisateur
 * et un mot de passe pour accès à l'API.
 *
 * Cet utilisateur est distinct de ceux définis dans la page de gestion des
 * identifiants d'accès à l'API, et aura accès à TOUT en écriture/administration.
 *
 * Défaut: null
 */
//const API_USER = 'coraline';
//const API_PASSWORD = 'thisIsASecretPassword42';

/**
 * DISABLE_INSTALL_PING
 *
 * Lors de l'installation, ou d'une mise à jour, la version installée de Paheko,
 * ainsi que celle de PHP et de SQLite, sont envoyées à Paheko.cloud.
 *
 * Cela permet de savoir quelles sont les versions utilisées, et également de compter
 * le nombre d'installations effectuées.
 *
 * Aucune donnée personnelle n'est envoyée. Un identifiant anonyme est envoyé,
 * permettant d'identifier l'installation et éviter les doublons.

src/include/data/schema.sql became a symlink with target [57116110a2].

Modified src/include/init.php from [62c653a3cf] to [73265b0a9f].

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
// Configuration par défaut, si les constantes ne sont pas définies dans CONFIG_FILE
// (fallback)
if (!defined('Garradin\ROOT'))
{
	define('Garradin\ROOT', dirname(__DIR__));
}

\spl_autoload_register(function (string $classname) {
	$classname = ltrim($classname, '\\');

	// Plugins
	if (substr($classname, 0, 16) == 'Garradin\\Plugin\\')
	{
		$classname = substr($classname, 16);
		$plugin_name = substr($classname, 0, strpos($classname, '\\'));
		$filename = str_replace('\\', '/', substr($classname, strpos($classname, '\\')+1));

		$path = Plugin::getPath(strtolower($plugin_name)) . '/lib/' . $filename . '.php';







	}
	else
	{
		// PSR-0 autoload
		$filename = str_replace('\\', '/', $classname);
		$path = ROOT . '/include/lib/' . $filename . '.php';
	}







|









|
>
>
>
>
>
>
>







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
// Configuration par défaut, si les constantes ne sont pas définies dans CONFIG_FILE
// (fallback)
if (!defined('Garradin\ROOT'))
{
	define('Garradin\ROOT', dirname(__DIR__));
}

\spl_autoload_register(function (string $classname): void {
	$classname = ltrim($classname, '\\');

	// Plugins
	if (substr($classname, 0, 16) == 'Garradin\\Plugin\\')
	{
		$classname = substr($classname, 16);
		$plugin_name = substr($classname, 0, strpos($classname, '\\'));
		$filename = str_replace('\\', '/', substr($classname, strpos($classname, '\\')+1));

		$path = Plugins::getPath(strtolower($plugin_name));

		// Plugin does not exist, just abort
		if (!$path) {
			return;
		}

		$path = $path . '/lib/' . $filename . '.php';
	}
	else
	{
		// PSR-0 autoload
		$filename = str_replace('\\', '/', $classname);
		$path = ROOT . '/include/lib/' . $filename . '.php';
	}
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
	'DB_FILE'               => DATA_ROOT . '/association.sqlite',
	'DB_SCHEMA'             => ROOT . '/include/data/schema.sql',
	'PLUGINS_ROOT'          => DATA_ROOT . '/plugins',
	'ALLOW_MODIFIED_IMPORT' => true,
	'SHOW_ERRORS'           => true,
	'MAIL_ERRORS'           => false,
	'ERRORS_REPORT_URL'     => null,

	'ENABLE_TECH_DETAILS'   => true,
	'HTTP_LOG_FILE'         => null,
	'ENABLE_UPGRADES'       => true,
	'USE_CRON'              => false,
	'ENABLE_XSENDFILE'      => false,
	'DISABLE_EMAIL'         => false,
	'SMTP_HOST'             => false,
	'SMTP_USER'             => null,
	'SMTP_PASSWORD'         => null,
	'SMTP_PORT'             => 587,
	'SMTP_SECURITY'         => 'STARTTLS',
	'MAIL_RETURN_PATH'      => null,
	'MAIL_BOUNCE_PASSWORD'  => null,
	'ADMIN_URL'             => WWW_URL . 'admin/',
	'NTP_SERVER'            => 'fr.pool.ntp.org',
	'ADMIN_COLOR1'          => '#20787a',
	'ADMIN_COLOR2'          => '#85b9ba',
	'ADMIN_BACKGROUND_IMAGE' => WWW_URL . 'admin/static/bg.png',
	'FORCE_CUSTOM_COLORS'   => false,

	'FILE_STORAGE_BACKEND'  => 'SQLite',
	'FILE_STORAGE_CONFIG'   => null,
	'FILE_STORAGE_QUOTA'    => null,
	'API_USER'              => null,
	'API_PASSWORD'          => null,
	'PDF_COMMAND'           => null,

	'CALC_CONVERT_COMMAND'  => null,
	'CONTRIBUTOR_LICENSE'   => null,
	'SQL_DEBUG'             => null,
	'SYSTEM_SIGNALS'        => [],
	'LOCAL_LOGIN'           => null,
	'LEGAL_LINE'            => 'Hébergé par <strong>%1$s</strong>, %2$s',
	'DISABLE_INSTALL_PING'  => false,







>



















>





|
>







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
	'DB_FILE'               => DATA_ROOT . '/association.sqlite',
	'DB_SCHEMA'             => ROOT . '/include/data/schema.sql',
	'PLUGINS_ROOT'          => DATA_ROOT . '/plugins',
	'ALLOW_MODIFIED_IMPORT' => true,
	'SHOW_ERRORS'           => true,
	'MAIL_ERRORS'           => false,
	'ERRORS_REPORT_URL'     => null,
	'REPORT_USER_EXCEPTIONS' => 0,
	'ENABLE_TECH_DETAILS'   => true,
	'HTTP_LOG_FILE'         => null,
	'ENABLE_UPGRADES'       => true,
	'USE_CRON'              => false,
	'ENABLE_XSENDFILE'      => false,
	'DISABLE_EMAIL'         => false,
	'SMTP_HOST'             => false,
	'SMTP_USER'             => null,
	'SMTP_PASSWORD'         => null,
	'SMTP_PORT'             => 587,
	'SMTP_SECURITY'         => 'STARTTLS',
	'MAIL_RETURN_PATH'      => null,
	'MAIL_BOUNCE_PASSWORD'  => null,
	'ADMIN_URL'             => WWW_URL . 'admin/',
	'NTP_SERVER'            => 'fr.pool.ntp.org',
	'ADMIN_COLOR1'          => '#20787a',
	'ADMIN_COLOR2'          => '#85b9ba',
	'ADMIN_BACKGROUND_IMAGE' => WWW_URL . 'admin/static/bg.png',
	'FORCE_CUSTOM_COLORS'   => false,
	'DISABLE_INSTALL_FORM'  => false,
	'FILE_STORAGE_BACKEND'  => 'SQLite',
	'FILE_STORAGE_CONFIG'   => null,
	'FILE_STORAGE_QUOTA'    => null,
	'API_USER'              => null,
	'API_PASSWORD'          => null,
	'PDF_COMMAND'           => 'auto',
	'PDF_USAGE_LOG'         => null,
	'CALC_CONVERT_COMMAND'  => null,
	'CONTRIBUTOR_LICENSE'   => null,
	'SQL_DEBUG'             => null,
	'SYSTEM_SIGNALS'        => [],
	'LOCAL_LOGIN'           => null,
	'LEGAL_LINE'            => 'Hébergé par <strong>%1$s</strong>, %2$s',
	'DISABLE_INSTALL_PING'  => false,
366
367
368
369
370
371
372

373
374

375
376
377
378
379
380
381
		$tpl->assign('admin_url', ADMIN_URL);
		$tpl->display();
	}

	exit;
}


// Message d'erreur simple pour les erreurs de l'utilisateur
ErrorManager::setCustomExceptionHandler('\Garradin\UserException', '\Garradin\user_error');


// Clé secrète utilisée pour chiffrer les tokens CSRF etc.
if (!defined('Garradin\SECRET_KEY'))
{
	if (!is_writable(ROOT)) {
		throw new \RuntimeException('Impossible de créer le fichier de configuration "'. CONFIG_FILE .'". Le répertoire "'. ROOT . '" n\'est pas accessible en écriture.');
	}







>
|
|
>







376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
		$tpl->assign('admin_url', ADMIN_URL);
		$tpl->display();
	}

	exit;
}

if (REPORT_USER_EXCEPTIONS < 2) {
	// Message d'erreur simple pour les erreurs de l'utilisateur
	ErrorManager::setCustomExceptionHandler('\Garradin\UserException', '\Garradin\user_error');
}

// Clé secrète utilisée pour chiffrer les tokens CSRF etc.
if (!defined('Garradin\SECRET_KEY'))
{
	if (!is_writable(ROOT)) {
		throw new \RuntimeException('Impossible de créer le fichier de configuration "'. CONFIG_FILE .'". Le répertoire "'. ROOT . '" n\'est pas accessible en écriture.');
	}
393
394
395
396
397
398
399
400


401
402
403
404
405
406
407

/*
 * Vérifications pour enclencher le processus d'installation ou de mise à jour
 */

if (!defined('Garradin\INSTALL_PROCESS'))
{
	if (!file_exists(DB_FILE)) {


		if (in_array('install.php', get_included_files())) {
			die('Erreur de redirection en boucle : problème de configuration ?');
		}

		Utils::redirect(ADMIN_URL . 'install.php');
	}








|
>
>







405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421

/*
 * Vérifications pour enclencher le processus d'installation ou de mise à jour
 */

if (!defined('Garradin\INSTALL_PROCESS'))
{
	$exists = file_exists(DB_FILE);

	if (!$exists) {
		if (in_array('install.php', get_included_files())) {
			die('Erreur de redirection en boucle : problème de configuration ?');
		}

		Utils::redirect(ADMIN_URL . 'install.php');
	}

Modified src/include/lib/Garradin/API.php from [ef07b4677c] to [1c14e4473d].

205
206
207
208
209
210
211
212












213
214
215
216
217




























218
219
220
221
222
223
224
			}
		}
		elseif ($fn == 'years') {
			if ($this->method != 'GET') {
				throw new APIException('Wrong request method', 400);
			}

			if (preg_match('!^(\d+)/journal!', $param, $match)) {












				try {
					return iterator_to_array(Reports::getJournal(['year' => $match[1]]));
				}
				catch (\LogicException $e) {
					throw new APIException('Missing parameter for journal: ' . $e->getMessage(), 400, $e);




























				}
			}
			elseif ($param == '') {
				return Years::list();
			}
			else {
				throw new APIException('Unknown years action', 404);







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







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
			}
		}
		elseif ($fn == 'years') {
			if ($this->method != 'GET') {
				throw new APIException('Wrong request method', 400);
			}

			if (preg_match('!^(\d+|current)/(journal|account/journal)!', $param, $match)) {
				if ($match[1] == 'current') {
					$id_year = Years::getCurrentOpenYearId();

					if (!$id_year) {
						throw new APIException('There are no currently open years', 404);
					}
				}
				else {
					$id_year = (int)$match[1];
				}

				if ($match[2] == 'journal') {
					try {
						return iterator_to_array(Reports::getJournal(['year' => $id_year]));
					}
					catch (\LogicException $e) {
						throw new APIException('Missing parameter for journal: ' . $e->getMessage(), 400, $e);
					}
				}
				else {
					$year = Years::get($id_year);

					if (!$year) {
						throw new APIException('Invalid year.', 400, $e);
					}

					$a = $year->chart()->accounts();

					if (!empty($_GET['code'])) {
						$account = $a->getWithCode($_GET['code']);
					}
					else {
						$account = $a->get((int)$_GET['code'] ?? null);
					}

					if (!$account) {
						throw new APIException('Unknown account id or code.', 400, $e);
					}

					$list = $account->listJournal($year->id, false);
					$list->setTitle(sprintf('Journal - %s - %s', $account->code, $account->label));
					$list->loadFromQueryString();
					$list->setPageSize(null);
					$list->orderBy('date', false);
					return iterator_to_array($list->iterate());
				}
			}
			elseif ($param == '') {
				return Years::list();
			}
			else {
				throw new APIException('Unknown years action', 404);

Modified src/include/lib/Garradin/Accounting/Accounts.php from [fe546e2752] to [171b8add61].

25
26
27
28
29
30
31





32
33
34
35
36
37
38
		$this->em = EntityManager::getInstance(Account::class);
	}

	static public function get(int $id)
	{
		return EntityManager::findOneById(Account::class, $id);
	}






	static public function getSelector(?int $id): ?array
	{
		if (!$id) {
			return null;
		}








>
>
>
>
>







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
		$this->em = EntityManager::getInstance(Account::class);
	}

	static public function get(int $id)
	{
		return EntityManager::findOneById(Account::class, $id);
	}

	public function getWithCode(string $code): ?Account
	{
		return EntityManager::findOne(Account::class, 'SELECT * FROM @TABLE WHERE code = ? AND id_chart = ?', $code, $this->chart_id);
	}

	static public function getSelector(?int $id): ?array
	{
		if (!$id) {
			return null;
		}

Modified src/include/lib/Garradin/Accounting/AssistedReconciliation.php from [d7c91fad25] to [3d189be803].

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
			$line->amount = Utils::moneyToInteger($line->amount ?? 0);

			if (!empty($line->balance)) {
				$line->balance = (substr($line->balance, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->balance);
			}

			$line->new_params = http_build_query([
				'a' => abs($line->amount)/100,
				'l' => $line->label,
				'd' => $date ? $date->format('Y-m-d') : '',
				't' => $line->amount < 0 ? Transaction::TYPE_EXPENSE : Transaction::TYPE_REVENUE,
				'account' => $account->id,
			]);

			return $line;
		});
	}







|

|







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
			$line->amount = Utils::moneyToInteger($line->amount ?? 0);

			if (!empty($line->balance)) {
				$line->balance = (substr($line->balance, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->balance);
			}

			$line->new_params = http_build_query([
				'0' => abs($line->amount)/100,
				'l' => $line->label,
				'dt' => $date ? $date->format('Y-m-d') : '',
				't' => $line->amount < 0 ? Transaction::TYPE_EXPENSE : Transaction::TYPE_REVENUE,
				'account' => $account->id,
			]);

			return $line;
		});
	}

Modified src/include/lib/Garradin/Accounting/Import.php from [0aadea6577] to [1198624fe0].

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
					// FEC does not define type, so don't change it
					if (isset($row->type)) {
						$transaction->type = $types[$row->type];
					}

					$fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference']));

					// Remove NULL fields
					if (array_key_exists('date', $fields) && null === $fields['date']) {
						unset($fields['date']);
					}

					if (array_key_exists('label', $fields) && null === $fields['label']) {
						unset($fields['label']);
					}

					$transaction->importForm($fields);

					// Set status
					if (!empty($row->status)) {
						$status_list = array_map('trim', explode(',', $row->status));
						$status = 0;







|
<
<
<
|
<
<
<







230
231
232
233
234
235
236
237



238



239
240
241
242
243
244
245
					// FEC does not define type, so don't change it
					if (isset($row->type)) {
						$transaction->type = $types[$row->type];
					}

					$fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference']));

					// Remove empty values



					$fields = array_filter($fields);




					$transaction->importForm($fields);

					// Set status
					if (!empty($row->status)) {
						$status_list = array_map('trim', explode(',', $row->status));
						$status = 0;

Modified src/include/lib/Garradin/Accounting/Reports.php from [428de3f988] to [da4d116997].

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
		else {
			$where = self::getWhereClause($criterias);
			$sql = sprintf('SELECT position, SUM(balance) FROM acc_accounts_balances WHERE %s GROUP BY position;', $where);
		}

		$balances = DB::getInstance()->getAssoc($sql);

		//var_dump('<pre>', $sql, $balances[Account::REVENUE]); exit;

		return ($balances[Account::REVENUE] ?? 0) - ($balances[Account::EXPENSE] ?? 0);
	}

	static public function getBalancesSQL(array $parts = [])
	{
		return sprintf('SELECT %s id_year, id, label, code, type, debit, credit, position, %s, is_debt
			FROM (







<
<







176
177
178
179
180
181
182


183
184
185
186
187
188
189
		else {
			$where = self::getWhereClause($criterias);
			$sql = sprintf('SELECT position, SUM(balance) FROM acc_accounts_balances WHERE %s GROUP BY position;', $where);
		}

		$balances = DB::getInstance()->getAssoc($sql);



		return ($balances[Account::REVENUE] ?? 0) - ($balances[Account::EXPENSE] ?? 0);
	}

	static public function getBalancesSQL(array $parts = [])
	{
		return sprintf('SELECT %s id_year, id, label, code, type, debit, credit, position, %s, is_debt
			FROM (

Modified src/include/lib/Garradin/Accounting/Transactions.php from [ea5b0a2b00] to [f94c739ea4].

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
		return DB::getInstance()->count('acc_transactions_users', 'id_user = ?', $user_id);
	}

	static public function countForCreator(int $user_id): int
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}





















































	static public function setProject(?int $id_project, ?array $transactions = null, ?array $lines = null)
	{
		$db = DB::getInstance();

		if (null !== $id_project && !$db->test(Project::TABLE, 'id = ?', $id_project)) {
			throw new \InvalidArgumentException('Invalid project ID');
		}

		if (isset($transactions, $lines) || ($transactions === null && $lines === null)) {
			throw new \BadMethodCallException('Only one of transactions or lines should be set');
		}

		$selection = array_map('intval', $transactions ?? $lines);
		$where = sprintf($transactions ? 'id_transaction IN (%s)' : 'id IN (%s)', implode(', ', $selection));

		return $db->exec(sprintf('UPDATE acc_transactions_lines SET id_project = %s WHERE %s;',
			(int)$id_project ?: 'NULL', $where));
	}

	static public function listByType(int $year_id, ?int $type)
	{
		$reverse = 1;

		$columns = Account::LIST_COLUMNS;
		unset($columns['line_label'], $columns['sum'], $columns['debit'], $columns['credit']);
		$columns['line_reference']['label'] = 'Réf. paiement';
		$columns['change']['select'] = sprintf('SUM(l.credit) * %d', $reverse);







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




















|







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
		return DB::getInstance()->count('acc_transactions_users', 'id_user = ?', $user_id);
	}

	static public function countForCreator(int $user_id): int
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}

	/**
	 * Returns a dynamic list of all waiting credit and debt transactions for closed years
	 */
	static public function listPendingCreditAndDebtForClosedYears(): DynamicList
	{
		$columns = Account::LIST_COLUMNS;

		unset($columns['line_label'], $columns['sum'], $columns['debit'], $columns['credit']);
		unset($columns['project_code'], $columns['id_project'], $columns['line_reference']);

		$columns['change']['select'] = 'SUM(l.credit)';
		$columns['change']['label'] = 'Montant';

		$columns = [
			'year_label' => [
				'select' => 'y.label',
				'label' => 'Exercice',
			],
			'type_label' => [
				'select' => 't.type',
				'label' => 'Type',
			]]
			+ $columns;

		$conditions = sprintf('y.closed = 1 AND t.status & %d AND t.type IN (%d, %d)',
			Transaction::STATUS_WAITING, Transaction::TYPE_CREDIT, Transaction::TYPE_DEBT);

		$tables = 'acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			INNER JOIN acc_years y ON y.id = t.id_year';

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', true);
		$list->setCount('COUNT(DISTINCT t.id)');
		$list->groupBy('t.id');
		$list->setModifier(function (&$row) {
			$row->date = \DateTime::createFromFormat('!Y-m-d', $row->date);

			if (isset($row->type_label)) {
				$row->type_label = Transaction::TYPES_NAMES[(int)$row->type_label];
			}
		});

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

		$list->setTitle('Dettes et créances en attente');

		return $list;
	}

	static public function setProject(?int $id_project, ?array $transactions = null, ?array $lines = null)
	{
		$db = DB::getInstance();

		if (null !== $id_project && !$db->test(Project::TABLE, 'id = ?', $id_project)) {
			throw new \InvalidArgumentException('Invalid project ID');
		}

		if (isset($transactions, $lines) || ($transactions === null && $lines === null)) {
			throw new \BadMethodCallException('Only one of transactions or lines should be set');
		}

		$selection = array_map('intval', $transactions ?? $lines);
		$where = sprintf($transactions ? 'id_transaction IN (%s)' : 'id IN (%s)', implode(', ', $selection));

		return $db->exec(sprintf('UPDATE acc_transactions_lines SET id_project = %s WHERE %s;',
			(int)$id_project ?: 'NULL', $where));
	}

	static public function listByType(int $year_id, ?int $type): DynamicList
	{
		$reverse = 1;

		$columns = Account::LIST_COLUMNS;
		unset($columns['line_label'], $columns['sum'], $columns['debit'], $columns['credit']);
		$columns['line_reference']['label'] = 'Réf. paiement';
		$columns['change']['select'] = sprintf('SUM(l.credit) * %d', $reverse);

Modified src/include/lib/Garradin/Accounting/Years.php from [dd4bd35675] to [f68afe4a33].

157
158
159
160
161
162
163

164
165
166
167
168
169
170

		$t = new Transaction;
		$t->type = $t::TYPE_ADVANCED;
		$t->id_year = $year->id();
		$t->label = 'Affectation automatique du résultat';
		$t->notes = 'Le résultat a été affecté automatiquement lors de la balance d\'ouverture';
		$t->date = $year->start_date;


		$sum = 0;

		if (!empty($balances[Account::TYPE_NEGATIVE_RESULT])) {
			$account = $balances[Account::TYPE_NEGATIVE_RESULT];

			$line = Line::create($account->id, abs($account->balance), 0);







>







157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

		$t = new Transaction;
		$t->type = $t::TYPE_ADVANCED;
		$t->id_year = $year->id();
		$t->label = 'Affectation automatique du résultat';
		$t->notes = 'Le résultat a été affecté automatiquement lors de la balance d\'ouverture';
		$t->date = $year->start_date;
		$t->addStatus($t::STATUS_OPENING_BALANCE);

		$sum = 0;

		if (!empty($balances[Account::TYPE_NEGATIVE_RESULT])) {
			$account = $balances[Account::TYPE_NEGATIVE_RESULT];

			$line = Line::create($account->id, abs($account->balance), 0);

Modified src/include/lib/Garradin/DynamicList.php from [04482bebe4] to [b3c71aaba9].

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

	public function SQL()
	{
		$start = ($this->page - 1) * $this->per_page;
		$columns = [];


		foreach ($this->columns as $alias => $properties) {
			// Skip columns that require a certain order (eg. calculating a running sum)
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}

			$select = array_key_exists('select', $properties) ? $properties['select'] : $alias;

			if (null === $select) {
				$select = 'NULL';

			}



			$columns[] = sprintf('%s AS %s', $select, $alias);
		}

		$columns = implode(', ', $columns);

		if (isset($this->columns[$this->order]['order'])) {
			$order = sprintf($this->columns[$this->order]['order'], $this->desc ? 'DESC' : 'ASC');
		}
		else {
			$order = $this->order;

			if (true === $this->desc) {
				$order .= ' DESC';
			}
		}

		$group = $this->group ? 'GROUP BY ' . $this->group : '';







>












|
<
<
|
>

>
>
|
<








|







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

	public function SQL()
	{
		$start = ($this->page - 1) * $this->per_page;
		$columns = [];
		$db = DB::getInstance();

		foreach ($this->columns as $alias => $properties) {
			// Skip columns that require a certain order (eg. calculating a running sum)
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}

			if (array_key_exists('select', $properties)) {


				$select = $properties['select'] ?? 'NULL';
				$columns[] = sprintf('%s AS %s', $select, $db->quoteIdentifier($alias));
			}
			else {
				$columns[] = $db->quoteIdentifier($alias);
			}

		}

		$columns = implode(', ', $columns);

		if (isset($this->columns[$this->order]['order'])) {
			$order = sprintf($this->columns[$this->order]['order'], $this->desc ? 'DESC' : 'ASC');
		}
		else {
			$order = $db->quoteIdentifiers($this->order);

			if (true === $this->desc) {
				$order .= ' DESC';
			}
		}

		$group = $this->group ? 'GROUP BY ' . $this->group : '';

Modified src/include/lib/Garradin/Email/Emails.php from [967d4a8042] to [95f634e297].

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\Email;

use Garradin\Config;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Plugin;
use Garradin\UserException;
use Garradin\Entities\Email\Email;
use Garradin\Entities\Users\User;
use Garradin\Users\DynamicFields;
use Garradin\UserTemplate\UserTemplate;
use Garradin\Web\Render\Render;
use Garradin\Web\Skeleton;

use const Garradin\{USE_CRON, MAIL_RETURN_PATH};
use const Garradin\{SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURITY};

use KD2\SMTP;
use KD2\Security;
use KD2\Mail_Message;
use KD2\DB\EntityManager as EM;








|








|







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\Email;

use Garradin\Config;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Plugins;
use Garradin\UserException;
use Garradin\Entities\Email\Email;
use Garradin\Entities\Users\User;
use Garradin\Users\DynamicFields;
use Garradin\UserTemplate\UserTemplate;
use Garradin\Web\Render\Render;
use Garradin\Web\Skeleton;

use const Garradin\{USE_CRON, MAIL_RETURN_PATH, DISABLE_EMAIL};
use const Garradin\{SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_SECURITY};

use KD2\SMTP;
use KD2\Security;
use KD2\Mail_Message;
use KD2\DB\EntityManager as EM;

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		if (!count($list)) {
			return;
		}

		$recipients = $list;
		unset($list);

		if (Plugin::fireSignal('email.queue.before', compact('context', 'recipients', 'sender', 'subject', 'content', 'render'))) {
			// queue handling was done by a plugin
			return;
		}

		$template = ($content instanceof UserTemplate) ? $content : null;
		$skel = null;
		$content_html = null;







|







112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
		if (!count($list)) {
			return;
		}

		$recipients = $list;
		unset($list);

		if (Plugins::fireSignal('email.queue.before', compact('context', 'recipients', 'sender', 'subject', 'content', 'render'))) {
			// queue handling was done by a plugin
			return;
		}

		$template = ($content instanceof UserTemplate) ? $content : null;
		$skel = null;
		$content_html = null;
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
					'recipient' => $to,
					'data'      => $variables,
					'context'   => $context,
					'from'      => $sender,
				]);
			}

			if (Plugin::fireSignal('email.queue.insert', compact('context', 'to', 'sender', 'subject', 'content', 'render', 'hash', 'content_html') + ['pgp_key' => $data['pgp_key'] ?? null])) {
				// queue insert was done by a plugin
				continue;
			}

			$st->bindValue(':sender', $sender);
			$st->bindValue(':subject', $subject);
			$st->bindValue(':context', $context);
			$st->bindValue(':recipient', $to);
			$st->bindValue(':recipient_pgp_key', $variables['pgp_key'] ?? null);
			$st->bindValue(':recipient_hash', $hash);
			$st->bindValue(':content', $content);
			$st->bindValue(':content_html', $content_html);
			$st->execute();

			$st->reset();
			$st->clear();
		}

		$db->commit();

		if (Plugin::fireSignal('email.queue.after', compact('context', 'recipients', 'sender', 'subject', 'content', 'render'))) {
			return;
		}

		// If no crontab is used, then the queue should be run now
		if (!USE_CRON) {
			self::runQueue();
		}







|




















|







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
					'recipient' => $to,
					'data'      => $variables,
					'context'   => $context,
					'from'      => $sender,
				]);
			}

			if (Plugins::fireSignal('email.queue.insert', compact('context', 'to', 'sender', 'subject', 'content', 'render', 'hash', 'content_html') + ['pgp_key' => $data['pgp_key'] ?? null])) {
				// queue insert was done by a plugin
				continue;
			}

			$st->bindValue(':sender', $sender);
			$st->bindValue(':subject', $subject);
			$st->bindValue(':context', $context);
			$st->bindValue(':recipient', $to);
			$st->bindValue(':recipient_pgp_key', $variables['pgp_key'] ?? null);
			$st->bindValue(':recipient_hash', $hash);
			$st->bindValue(':content', $content);
			$st->bindValue(':content_html', $content_html);
			$st->execute();

			$st->reset();
			$st->clear();
		}

		$db->commit();

		if (Plugins::fireSignal('email.queue.after', compact('context', 'recipients', 'sender', 'subject', 'content', 'render'))) {
			return;
		}

		// If no crontab is used, then the queue should be run now
		if (!USE_CRON) {
			self::runQueue();
		}
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597

	static public function sendMessage(int $context, Mail_Message $message): void
	{
		if (DISABLE_EMAIL) {
			return;
		}

		$email_sent_via_plugin = Plugin::fireSignal('email.send.before', compact('context', 'message'));

		if ($email_sent_via_plugin) {
			return;
		}

		if (SMTP_HOST) {
			$const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY);
			$secure = constant($const);

			$smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure);
			$smtp->send($message);
		}
		else {
			$message->send();
		}

		Plugin::fireSignal('email.send.after', compact('context', 'message'));
	}

	/**
	 * Handle a bounce message
	 * @param  string $raw_message Raw MIME message from SMTP
	 */
	static public function handleBounce(string $raw_message): ?array
	{
		$message = new Mail_Message;
		$message->parse($raw_message);

		$return = $message->identifyBounce();

		if (Plugin::fireSignal('email.bounce', compact('message', 'return', 'raw_message'))) {
			return null;
		}

		if (!$return) {
			return null;
		}








|
















|













|







552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597

	static public function sendMessage(int $context, Mail_Message $message): void
	{
		if (DISABLE_EMAIL) {
			return;
		}

		$email_sent_via_plugin = Plugins::fireSignal('email.send.before', compact('context', 'message'));

		if ($email_sent_via_plugin) {
			return;
		}

		if (SMTP_HOST) {
			$const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY);
			$secure = constant($const);

			$smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure);
			$smtp->send($message);
		}
		else {
			$message->send();
		}

		Plugins::fireSignal('email.send.after', compact('context', 'message'));
	}

	/**
	 * Handle a bounce message
	 * @param  string $raw_message Raw MIME message from SMTP
	 */
	static public function handleBounce(string $raw_message): ?array
	{
		$message = new Mail_Message;
		$message->parse($raw_message);

		$return = $message->identifyBounce();

		if (Plugins::fireSignal('email.bounce', compact('message', 'return', 'raw_message'))) {
			return null;
		}

		if (!$return) {
			return null;
		}

625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
		$return = compact('recipient', 'type', 'message');
		$email = self::getOrCreateEmail($return['recipient']);

		if (!$email) {
			return null;
		}

		Plugin::fireSignal('email.bounce', compact('email', 'return'));
		$email->hasFailed($return);
		$email->save();

		return $return;
	}









|







625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
		$return = compact('recipient', 'type', 'message');
		$email = self::getOrCreateEmail($return['recipient']);

		if (!$email) {
			return null;
		}

		Plugins::fireSignal('email.bounce', compact('email', 'return'));
		$email->hasFailed($return);
		$email->save();

		return $return;
	}


Modified src/include/lib/Garradin/Entities/Accounting/Account.php from [75230895b2] to [0d077ef548].

757
758
759
760
761
762
763
764
765














766





























767
768
769
770
771
772
773
774
775
776



777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
				$prev = $line->csv;
			}
		}

		return $lines;
	}

	public function getDepositJournal(int $year_id, array $checked = []): \Generator
	{














		$res = DB::getInstance()->iterate('SELECT l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, l.reconciled, l.id AS id_line, l.id_account





























			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			WHERE t.id_year = ? AND l.id_account = ? AND l.credit = 0 AND NOT (t.status & ?)
			ORDER BY t.date, t.id;',
			$year_id, $this->id(), Transaction::STATUS_DEPOSIT);

		$sum = 0;

		foreach ($res as $row) {
			$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);



			$sum += ($row->credit - $row->debit);
			$row->running_sum = $sum;
			$row->checked = array_key_exists($row->id, $checked);
			yield $row;
		}
	}

	public function countDepositJournal(int $year_id): int
	{
		 return DB::getInstance()->firstColumn('SELECT COUNT(*)
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			WHERE t.id_year = ? AND l.id_account = ? AND l.credit = 0 AND NOT (t.status & ?)
			ORDER BY t.date, t.id;',
			$year_id, $this->id(), Transaction::STATUS_DEPOSIT);
	}

	public function getDepositMissingBalance(int $year_id): int
	{
		$deposit_balance = DB::getInstance()->firstColumn('SELECT SUM(l.debit)
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction







|

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


<
|
<
<
>
>
>



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







757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812

813
814

815


816
817
818
819
820
821

822
823
824








825
826
827
828
829
830
831
				$prev = $line->csv;
			}
		}

		return $lines;
	}

	public function getDepositJournal(int $year_id, array $checked = []): DynamicList
	{
		$columns = [
			'id' => [
				'label' => 'Num.',
				'select' => 't.id',
			],
			'date' => [
				'select' => 't.date',
				'label' => 'Date',
				'order' => 't.date %s, t.id %1$s',
			],
			'reference' => [
				'select' => 't.reference',
				'label' => 'Réf. écriture',
			],
			'line_reference' => [
				'select' => 'l.reference',
				'label' => 'Réf. paiement',
			],
			'label' => [
				'label' => 'Libellé',
				'select' => 't.label',
			],
			'amount' => [
				'label' => 'Montant',
				'select' => 'l.debit',
			],
			'running_sum' => [
				'label' => 'Solde cumulé',
				'only_with_order' => 'date',
				'select' => null,
			],
			'credit' => [
				'select' => 'l.credit',
			],
			'debit' => [
				'select' => 'l.debit',
			],
			'id_account' => [
				'select' => 'l.id_account',
			],
			'id_line' => [
				'select' => 'l.id',
			],
		];

		$tables = 'acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction';
		$conditions = sprintf('t.id_year = %d AND l.id_account = %d AND l.credit = 0 AND NOT (t.status & %d)',

			$year_id, $this->id(), Transaction::STATUS_DEPOSIT);


		$list = new DynamicList($columns, $tables, $conditions);


		$list->setPageSize(null);
		$list->orderBy('date', true);
		$list->setModifier(function (&$row) use (&$sum, $checked) {
			$sum += ($row->credit - $row->debit);
			$row->running_sum = $sum;
			$row->checked = array_key_exists($row->id, $checked);

		});

		return $list;








	}

	public function getDepositMissingBalance(int $year_id): int
	{
		$deposit_balance = DB::getInstance()->firstColumn('SELECT SUM(l.debit)
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction

Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [a5e9d959a8] to [0a0f54d4f9].

14
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
use Garradin\Users\DynamicFields;

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

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Projects;

use Garradin\ValidationException;

class Transaction extends Entity
{
	const NAME = 'Écriture';
	const PRIVATE_URL = '!acc/transactions/details.php?id=%d';

	const TABLE = 'acc_transactions';

	const TYPE_ADVANCED = 0;
	const TYPE_REVENUE = 1;
	const TYPE_EXPENSE = 2;
	const TYPE_TRANSFER = 3;
	const TYPE_DEBT = 4;
	const TYPE_CREDIT = 5;

	const STATUS_WAITING = 1;
	const STATUS_PAID = 2;
	const STATUS_DEPOSIT = 4;
	const STATUS_ERROR = 8;


	const STATUS_NAMES = [
		1 => 'En attente de règlement',
		2 => 'Réglé',
		4 => 'Déposé en banque',
	];








>




















>







14
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
use Garradin\Users\DynamicFields;

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

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Projects;
use Garradin\Accounting\Years;
use Garradin\ValidationException;

class Transaction extends Entity
{
	const NAME = 'Écriture';
	const PRIVATE_URL = '!acc/transactions/details.php?id=%d';

	const TABLE = 'acc_transactions';

	const TYPE_ADVANCED = 0;
	const TYPE_REVENUE = 1;
	const TYPE_EXPENSE = 2;
	const TYPE_TRANSFER = 3;
	const TYPE_DEBT = 4;
	const TYPE_CREDIT = 5;

	const STATUS_WAITING = 1;
	const STATUS_PAID = 2;
	const STATUS_DEPOSIT = 4;
	const STATUS_ERROR = 8;
	const STATUS_OPENING_BALANCE = 16;

	const STATUS_NAMES = [
		1 => 'En attente de règlement',
		2 => 'Réglé',
		4 => 'Déposé en banque',
	];

503
504
505
506
507
508
509





510
511
512
513
514
515
516
		}

		return true;
	}

	public function assertCanBeModified(): void
	{





		// We allow to change notes and id_project in a locked transaction
		if (!$this->canSaveChanges()) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été verrouillée');
		}

		$db = DB::getInstance();








>
>
>
>
>







505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
		}

		return true;
	}

	public function assertCanBeModified(): void
	{
		// Allow to change the status
		if (count($this->_modified) === 1 && array_key_exists('status', $this->_modified)) {
			return;
		}

		// We allow to change notes and id_project in a locked transaction
		if (!$this->canSaveChanges()) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été verrouillée');
		}

		$db = DB::getInstance();

655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674








675
676
677
678
679
680
681

	public function selfCheck(): void
	{
		$db = DB::getInstance();

		$this->assert(!empty($this->id_year), 'L\'ID de l\'exercice doit être renseigné.');

		$this->assert(trim((string)$this->label) !== '', 'Le champ libellé ne peut rester vide.');
		$this->assert(strlen($this->label) <= 200, 'Le champ libellé ne peut faire plus de 200 caractères.');
		$this->assert(!isset($this->reference) || strlen($this->reference) <= 200, 'Le champ numéro de pièce comptable ne peut faire plus de 200 caractères.');
		$this->assert(!isset($this->notes) || strlen($this->notes) <= 2000, 'Le champ remarques ne peut faire plus de 2000 caractères.');
		$this->assert(!empty($this->date), 'Le champ date ne peut rester vide.');

		$this->assert(null !== $this->id_year, 'Aucun exercice spécifié.');
		$this->assert(array_key_exists($this->type, self::TYPES_NAMES), 'Type d\'écriture inconnu : ' . $this->type);
		$this->assert(null === $this->id_creator || $db->test('users', 'id = ?', $this->id_creator), 'Le membre créateur de l\'écriture n\'existe pas ou plus');

		$is_in_year = $db->test(Year::TABLE, 'id = ? AND start_date <= ? AND end_date >= ?', $this->id_year, $this->date->format('Y-m-d'), $this->date->format('Y-m-d'));

		$this->assert($is_in_year, 'La date ne correspond pas à l\'exercice sélectionné : ' . $this->date->format('d/m/Y'));









		$total = 0;

		$lines = $this->getLines();
		$count = count($lines);

		$this->assert($count > 0, 'Cette écriture ne comporte aucune ligne.');







|











|
>
>
>
>
>
>
>
>







662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696

	public function selfCheck(): void
	{
		$db = DB::getInstance();

		$this->assert(!empty($this->id_year), 'L\'ID de l\'exercice doit être renseigné.');

		$this->assert(!empty($this->label) && trim((string)$this->label) !== '', 'Le champ libellé ne peut rester vide.');
		$this->assert(strlen($this->label) <= 200, 'Le champ libellé ne peut faire plus de 200 caractères.');
		$this->assert(!isset($this->reference) || strlen($this->reference) <= 200, 'Le champ numéro de pièce comptable ne peut faire plus de 200 caractères.');
		$this->assert(!isset($this->notes) || strlen($this->notes) <= 2000, 'Le champ remarques ne peut faire plus de 2000 caractères.');
		$this->assert(!empty($this->date), 'Le champ date ne peut rester vide.');

		$this->assert(null !== $this->id_year, 'Aucun exercice spécifié.');
		$this->assert(array_key_exists($this->type, self::TYPES_NAMES), 'Type d\'écriture inconnu : ' . $this->type);
		$this->assert(null === $this->id_creator || $db->test('users', 'id = ?', $this->id_creator), 'Le membre créateur de l\'écriture n\'existe pas ou plus');

		$is_in_year = $db->test(Year::TABLE, 'id = ? AND start_date <= ? AND end_date >= ?', $this->id_year, $this->date->format('Y-m-d'), $this->date->format('Y-m-d'));

		if (!$is_in_year) {
			$year = Years::get($this->id_year);
			throw new ValidationException(sprintf('La date (%s) de l\'écriture ne correspond pas à l\'exercice "%s" : la date doit être entre le %s et le %s.',
				Utils::shortDate($this->date),
				$year->label ?? '',
				Utils::shortDate($year->start_date),
				Utils::shortDate($year->end_date)
			));
		}

		$total = 0;

		$lines = $this->getLines();
		$count = count($lines);

		$this->assert($count > 0, 'Cette écriture ne comporte aucune ligne.');
946
947
948
949
950
951
952

953
954
955
956
957
958
959
			$source = $_POST;
		}

		$this->label = 'Balance d\'ouverture';
		$this->date = $year->start_date;
		$this->id_year = $year->id();
		$this->type = self::TYPE_ADVANCED;


		$this->importFromNewForm($source);

		$diff = $this->getLinesCreditSum() - $this->getLinesDebitSum();

		if (!$diff) {
			return;







>







961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
			$source = $_POST;
		}

		$this->label = 'Balance d\'ouverture';
		$this->date = $year->start_date;
		$this->id_year = $year->id();
		$this->type = self::TYPE_ADVANCED;
		$this->addStatus(self::STATUS_OPENING_BALANCE);

		$this->importFromNewForm($source);

		$diff = $this->getLinesCreditSum() - $this->getLinesDebitSum();

		if (!$diff) {
			return;

Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [264d89b87d] to [19edb1963d].

240
241
242
243
244
245
246



247











				}
			}
		}

		return $out;
	}




}


















>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
				}
			}
		}

		return $out;
	}

	public function hasOpeningBalance(): bool
	{
		return DB::getInstance()->test(Transaction::TABLE, 'id_year = ? AND status & ?', $this->id(), Transaction::STATUS_OPENING_BALANCE);
	}

	public function deleteOpeningBalance(): void
	{
		$em = EntityManager::getInstance(Transaction::class);
		$list = $em->iterate('SELECT * FROM @TABLE WHERE id_year = ? AND status & ?', $this->id(), Transaction::STATUS_OPENING_BALANCE);

		foreach ($list as $t) {
			$t->delete();
		}
	}
}

Modified src/include/lib/Garradin/Entities/Email/Email.php from [ceddb31aa6] to [36e29843f4].

136
137
138
139
140
141
142





143
144
145
146
147
148
149
150
151
152
153
				if ($percent > 90) {
					throw new UserException(sprintf('Adresse e-mail invalide : avez-vous fait une erreur, par exemple "%s" à la place de "%s" ?', $host, $common_domain));
				}
			}

			throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.');
		}






		getmxrr($host, $mx_list);

		if (!count($mx_list)) {
			throw new UserException('Adresse e-mail invalide (le domaine indiqué n\'a pas de service e-mail) : vérifiez que vous n\'avez pas fait une faute de frappe.');
		}

		foreach ($mx_list as $mx) {
  			if (preg_match(self::BLACKLIST_MANUAL_VALIDATION_MX, $mx)) {
				throw new UserException('Adresse e-mail invalide : impossible d\'envoyer des mails à un service (de type mailinblack ou spamenmoins) qui demande une validation manuelle de l\'expéditeur. Merci de choisir une autre adresse e-mail.');
			}







>
>
>
>
>



|







136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
				if ($percent > 90) {
					throw new UserException(sprintf('Adresse e-mail invalide : avez-vous fait une erreur, par exemple "%s" à la place de "%s" ?', $host, $common_domain));
				}
			}

			throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.');
		}

		// Windows does not support MX lookups
		if (PHP_OS_FAMILY == 'Windows') {
			return;
		}

		getmxrr($host, $mx_list);

		if (empty($mx_list)) {
			throw new UserException('Adresse e-mail invalide (le domaine indiqué n\'a pas de service e-mail) : vérifiez que vous n\'avez pas fait une faute de frappe.');
		}

		foreach ($mx_list as $mx) {
  			if (preg_match(self::BLACKLIST_MANUAL_VALIDATION_MX, $mx)) {
				throw new UserException('Adresse e-mail invalide : impossible d\'envoyer des mails à un service (de type mailinblack ou spamenmoins) qui demande une validation manuelle de l\'expéditeur. Merci de choisir une autre adresse e-mail.');
			}

Modified src/include/lib/Garradin/Entities/Files/File.php from [7450a23eb6] to [89f8d9569c].

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

namespace Garradin\Entities\Files;

use KD2\Graphics\Image;
use KD2\Graphics\Blob;
use KD2\DB\EntityManager as EM;
use KD2\Security;

use Garradin\Config;
use Garradin\DB;
use Garradin\Entity;
use Garradin\Plugin;
use Garradin\Template;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Users\Session;
use Garradin\Static_Cache;
use Garradin\Utils;
use Garradin\Entities\Web\Page;












|







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

namespace Garradin\Entities\Files;

use KD2\Graphics\Image;
use KD2\Graphics\Blob;
use KD2\DB\EntityManager as EM;
use KD2\Security;

use Garradin\Config;
use Garradin\DB;
use Garradin\Entity;
use Garradin\Plugins;
use Garradin\Template;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Users\Session;
use Garradin\Static_Cache;
use Garradin\Utils;
use Garradin\Entities\Web\Page;
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
		Files::callStorage('checkLock');

		Web_Cache::delete($this->uri());

		// Delete actual file content
		Files::callStorage('delete', $this);

		Plugin::fireSignal('files.delete', ['file' => $this]);

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








|







198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
		Files::callStorage('checkLock');

		Web_Cache::delete($this->uri());

		// Delete actual file content
		Files::callStorage('delete', $this);

		Plugins::fireSignal('files.delete', ['file' => $this]);

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

263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
				throw new UserException(sprintf('Impossible de renommer "%s" vers "%s"', $this->path, $new_path));
			}
		}

		Files::ensureDirectoryExists(Utils::dirname($new_path));
		$return = Files::callStorage('move', $this, $new_path);

		Plugin::fireSignal('files.move', ['file' => $this, 'new_path' => $new_path]);

		return $return;
	}

	/**
	 * Copy the current file to a new location
	 * @param  string $target Target path







|







263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
				throw new UserException(sprintf('Impossible de renommer "%s" vers "%s"', $this->path, $new_path));
			}
		}

		Files::ensureDirectoryExists(Utils::dirname($new_path));
		$return = Files::callStorage('move', $this, $new_path);

		Plugins::fireSignal('files.move', ['file' => $this, 'new_path' => $new_path]);

		return $return;
	}

	/**
	 * Copy the current file to a new location
	 * @param  string $target Target path
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
			fclose($pointer);
		}

		if (!$return) {
			throw new UserException('Le fichier n\'a pas pu être enregistré.');
		}

		Plugin::fireSignal('files.store', ['file' => $this]);

		if ($index_search && $content) {
			$this->indexForSearch($content);
		}
		else {
			$this->removeFromSearch();
		}







|







403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
			fclose($pointer);
		}

		if (!$return) {
			throw new UserException('Le fichier n\'a pas pu être enregistré.');
		}

		Plugins::fireSignal('files.store', ['file' => $this]);

		if ($index_search && $content) {
			$this->indexForSearch($content);
		}
		else {
			$this->removeFromSearch();
		}

Modified src/include/lib/Garradin/Entities/Module.php from [df8c6b5f42] to [2994320732].

12
13
14
15
16
17
18
19
20

21

22
23
24
25
26
27
28

use const Garradin\{ROOT, WWW_URL};

class Module extends Entity
{
	const ROOT = File::CONTEXT_SKELETON . '/modules';
	const DIST_ROOT = ROOT . '/skel-dist/modules';
	const META_FILE = 'module.json';


	const CONFIG_TEMPLATE = 'config.html';


	// Snippets, don't forget to create alias constant in UserTemplate\Modules class
	const SNIPPET_TRANSACTION = 'snippets/transaction_details.html';
	const SNIPPET_USER = 'snippets/user_details.html';
	const SNIPPET_HOME_BUTTON = 'snippets/home_button.html';

	const SNIPPETS = [







|
|
>
|
>







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

use const Garradin\{ROOT, WWW_URL};

class Module extends Entity
{
	const ROOT = File::CONTEXT_SKELETON . '/modules';
	const DIST_ROOT = ROOT . '/skel-dist/modules';
	const META_FILE = 'module.ini';
	const ICON_FILE = 'icon.svg';
	const README_FILE = 'README.md';
	const CONFIG_FILE = 'config.html';
	const INDEX_FILE = 'index.html';

	// Snippets, don't forget to create alias constant in UserTemplate\Modules class
	const SNIPPET_TRANSACTION = 'snippets/transaction_details.html';
	const SNIPPET_USER = 'snippets/user_details.html';
	const SNIPPET_HOME_BUTTON = 'snippets/home_button.html';

	const SNIPPETS = [
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
	/**
	 * Directory name
	 */
	protected string $name;

	protected string $label;
	protected ?string $description;

	protected ?string $config;





	protected bool $enabled;

	public function selfCheck(): void
	{
		$this->assert(preg_match('/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/', $this->name), 'Nom unique de module invalide: ' . $this->name);
		$this->assert(trim($this->label) !== '', 'Le libellé ne peut rester vide');
	}

	/**
	 * Fills information from module.json file
	 */
	public function updateFromJSON(bool $loadConfig = false): bool
	{
		if ($file = Files::get($this->path(self::META_FILE))) {
			$json = $file->fetch();
		}
		elseif (file_exists($this->distPath(self::META_FILE))) {
			$json = file_get_contents($this->distPath(self::META_FILE));
		}
		else {
			return false;
		}

		$json = json_decode($json);

		if (!isset($json->label)) {
			return false;
		}
		$this->set('label', $json->label);
		$this->set('description', $json->description ?? null);
		if ($loadConfig) {
			$this->setConfig(get_object_vars($json));
		}

		return true;
	}

	public function setConfig(array $config): void
	{
		$tmp = [];
		foreach($config as $configKey => $value) {
			if (is_string($value) && $configKey !== 'label' && $configKey !== 'description') // black-list would be welcomed




				$tmp[$configKey] = $value;

		}
		$this->set('config', json_encode($tmp));

	}

	public function updateTemplates(): void
	{
		$check = self::SNIPPETS + [self::CONFIG_TEMPLATE => 'Config'];
		$templates = [];
		$db = DB::getInstance();

		$db->begin();
		$db->delete('modules_templates', 'id_module = ' . (int)$this->id());

		foreach ($check as $file => $label) {
			if (Files::exists($this->path($file)) || file_exists($this->distPath($file))) {
				$templates[] = $file;
				$db->insert('modules_templates', ['id_module' => $this->id(), 'name' => $file]);
			}
		}

		$db->commit();
	}

	public function icon_url(): ?string
	{
		if (!$this->hasFile('icon.svg')) {
			return null;
		}

		return $this->url('icon.svg');
	}

	public function path(string $file = null): string
	{
		return self::ROOT . '/' . $this->name . ($file ? '/' . $file : '');
	}








>
|
>
>
>
>
>









|

|

|
|


|





|

|


|
<
<
|
|
>
|
|

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




|


















|



|







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
	/**
	 * Directory name
	 */
	protected string $name;

	protected string $label;
	protected ?string $description;
	protected ?string $author;
	protected ?string $author_url;
	protected ?string $restrict_section;
	protected ?int $restrict_level;
	protected bool $home_button;
	protected bool $menu;
	protected ?\stdClass $config;
	protected bool $enabled;

	public function selfCheck(): void
	{
		$this->assert(preg_match('/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/', $this->name), 'Nom unique de module invalide: ' . $this->name);
		$this->assert(trim($this->label) !== '', 'Le libellé ne peut rester vide');
	}

	/**
	 * Fills information from module.ini file
	 */
	public function updateFromINI(bool $use_local = true): bool
	{
		if ($use_local && ($file = Files::get($this->path(self::META_FILE)))) {
			$ini = $file->fetch();
		}
		elseif (file_exists($this->distPath(self::META_FILE))) {
			$ini = file_get_contents($this->distPath(self::META_FILE));
		}
		else {
			return false;
		}

		$ini = @parse_ini_string($ini, false, \INI_SCANNER_TYPED);

		if (empty($ini)) {
			return false;
		}



		$ini = (object) $ini;

		if (!isset($ini->name)) {
			return false;
		}



		$this->set('label', $ini->name);

		$this->set('description', $ini->description ?? null);
		$this->set('author', $ini->author ?? null);
		$this->set('author_url', $ini->author_url ?? null);
		$this->set('home_button', !empty($ini->home_button));
		$this->set('menu', !empty($ini->menu));
		$this->set('restrict_section', $ini->restrict_section ?? null);
		$this->set('restrict_level', isset($ini->restrict_section, $ini->restrict_level, Session::ACCESS_WORDS[$ini->restrict_level]) ? Session::ACCESS_WORDS[$ini->restrict_level] : null);


		return true;
	}

	public function updateTemplates(): void
	{
		$check = self::SNIPPETS + [self::CONFIG_FILE => 'Config'];
		$templates = [];
		$db = DB::getInstance();

		$db->begin();
		$db->delete('modules_templates', 'id_module = ' . (int)$this->id());

		foreach ($check as $file => $label) {
			if (Files::exists($this->path($file)) || file_exists($this->distPath($file))) {
				$templates[] = $file;
				$db->insert('modules_templates', ['id_module' => $this->id(), 'name' => $file]);
			}
		}

		$db->commit();
	}

	public function icon_url(): ?string
	{
		if (!$this->hasFile(self::ICON_FILE)) {
			return null;
		}

		return $this->url(self::ICON_FILE);
	}

	public function path(string $file = null): string
	{
		return self::ROOT . '/' . $this->name . ($file ? '/' . $file : '');
	}

145
146
147
148
149
150
151





152
153
154
155





156
157
158
159
160
161
162
163
164
165
166
167
		return false;
	}

	public function hasDist(): bool
	{
		return file_exists($this->distPath());
	}






	public function hasConfig(): bool
	{
		return DB::getInstance()->test('modules_templates', 'id_module = ? AND name = ?', $this->id(), self::CONFIG_TEMPLATE);





	}

	public function canDelete(): bool
	{
		return $this->dir() ? true : false;
	}

	public function delete(): bool
	{
		$dir = $this->dir();

		if ($dir) {







>
>
>
>
>



|
>
>
>
>
>




|







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

	public function hasDist(): bool
	{
		return file_exists($this->distPath());
	}

	public function hasLocal(): bool
	{
		return Files::exists($this->path());
	}

	public function hasConfig(): bool
	{
		return DB::getInstance()->test('modules_templates', 'id_module = ? AND name = ?', $this->id(), self::CONFIG_FILE);
	}

	public function hasData(): bool
	{
		return DB::getInstance()->test('sqlite_master', 'type = \'table\' AND name = ?', sprintf('modules_data_%s', $this->name));
	}

	public function canDelete(): bool
	{
		return !empty($this->config) || $this->hasLocal() || $this->hasData();
	}

	public function delete(): bool
	{
		$dir = $this->dir();

		if ($dir) {
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
		if (!preg_match('!^(?:snippets/)?[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $file)) {
			throw new \InvalidArgumentException('Invalid skeleton name');
		}
	}

	public function template(string $file)
	{
		if ($file == self::CONFIG_TEMPLATE) {
			Session::getInstance()->requireAccess(Session::SECTION_CONFIG, Session::ACCESS_ADMIN);
		}

		$this->validateFileName($file);

		$ut = new UserTemplate('modules/' . $this->name . '/' . $file);
		$moduleVars = array_merge($this->asArray(false), ['url' => $this->url()]);
		$moduleVars['config'] = json_decode($this->config, true);
		$ut->assign('module', $moduleVars);

		return $ut;
	}

	public function fetch(string $file, array $params): string
	{
		$ut = $this->template($file);
		$ut->assignArray($params);
		return $ut->fetch();
	}
}







|






|
<
<











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
		if (!preg_match('!^(?:snippets/)?[\w\d_-]+(?:\.[\w\d_-]+)*$!i', $file)) {
			throw new \InvalidArgumentException('Invalid skeleton name');
		}
	}

	public function template(string $file)
	{
		if ($file == self::CONFIG_FILE) {
			Session::getInstance()->requireAccess(Session::SECTION_CONFIG, Session::ACCESS_ADMIN);
		}

		$this->validateFileName($file);

		$ut = new UserTemplate('modules/' . $this->name . '/' . $file);
		$ut->assign('module', array_merge($this->asArray(false), ['url' => $this->url()]));



		return $ut;
	}

	public function fetch(string $file, array $params): string
	{
		$ut = $this->template($file);
		$ut->assignArray($params);
		return $ut->fetch();
	}
}

Added src/include/lib/Garradin/Entities/Plugin.php version [b18b04117d].



































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
<?php

namespace Garradin\Entities;

use Garradin\Entity;
use Garradin\DB;
use Garradin\Plugins;
use Garradin\Template;
use Garradin\UserException;
use Garradin\Files\Files;
use Garradin\UserTemplate\UserTemplate;
use Garradin\Users\Session;
use Garradin\Web\Render\Parsedown;

use Garradin\Entities\Files\File;

use const Garradin\{PLUGINS_ROOT, WWW_URL, ROOT, ADMIN_URL};

class Plugin extends Entity
{
	const META_FILE = 'plugin.ini';
	const CONFIG_FILE = 'admin/config.php';
	const INDEX_FILE = 'admin/index.php';
	const ICON_FILE = 'admin/icon.svg';
	const INSTALL_FILE = 'install.php';
	const UPGRADE_FILE = 'upgrade.php';
	const UNINSTALL_FILE = 'uninstall.php';
	const README_FILE = 'admin/README.md';

	const PROTECTED_FILES = [
		self::META_FILE,
		self::INSTALL_FILE,
		self::UPGRADE_FILE,
		self::UNINSTALL_FILE,
	];

	const MIME_TYPES = [
		'css'  => 'text/css',
		'gif'  => 'image/gif',
		'htm'  => 'text/html',
		'html' => 'text/html',
		'ico'  => 'image/x-ico',
		'jpe'  => 'image/jpeg',
		'jpg'  => 'image/jpeg',
		'jpeg' => 'image/jpeg',
		'js'   => 'application/javascript',
		'pdf'  => 'application/pdf',
		'png'  => 'image/png',
		'xml'  => 'text/xml',
		'svg'  => 'image/svg+xml',
		'webp' => 'image/webp',
		'md'   => 'text/x-markdown',
	];

	const TABLE = 'plugins';

	protected ?int $id;

	/**
	 * Directory name
	 */
	protected string $name;

	protected string $label;
	protected string $version;

	protected ?string $description;
	protected ?string $author;
	protected ?string $author_url;

	protected bool $home_button;
	protected bool $menu;
	protected ?string $restrict_section;
	protected ?int $restrict_level;

	protected ?\stdClass $config;
	protected bool $enabled;

	protected ?string $_broken_message = null;

	public function hasCode(): bool
	{
		return Plugins::exists($this->name);
	}

	public function selfCheck(): void
	{
		$this->assert(preg_match('/^' . Plugins::NAME_REGEXP . '$/', $this->name), 'Nom unique d\'extension invalide: ' . $this->name);
		$this->assert(isset($this->label) && trim($this->label) !== '', sprintf('%s : le nom de l\'extension ("name") ne peut rester vide', $this->name));
		$this->assert(isset($this->label) && trim($this->version) !== '', sprintf('%s : la version ne peut rester vide', $this->name));

		if ($this->hasCode() || $this->enabled) {
			$this->assert(!$this->menu || $this->hasFile(self::INDEX_FILE), 'Le fichier admin/index.php n\'existe pas alors que la directive "menu" est activée.');
			$this->assert(!$this->home_button || $this->hasFile(self::INDEX_FILE), 'Le fichier admin/index.php n\'existe pas alors que la directive "home_button" est activée.');
			$this->assert(!$this->home_button || $this->hasFile(self::ICON_FILE), 'Le fichier admin/icon.svg n\'existe pas alors que la directive "home_button" est activée.');
		}
	}

	public function setBrokenMessage(string $str)
	{
		$this->_broken_message = $str;
	}

	public function getBrokenMessage(): ?string
	{
		return $this->_broken_message;
	}

	/**
	 * Fills information from plugin.ini file
	 */
	public function updateFromINI(): bool
	{
		$ini = parse_ini_file($this->path(self::META_FILE), false, \INI_SCANNER_TYPED);

		if (empty($ini)) {
			return false;
		}

		$ini = (object) $ini;

		if (!isset($ini->name)) {
			return false;
		}

		$this->assert(empty($ini->min_version) || version_compare(\Garradin\garradin_version(), $ini->min_version, '>='), sprintf('L\'extension "%s" nécessite Paheko version %s ou supérieure.', $this->name, $ini->min_version));

		$this->set('label', $ini->name);
		$this->set('version', $ini->version);
		$this->set('description', $ini->description ?? null);
		$this->set('author', $ini->author ?? null);
		$this->set('author_url', $ini->author_url ?? null);
		$this->set('home_button', !empty($ini->home_button));
		$this->set('menu', !empty($ini->menu));
		$this->set('restrict_section', $ini->restrict_section ?? null);
		$this->set('restrict_level', isset($ini->restrict_section, $ini->restrict_level, Session::ACCESS_WORDS[$ini->restrict_level]) ? Session::ACCESS_WORDS[$ini->restrict_level] : null);

		return true;
	}

	public function icon_url(): ?string
	{
		if (!$this->hasFile(self::ICON_FILE)) {
			return null;
		}

		return $this->url(self::ICON_FILE);
	}

	public function path(string $file = null): string
	{
		return Plugins::getPath($this->name) . ($file ? '/' . $file : '');
	}

	public function hasFile(string $file): bool
	{
		return file_exists($this->path($file));
	}

	public function hasConfig(): bool
	{
		return $this->hasFile(self::CONFIG_FILE);
	}

	public function url(string $file = '', array $params = null)
	{
		if (null !== $params) {
			$params = '?' . http_build_query($params);
		}

		if (substr($file, 0, 6) == 'admin/') {
			$url = ADMIN_URL;
			$file = substr($file, 6);
		}
		else {
			$url = WWW_URL;
		}

		return sprintf('%sp/%s/%s%s', $url, $this->name, $file, $params);
	}

	public function getConfig(string $key = null)
	{
		if (is_null($key)) {
			return $this->config;
		}

		if (property_exists($this->config, $key)) {
			return $this->config->$key;
		}

		return null;
	}

	public function setConfigProperty(string $key, $value = null)
	{
		if (null === $this->config) {
			$this->config = new \stdClass;
		}

		if (is_null($value)) {
			unset($this->config->$key);
		}
		else {
			$this->config->$key = $value;
		}

		$this->_modified['config'] = true;
	}

	public function setConfig(\stdClass $config)
	{
		$this->config = $config;
		$this->_modified['config'] = true;
	}

	/**
	 * Associer un signal à un callback du plugin
	 * @param  string $signal   Nom du signal (par exemple boucle.agenda pour la boucle de type AGENDA)
	 * @param  mixed  $callback Callback, sous forme d'un nom de fonction ou de méthode statique
	 * @return boolean TRUE
	 */
	public function registerSignal(string $signal, callable $callback): void
	{
		$callable_name = '';

		if (!is_callable($callback, true, $callable_name) || !is_string($callable_name))
		{
			throw new \LogicException('Le callback donné n\'est pas valide.');
		}

		// pour empêcher d'appeler des méthodes de Garradin après un import de base de données "hackée"
		if (strpos($callable_name, 'Garradin\\Plugin\\') !== 0)
		{
			throw new \LogicException('Le callback donné n\'utilise pas le namespace Garradin\\Plugin : ' . $callable_name);
		}

		$db = DB::getInstance();

		$callable_name = str_replace('Garradin\\Plugin\\', '', $callable_name);

		$db->preparedQuery('INSERT OR REPLACE INTO plugins_signals VALUES (?, ?, ?);', [$signal, $this->name, $callable_name]);
	}

	public function unregisterSignal(string $signal): void
	{
		DB::getInstance()->preparedQuery('DELETE FROM plugins_signals WHERE plugin = ? AND signal = ?;', [$this->name, $signal]);
	}

	public function delete(): bool
	{
		if ($this->hasFile(self::UNINSTALL_FILE)) {
			$this->call(self::UNINSTALL_FILE, true);
		}

		$db = DB::getInstance();
		$db->delete('plugins_signals', 'plugin = ?', $this->name);
		return parent::delete();
	}

	/**
	 * Renvoie TRUE si le plugin a besoin d'être mis à jour
	 * (si la version notée dans la DB est différente de la version notée dans paheko_plugin.ini)
	 * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon
	 */
	public function needUpgrade(): bool
	{
		$infos = (object) parse_ini_file($this->path(self::META_FILE), false);

		if (version_compare($this->version, $infos->version, '!=')) {
			return true;
		}

		return false;
	}

	/**
	 * Mettre à jour le plugin
	 * Appelle le fichier upgrade.php dans l'archive si celui-ci existe.
	 */
	public function upgrade(): void
	{
		$this->updateFromINI();

		if ($this->hasFile(self::UPGRADE_FILE)) {
			$this->call(self::UPGRADE_FILE, true);
		}

		$this->save();
	}

	public function oldVersion(): ?string
	{
		return $this->getModifiedProperty('version');
	}

	public function call(string $file, bool $allow_protected = false): void
	{
		$file = ltrim($file, './');

		if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file)) {
			throw new \UnexpectedValueException('Chemin de fichier incorrect.');
		}

		if (!$allow_protected && in_array($file, self::PROTECTED_FILES)) {
			throw new UserException('Le fichier ' . $file . ' ne peut être appelé par cette méthode.');
		}

		$path = $this->path($file);

		if (!file_exists($path)) {
			throw new UserException(sprintf('Le fichier "%s" n\'existe pas dans le plugin "%s"', $file, $this->name));
		}

		if (is_dir($path)) {
			throw new UserException(sprintf('Sécurité : impossible de lister le répertoire "%s" du plugin "%s".', $file, $this->name));
		}

		$is_private = (0 === strpos($file, 'admin/'));

		// Créer l'environnement d'exécution du plugin
		if (substr($file, -4) === '.php') {
			if (substr($file, 0, 6) == 'admin/' || substr($file, 0, 7) == 'public/') {
				define('Garradin\PLUGIN_ROOT', $this->path());
				define('Garradin\PLUGIN_URL', WWW_URL . 'p/' . $this->name . '/');
				define('Garradin\PLUGIN_ADMIN_URL', WWW_URL .'admin/p/' . $this->name . '/');
				define('Garradin\PLUGIN_QSP', '?');

				$tpl = Template::getInstance();

				if ($is_private) {
					require ROOT . '/www/admin/_inc.php';
					$tpl->assign('current', 'plugin_' . $this->name);
				}

				$tpl->assign('plugin', $this);
				$tpl->assign('plugin_url', \Garradin\PLUGIN_URL);
				$tpl->assign('plugin_admin_url', \Garradin\PLUGIN_ADMIN_URL);
				$tpl->assign('plugin_root', \Garradin\PLUGIN_ROOT);
			}

			$plugin = $this;

			include $path;
		}
		elseif (substr($file, -3) === '.md' && $is_private) {
			$p = new ParseDown(null, null);
			header('Content-Type: text/html');

			printf('<!DOCYPE html><head>
				<style type="text/css">body { font-family: Verdana, sans-serif; padding: .5em; margin: 0; background: #fff; color: #000; }</style>
				<link rel="stylesheet" type="text/css" href="%scss.php" /></head><body>', ADMIN_URL);
			echo $p->text(file_get_contents($path));
		}
		else {
			// Récupération du type MIME à partir de l'extension
			$pos = strrpos($path, '.');
			$ext = substr($path, $pos+1);

			$mime = self::MIME_TYPES[$ext] ?? 'text/plain';

			header('Content-Type: ' .$mime);
			header('Content-Length: ' . filesize($path));
			header('Cache-Control: public, max-age=3600');
			header('Last-Modified: ' . date(DATE_RFC7231, filemtime($path)));

			readfile($path);
		}
	}

	public function route(string $uri): void
	{
		$uri = ltrim($uri, '/');

		if (0 === strpos($uri, 'admin/')) {
			if (!Session::getInstance()->isLogged()) {
				Utils::redirect('!login.php');
			}
		}
		else {
			$uri = 'public/' . $uri;
		}

		if (!$uri || substr($uri, -1) == '/') {
			$uri .= 'index.php';
		}

		try {
			$this->call($uri);
		}
		catch (\UnexpectedValueException $e) {
			http_response_code(404);
			throw new UserException($e->getMessage());
		}
	}

	public function isAvailable(): bool
	{
		return $this->hasFile(self::META_FILE);
	}
}

Modified src/include/lib/Garradin/Entities/Services/Service_User.php from [06d5c19167] to [dcc6fb2e78].

185
186
187
188
189
190
191












192
193
194
195
196
197
198
		$transaction->type = Transaction::TYPE_REVENUE;

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

		return $transaction;
	}













	static public function createFromForm(array $users, int $creator_id, bool $from_copy = false, ?array $source = null): self
	{
		if (null === $source) {
			$source = $_POST;
		}








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







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
		$transaction->type = Transaction::TYPE_REVENUE;

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

		return $transaction;
	}

	public function updateExpectedAmount(): void
	{
		$fee = $this->fee();

		if ($fee && $fee->id_account && $this->id_user) {
			$this->set('expected_amount', $fee->getAmountForUser($this->id_user));
		}
		else {
			$this->set('expected_amount', null);
		}
	}

	static public function createFromForm(array $users, int $creator_id, bool $from_copy = false, ?array $source = null): self
	{
		if (null === $source) {
			$source = $_POST;
		}

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
			$su->importForm($source);
			$su->id_user = (int) $id;

			if (empty($su->id_service)) {
				throw new ValidationException('Aucune activité n\'a été sélectionnée.');
			}

			if ($su->id_fee && $su->fee() && $su->fee()->id_account && $su->id_user) {
				$su->expected_amount = $su->fee()->getAmountForUser($su->id_user);
			}

			if ($su->isDuplicate($from_copy ? false : true)) {
				if ($from_copy) {
					continue;
				}
				else {
					throw new ValidationException(sprintf('%s : Cette activité a déjà été enregistrée pour ce membre et cette date', $name));







<
|
<







221
222
223
224
225
226
227

228

229
230
231
232
233
234
235
			$su->importForm($source);
			$su->id_user = (int) $id;

			if (empty($su->id_service)) {
				throw new ValidationException('Aucune activité n\'a été sélectionnée.');
			}


			$su->updateExpectedAmount();


			if ($su->isDuplicate($from_copy ? false : true)) {
				if ($from_copy) {
					continue;
				}
				else {
					throw new ValidationException(sprintf('%s : Cette activité a déjà été enregistrée pour ce membre et cette date', $name));

Modified src/include/lib/Garradin/Entities/Users/DynamicField.php from [b95114f15e] to [74af5ec509].

175
176
177
178
179
180
181

182
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197

	const SYSTEM_FIELDS = [
		'id'           => '?int',
		'id_category'  => 'int',
		'pgp_key'      => '?string',
		'otp_secret'   => '?string',
		'date_login'   => '?DateTime',

		'id_parent'    => '?int',
		'is_parent'    => 'bool',
		'preferences'  => '?stdClass',
	];

	const SYSTEM_FIELDS_SQL = [
		'id INTEGER PRIMARY KEY,',
		'id_category INTEGER NOT NULL REFERENCES users_categories(id),',
		'date_login TEXT NULL CHECK (date_login IS NULL OR datetime(date_login) = date_login),',

		'otp_secret TEXT NULL,',
		'pgp_key TEXT NULL,',
		'id_parent INTEGER NULL REFERENCES users(id) ON DELETE SET NULL CHECK (id_parent IS NULL OR is_parent = 0),',
		'is_parent INTEGER NOT NULL DEFAULT 0,',
		'preferences TEXT NULL,'
	];








>









>







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

	const SYSTEM_FIELDS = [
		'id'           => '?int',
		'id_category'  => 'int',
		'pgp_key'      => '?string',
		'otp_secret'   => '?string',
		'date_login'   => '?DateTime',
		'date_updated' => '?DateTime',
		'id_parent'    => '?int',
		'is_parent'    => 'bool',
		'preferences'  => '?stdClass',
	];

	const SYSTEM_FIELDS_SQL = [
		'id INTEGER PRIMARY KEY,',
		'id_category INTEGER NOT NULL REFERENCES users_categories(id),',
		'date_login TEXT NULL CHECK (date_login IS NULL OR datetime(date_login) = date_login),',
		'date_updated TEXT NULL CHECK (date_updated IS NULL OR datetime(date_updated) = date_updated),',
		'otp_secret TEXT NULL,',
		'pgp_key TEXT NULL,',
		'id_parent INTEGER NULL REFERENCES users(id) ON DELETE SET NULL CHECK (id_parent IS NULL OR is_parent = 0),',
		'is_parent INTEGER NOT NULL DEFAULT 0,',
		'preferences TEXT NULL,'
	];

Modified src/include/lib/Garradin/Entities/Users/User.php from [07dacbb826] to [71610b9fa9].

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
		// Check email addresses
		foreach (DynamicFields::getEmailFields() as $field) {
			$this->assert($this->$field === null || SMTP::checkEmailIsValid($this->$field, false), 'Cette adresse email n\'est pas valide.');
		}

		// check user number
		$field = DynamicFields::getNumberField();
		$this->assert($this->$field !== null && !ctype_digit((string)$this->$field), 'Numéro de membre invalide : ne peut contenir que des chiffres');

		$db = DB::getInstance();

		if (!$this->exists()) {
			$number_exists = $db->test(self::TABLE, sprintf('%s = ?', $db->quoteIdentifier($field)), $this->$field);
		}
		else {







|







125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
		// Check email addresses
		foreach (DynamicFields::getEmailFields() as $field) {
			$this->assert($this->$field === null || SMTP::checkEmailIsValid($this->$field, false), 'Cette adresse email n\'est pas valide.');
		}

		// check user number
		$field = DynamicFields::getNumberField();
		$this->assert($this->$field !== null && ctype_digit((string)$this->$field), 'Numéro de membre invalide : ne peut contenir que des chiffres');

		$db = DB::getInstance();

		if (!$this->exists()) {
			$number_exists = $db->test(self::TABLE, sprintf('%s = ?', $db->quoteIdentifier($field)), $this->$field);
		}
		else {
195
196
197
198
199
200
201




202
203
204
205


206
207
208
209
210
211
212
		}

		return $out;
	}

	public function save(bool $selfcheck = true): bool
	{




		$columns = array_intersect(DynamicFields::getInstance()->getSearchColumns(), array_keys($this->_modified));
		$login_field = DynamicFields::getLoginField();
		$login_modified = $this->_modified[$login_field] ?? null;
		$password_modified = $this->_modified['password'] ?? null;



		parent::save($selfcheck);

		// We are not using a trigger as it would make modifying the users table from outside impossible
		// (because the transliterate_to_ascii function does not exist)
		if (count($columns)) {
			DynamicFields::getInstance()->rebuildUserSearchCache($this->id());







>
>
>
>




>
>







195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
		}

		return $out;
	}

	public function save(bool $selfcheck = true): bool
	{
		if (!count($this->_modified) && $this->exists()) {
			return true;
		}

		$columns = array_intersect(DynamicFields::getInstance()->getSearchColumns(), array_keys($this->_modified));
		$login_field = DynamicFields::getLoginField();
		$login_modified = $this->_modified[$login_field] ?? null;
		$password_modified = $this->_modified['password'] ?? null;

		$this->set('date_updated', new \DateTime);

		parent::save($selfcheck);

		// We are not using a trigger as it would make modifying the users table from outside impossible
		// (because the transliterate_to_ascii function does not exist)
		if (count($columns)) {
			DynamicFields::getInstance()->rebuildUserSearchCache($this->id());

Modified src/include/lib/Garradin/Entity.php from [6c483405f0] to [f2feecdbe4].

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
			$this->selfCheck();
		}

		$new = $this->exists() ? false : true;
		$modified = $this->isModified();

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

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

		$return = parent::save(false);

		// Log creation/edit, but don't record stuff that doesn't change anything
		if ($this::NAME && ($new || $modified)) {
			$type = str_replace('Garradin\Entities\\', '', get_class($this));
			Log::add($new ? Log::CREATE : Log::EDIT, ['entity' => $type, 'id' => $this->id()]);
		}

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

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

		return $return;
	}

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

		$id = $this->id();

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

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

		$return = parent::delete();

		if ($this::NAME) {
			$type = str_replace('Garradin\Entities\\', '', get_class($this));
			Log::add(Log::DELETE, ['entity' => $type, 'id' => $id]);
		}

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

		return $return;
	}
}







|




|











|

|






|
|
|



|




|






<
|


|
|




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
			$this->selfCheck();
		}

		$new = $this->exists() ? false : true;
		$modified = $this->isModified();

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

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

		$return = parent::save(false);

		// Log creation/edit, but don't record stuff that doesn't change anything
		if ($this::NAME && ($new || $modified)) {
			$type = str_replace('Garradin\Entities\\', '', get_class($this));
			Log::add($new ? Log::CREATE : Log::EDIT, ['entity' => $type, 'id' => $this->id()]);
		}

		Plugins::fireSignal($name . '.after', ['entity' => $this, 'success' => $return, 'new' => $new]);

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

		return $return;
	}

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

		$id = $this->id();

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

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

		$return = parent::delete();

		if ($this::NAME) {

			Log::add(Log::DELETE, ['entity' => $name, 'id' => $id]);
		}

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

		return $return;
	}
}

Modified src/include/lib/Garradin/Files/Files.php from [80bfb98411] to [c6af2950e6].

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\Config;
use Garradin\DB;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Users\Session;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;








|







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\Config;
use Garradin\DB;
use Garradin\Plugins;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\ValidationException;
use Garradin\Users\Session;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;

867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
			'image'    => false,
		]);

		$file->modified = new \DateTime;

		Files::callStorage('mkdir', $file);

		Plugin::fireSignal('files.mkdir', compact('file'));

		return $file;
	}

	static public function ensureDirectoryExists(string $path): void
	{
		$db = DB::getInstance();







|







867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
			'image'    => false,
		]);

		$file->modified = new \DateTime;

		Files::callStorage('mkdir', $file);

		Plugins::fireSignal('files.mkdir', compact('file'));

		return $file;
	}

	static public function ensureDirectoryExists(string $path): void
	{
		$db = DB::getInstance();

Modified src/include/lib/Garradin/Files/WebDAV/NextCloud.php from [98ea2e4b65] to [8231ba23c4].

19
20
21
22
23
24
25












26
27
28
29
30
31
32
	protected string $prefix = File::CONTEXT_DOCUMENTS . '/';

	public function __construct()
	{
		$this->temporary_chunks_path =  CACHE_ROOT . '/webdav.chunks';
		$this->setRootURL(WWW_URL);
	}













	public function auth(?string $login, ?string $password): bool
	{
		$session = Session::getInstance();

		if ($session->isLogged()) {
			return true;







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







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
	protected string $prefix = File::CONTEXT_DOCUMENTS . '/';

	public function __construct()
	{
		$this->temporary_chunks_path =  CACHE_ROOT . '/webdav.chunks';
		$this->setRootURL(WWW_URL);
	}

	public function route(?string $uri = null): bool
	{
		$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';

		// Currently, iOS apps are broken
		if (stristr($ua, 'nextcloud-ios') || stristr($ua, 'owncloudapp')) {
			throw new WebDAV_Exception('Your client is not compatible with this server. Consider using a different WebDAV client.', 403);
		}

		return parent::route($uri);
	}

	public function auth(?string $login, ?string $password): bool
	{
		$session = Session::getInstance();

		if ($session->isLogged()) {
			return true;

Modified src/include/lib/Garradin/Files/WebDAV/Storage.php from [ca6c2a31dc] to [6f6b7f9e7f].

258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
				$out[$name] = $v;
			}
		}

		return $out;
	}

	public function put(string $uri, $pointer, ?string $hash, ?int $mtime): bool
	{
		if (!strpos($uri, '/')) {
			throw new WebDAV_Exception('Impossible de créer un fichier ici', 403);
		}

		if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
			return false;







|







258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
				$out[$name] = $v;
			}
		}

		return $out;
	}

	public function put(string $uri, $pointer, ?string $hash_algo, ?string $hash, ?int $mtime): bool
	{
		if (!strpos($uri, '/')) {
			throw new WebDAV_Exception('Impossible de créer un fichier ici', 403);
		}

		if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) {
			return false;
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
		if ($new && !File::canCreate($uri, $this->session)) {
			throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de créer ce fichier', 403);
		}
		elseif (!$new && !$target->canWrite($this->session)) {
			throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de modifier ce fichier', 403);
		}

		$h = $hash ? hash_init('md5') : null;

		while (!feof($pointer)) {
			if ($h) {
				hash_update($h, fread($pointer, 8192));
			}
			else {
				fread($pointer, 8192);
			}
		}

		if ($h) {
			if (hash_final($h) != $hash) {
				throw new WebDAV_Exception('The data sent does not match the supplied MD5 hash', 400);
			}
		}

		// Check size
		$size = ftell($pointer);

		try {







|












|







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
		if ($new && !File::canCreate($uri, $this->session)) {
			throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de créer ce fichier', 403);
		}
		elseif (!$new && !$target->canWrite($this->session)) {
			throw new WebDAV_Exception('Vous n\'avez pas l\'autorisation de modifier ce fichier', 403);
		}

		$h = $hash ? hash_init($hash_algo == 'MD5' ? 'md5' : 'sha1') : null;

		while (!feof($pointer)) {
			if ($h) {
				hash_update($h, fread($pointer, 8192));
			}
			else {
				fread($pointer, 8192);
			}
		}

		if ($h) {
			if (hash_final($h) != $hash) {
				throw new WebDAV_Exception('The data sent does not match the supplied hash', 400);
			}
		}

		// Check size
		$size = ftell($pointer);

		try {

Modified src/include/lib/Garradin/Form.php from [dbe973dfc0] to [50a5e33df9].

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
			&& strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') {
			$this->addError('Le fichier envoyé dépasse la taille autorisée');
		}
	}

	public function run(callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): bool
	{

		if (null !== $csrf_key && !$this->check($csrf_key)) {
			return false;

		}

		try {
			call_user_func($fn);

			if (null !== $redirect) {
				if (array_key_exists('_dialog', $_GET)) {
					Utils::reloadParentFrame($follow_redirect ? $redirect : null);
				}

				Utils::redirect($redirect);
			}

			return true;
		}
		catch (UserException $e) {
			$this->addError($e);



			return false;
		}
	}











	public function runIf($condition, callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): ?bool
	{
		if (is_string($condition) && empty($_POST[$condition])) {
			return null;
		}
		elseif (is_bool($condition) && !$condition) {
			return null;
		}

		return $this->run($fn, $csrf_key, $redirect, $follow_redirect);
	}




	public function check($token_action = '', Array $rules = null)
	{
		if (!\KD2\Form::tokenCheck($token_action))
		{
			$this->errors[] = 'Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.';
			return false;
		}

		if (!is_null($rules) && !$this->validate($rules))
		{
			return false;
		}

		return true;
	}




	public function validate(Array $rules, array $source = null)
	{
		return \KD2\Form::validate($rules, $this->errors, $source);
	}

	public function hasErrors()
	{







>
|
<
>
|

<














>
>
>



>
>
>
>
>
>
>
>
>
>













>
>
>
















>
>
>







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
			&& strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') {
			$this->addError('Le fichier envoyé dépasse la taille autorisée');
		}
	}

	public function run(callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): bool
	{
		try {
			if (null !== $csrf_key && !\KD2\Form::tokenCheck($csrf_key)) {

				throw new ValidationException('Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.');
			}


			call_user_func($fn);

			if (null !== $redirect) {
				if (array_key_exists('_dialog', $_GET)) {
					Utils::reloadParentFrame($follow_redirect ? $redirect : null);
				}

				Utils::redirect($redirect);
			}

			return true;
		}
		catch (UserException $e) {
			$this->addError($e);

			Form::reportUserException($e);

			return false;
		}
	}

	static public function reportUserException(UserException $e): void
	{
		if (REPORT_USER_EXCEPTIONS === 2) {
			throw $e;
		}
		elseif (REPORT_USER_EXCEPTIONS === 1) {
			\KD2\ErrorManager::reportExceptionSilent($e);
		}
	}

	public function runIf($condition, callable $fn, ?string $csrf_key = null, ?string $redirect = null, bool $follow_redirect = false): ?bool
	{
		if (is_string($condition) && empty($_POST[$condition])) {
			return null;
		}
		elseif (is_bool($condition) && !$condition) {
			return null;
		}

		return $this->run($fn, $csrf_key, $redirect, $follow_redirect);
	}

	/**
	 * @deprecated
	 */
	public function check($token_action = '', Array $rules = null)
	{
		if (!\KD2\Form::tokenCheck($token_action))
		{
			$this->errors[] = 'Une erreur est survenue, merci de bien vouloir renvoyer le formulaire.';
			return false;
		}

		if (!is_null($rules) && !$this->validate($rules))
		{
			return false;
		}

		return true;
	}

	/**
	 * @deprecated
	 */
	public function validate(Array $rules, array $source = null)
	{
		return \KD2\Form::validate($rules, $this->errors, $source);
	}

	public function hasErrors()
	{

Modified src/include/lib/Garradin/Install.php from [7414c2f446] to [83fa8d0d78].

8
9
10
11
12
13
14


15
16
17
18
19
20
21
22
23

















24
25
26
27
28
29
30
use Garradin\Entities\Users\Category;
use Garradin\Entities\Users\User;
use Garradin\Entities\Files\File;
use Garradin\Entities\Search;
use Garradin\Users\DynamicFields;
use Garradin\Users\Session;
use Garradin\Files\Files;



use KD2\HTTP;

/**
 * Pour procéder à l'installation de l'instance Garradin
 * Utile pour automatiser l'installation sans passer par la page d'installation
 */
class Install
{

















	/**
	 * This sends the current installed version, as well as the PHP and SQLite versions
	 * for statistics purposes.
	 *
	 * You can disable this by setting DISABLE_INSTALL_PING to TRUE in CONFIG_FILE
	 */
	static public function ping(): void







>
>









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







8
9
10
11
12
13
14
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
use Garradin\Entities\Users\Category;
use Garradin\Entities\Users\User;
use Garradin\Entities\Files\File;
use Garradin\Entities\Search;
use Garradin\Users\DynamicFields;
use Garradin\Users\Session;
use Garradin\Files\Files;
use Garradin\UserTemplate\Modules;
use Garradin\Plugins;

use KD2\HTTP;

/**
 * Pour procéder à l'installation de l'instance Garradin
 * Utile pour automatiser l'installation sans passer par la page d'installation
 */
class Install
{
	/**
	 * List of plugins that should be displayed during installation (if present)
	 */
	const DEFAULT_PLUGINS = [
		'caisse',
		'taima',
	];

	const DEFAULT_MODULES = [
		'recus_fiscaux',
		'carte_membre',
		'recu_don',
		'recu_paiement',
		//'bilan_pc',
		//'invoice',
	];

	/**
	 * This sends the current installed version, as well as the PHP and SQLite versions
	 * for statistics purposes.
	 *
	 * You can disable this by setting DISABLE_INSTALL_PING to TRUE in CONFIG_FILE
	 */
	static public function ping(): void
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
		self::assert(isset($source['password']) && isset($source['password_confirmed']) && trim($source['password']) !== '', 'Le mot de passe n\'est pas renseigné');

		self::assert((bool)filter_var($source['user_email'], FILTER_VALIDATE_EMAIL), 'Adresse email invalide');

		self::assert(strlen($source['password']) >= User::MINIMUM_PASSWORD_LENGTH, 'Le mot de passe est trop court');
		self::assert($source['password'] === $source['password_confirmed'], 'La vérification du mot de passe ne correspond pas');




		try {
			self::install($source['country'], $source['name'], $source['user_name'], $source['user_email'], $source['password']);
			self::ping();
		}
		catch (\Exception $e) {
			@unlink(DB_FILE);
			throw $e;
		}
	}

	static public function install(string $country_code, string $name, string $user_name, string $user_email, string $user_password, ?string $welcome_text = null): void
	{
		if (file_exists(DB_FILE)) {
			throw new UserException('La base de données existe déjà.');
		}

		self::checkAndCreateDirectories();
		Files::disableQuota();







>
>
>

|








|







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
		self::assert(isset($source['password']) && isset($source['password_confirmed']) && trim($source['password']) !== '', 'Le mot de passe n\'est pas renseigné');

		self::assert((bool)filter_var($source['user_email'], FILTER_VALIDATE_EMAIL), 'Adresse email invalide');

		self::assert(strlen($source['password']) >= User::MINIMUM_PASSWORD_LENGTH, 'Le mot de passe est trop court');
		self::assert($source['password'] === $source['password_confirmed'], 'La vérification du mot de passe ne correspond pas');

		$plugins = isset($source['plugins']) ? array_keys($source['plugins']) : [];
		$modules = isset($source['modules']) ? array_keys($source['modules']) : [];

		try {
			self::install($source['country'], $source['name'], $source['user_name'], $source['user_email'], $source['password'], $plugins, $modules);
			self::ping();
		}
		catch (\Exception $e) {
			@unlink(DB_FILE);
			throw $e;
		}
	}

	static public function install(string $country_code, string $name, string $user_name, string $user_email, string $user_password, array $plugins = [], array $modules = []): void
	{
		if (file_exists(DB_FILE)) {
			throw new UserException('La base de données existe déjà.');
		}

		self::checkAndCreateDirectories();
		Files::disableQuota();
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
			'password_confirmed' => $user_password,
		]);

		$user->save();

		$config->set('files', array_map(fn () => null, $config::FILES));

		$welcome_text = $welcome_text ?? sprintf("Bienvenue dans l'administration de %s !\n\nUtilisez le menu à gauche pour accéder aux différentes sections.\n\nCe message peut être modifié dans la 'Configuration'.", $name);

		$config->setFile('admin_homepage', $welcome_text);

        // Import accounting chart
        $chart = Charts::installCountryDefault($country_code);

		// Create an example saved search (users)







|







287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
			'password_confirmed' => $user_password,
		]);

		$user->save();

		$config->set('files', array_map(fn () => null, $config::FILES));

		$welcome_text = sprintf("Bienvenue dans l'administration de %s !\n\nUtilisez le menu à gauche pour accéder aux différentes sections.\n\nSi vous êtes perdu, n'hésitez pas à consulter l'aide :-)", $name);

		$config->setFile('admin_homepage', $welcome_text);

        // Import accounting chart
        $chart = Charts::installCountryDefault($country_code);

		// Create an example saved search (users)
326
327
328
329
330
331
332
333


334
335
336
337
338
339
340









341


342
343
344
345
346
347
348
			'label'   => 'Écritures sans projet',
			'target'  => $search::TARGET_ACCOUNTING,
			'type'    => $search::TYPE_JSON,
			'content' => json_encode($query),
		]);
		$search->created = new \DateTime;
		$search->save();



		// Install welcome plugin if available
		$has_welcome_plugin = Plugin::getPath('welcome');

		if ($has_welcome_plugin) {
			Plugin::install('welcome', true);
		}










		$config->save();


		Files::enableQuota();
	}

	static public function checkAndCreateDirectories()
	{
		// Vérifier que les répertoires vides existent, sinon les créer
		$paths = [








>
>

|


|


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







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
			'label'   => 'Écritures sans projet',
			'target'  => $search::TARGET_ACCOUNTING,
			'type'    => $search::TYPE_JSON,
			'content' => json_encode($query),
		]);
		$search->created = new \DateTime;
		$search->save();

		$config->save();

		// Install welcome plugin if available
		$has_welcome_plugin = Plugins::exists('welcome');

		if ($has_welcome_plugin) {
			Plugins::install('welcome');
		}

		foreach ($plugins as $plugin) {
			Plugins::install($plugin);
		}

		Modules::refresh();

		foreach ($modules as $module) {
			$m = Modules::get($module);
			$m->set('enabled', true);
			$m->save();
		}

		Files::enableQuota();
	}

	static public function checkAndCreateDirectories()
	{
		// Vérifier que les répertoires vides existent, sinon les créer
		$paths = [
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
	}

	static public function setLocalConfig(string $key, $value, bool $overwrite = true): void
	{
		$path = ROOT . DIRECTORY_SEPARATOR . CONFIG_FILE;
		$new_line = sprintf('const %s = %s;', $key, var_export($value, true));

		if (file_exists($path)) {
			$config = file_get_contents($path);

			$pattern = sprintf('/^.*(?:const\s+%s|define\s*\(.*%1$s).*$/m', $key);

			$config = preg_replace($pattern, $new_line, $config, -1, $count);

			if ($count && !$overwrite) {







|







418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
	}

	static public function setLocalConfig(string $key, $value, bool $overwrite = true): void
	{
		$path = ROOT . DIRECTORY_SEPARATOR . CONFIG_FILE;
		$new_line = sprintf('const %s = %s;', $key, var_export($value, true));

		if (@filesize($path)) {
			$config = file_get_contents($path);

			$pattern = sprintf('/^.*(?:const\s+%s|define\s*\(.*%1$s).*$/m', $key);

			$config = preg_replace($pattern, $new_line, $config, -1, $count);

			if ($count && !$overwrite) {

Modified src/include/lib/Garradin/Log.php from [88467b613d] to [1aaa0e26e7].

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
		if ($count >= self::SOFT_LOCKOUT_ATTEMPTS) {
			return -1;
		}

		return 0;
	}

	static public function list(?int $id_user = null): DynamicList
	{
		$id_field = DynamicFields::getNameFieldsSQL('u');

		$columns = [
			'id_user' => [
			],
			'identity' => [
				'label' => $id_user ? null : 'Membre',
				'select' => $id_field,
			],
			'created' => [
				'label' => 'Date'
			],
			'type_icon' => [
				'select' => null,







|







|







110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
		if ($count >= self::SOFT_LOCKOUT_ATTEMPTS) {
			return -1;
		}

		return 0;
	}

	static public function list(array $params = []): DynamicList
	{
		$id_field = DynamicFields::getNameFieldsSQL('u');

		$columns = [
			'id_user' => [
			],
			'identity' => [
				'label' => isset($params['id_self']) ? null : (isset($params['history']) ? 'Membre à l\'origine de la modification' : 'Membre'),
				'select' => $id_field,
			],
			'created' => [
				'label' => 'Date'
			],
			'type_icon' => [
				'select' => null,
142
143
144
145
146
147
148










149

150
151
152
153
154
155
156
			'ip_address' => [
				'label' => 'Adresse IP',
			],
		];

		$tables = 'logs LEFT JOIN users u ON u.id = logs.id_user';











		$conditions = $id_user ? 'logs.id_user = ' . $id_user : '1';


		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('created', true);
		$list->setCount('COUNT(logs.id)');
		$list->setModifier(function (&$row) {
			$row->created = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->created);
			$row->details = $row->details ? json_decode($row->details) : null;







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







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
			'ip_address' => [
				'label' => 'Adresse IP',
			],
		];

		$tables = 'logs LEFT JOIN users u ON u.id = logs.id_user';

		if (isset($params['id_user'])) {
			$conditions = 'logs.id_user = ' . (int)$params['id_user'];
		}
		elseif (isset($params['id_self'])) {
			$conditions = sprintf('logs.id_user = %d AND type < 10', (int)$params['id_self']);
		}
		elseif (isset($params['history'])) {
			$conditions = sprintf('logs.type IN (%d, %d, %d) AND json_extract(logs.details, \'$.entity\') = \'Users\\User\' AND json_extract(logs.details, \'$.id\') = %d', self::CREATE, self::EDIT, self::DELETE, (int)$params['history']);
		}
		else {
			$conditions = '1';
		}

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('created', true);
		$list->setCount('COUNT(logs.id)');
		$list->setModifier(function (&$row) {
			$row->created = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->created);
			$row->details = $row->details ? json_decode($row->details) : null;

Modified src/include/lib/Garradin/Membres/Import.php from [efbfcdc67d] to [956a6ebd29].

161
162
163
164
165
166
167




168
169
170
171
172
173
174

						if ($found !== false) {
							$data[$name] |= 0x01 << $found;
						}
					}
				}
			}





			if (!empty($data['numero']) && $data['numero'] > 0)
			{
				$numero = (int)$data['numero'];
			}
			else
			{







>
>
>
>







161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178

						if ($found !== false) {
							$data[$name] |= 0x01 << $found;
						}
					}
				}
			}

			if (!count($data)) {
				throw new UserException('Erreur sur la ligne ' . $line . ' : aucun champ de la fiche membre n\'a été trouvé');
			}

			if (!empty($data['numero']) && $data['numero'] > 0)
			{
				$numero = (int)$data['numero'];
			}
			else
			{

Deleted src/include/lib/Garradin/Plugin.php version [b780b1e451].

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

namespace Garradin;

use Garradin\Users\Session;

class Plugin
{
	const PLUGIN_ID_REGEXP = '[a-z]+(?:_[a-z]+)*';

	protected $id = null;
	protected $plugin = null;
	protected $config_changed = false;

	/**
	 * Set to false to disable signal firing
	 * @var boolean
	 */
	static protected $signals = true;

	protected $mimes = [
		'css' => 'text/css',
		'gif' => 'image/gif',
		'htm' => 'text/html',
		'html' => 'text/html',
		'ico' => 'image/x-ico',
		'jpe' => 'image/jpeg',
		'jpg' => 'image/jpeg',
		'jpeg' => 'image/jpeg',
		'js' => 'application/javascript',
		'pdf' => 'application/pdf',
		'png' => 'image/png',
		'xml' => 'text/xml',
		'svg' => 'image/svg+xml',
	];

	static public function toggleSignals(bool $enabled) {
		self::$signals = $enabled;
	}

	static public function getPath($id)
	{
		if (file_exists(PLUGINS_ROOT . '/' . $id . '.tar.gz'))
		{
			return 'phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz';
		}
		elseif (is_dir(PLUGINS_ROOT . '/' . $id))
		{
			return PLUGINS_ROOT . '/' . $id;
		}

		return false;
	}

	static public function getURL(string $id, string $path = '')
	{
		return ADMIN_URL . 'p/' . $id . '/' . ltrim($path, '/');
	}

	static public function getPublicURL(string $id, string $path = '')
	{
		return WWW_URL . 'p/' . $id . '/' . ltrim($path, '/');
	}

	/**
	 * Construire un objet Plugin pour un plugin
	 * @param string $id Identifiant du plugin
	 * @throws UserException Si le plugin n'est pas installé (n'existe pas en DB)
	 */
	public function __construct(string $id)
	{
		$db = DB::getInstance();
		$this->plugin = $db->first('SELECT * FROM plugins WHERE id = ?;', $id);

		if (!$this->plugin)
		{
			throw new UserException(sprintf('Le plugin "%s" n\'existe pas ou n\'est pas installé correctement.', $id));
		}

		$this->plugin->config = json_decode($this->plugin->config ?? '');

		if (!is_object($this->plugin->config))
		{
			$this->plugin->config = new \stdClass;
		}

		// Juste pour vérifier que le fichier source du plugin existe bien
		self::getPath($id);

		$this->id = $id;
	}

	/**
	 * Enregistrer les changements dans la config
	 */
	public function __destruct()
	{
		if ($this->config_changed)
		{
			$db = DB::getInstance();
			$db->update('plugins', 
				['config' => json_encode($this->plugin->config)],
				'id = \'' . $this->id . '\'');
		}
	}

	/**
	 * Renvoie le chemin absolu vers l'archive du plugin
	 * @return string Chemin PHAR vers l'archive
	 */
	public function path()
	{
		return self::getPath($this->id);
	}

	/**
	 * Renvoie une entrée de la configuration ou la configuration complète
	 * @param  string $key Clé à rechercher, ou NULL si on désire toutes les entrées de la
	 * @return mixed       L'entrée demandée (mixed), ou l'intégralité de la config (array),
	 * ou NULL si l'entrée demandée n'existe pas.
	 */
	public function getConfig($key = null)
	{
		if (is_null($key))
		{
			return $this->plugin->config;
		}

		if (property_exists($this->plugin->config, $key))
		{
			return $this->plugin->config->$key;
		}

		return null;
	}

	/**
	 * Enregistre une entrée dans la configuration du plugin
	 * @param string $key   Clé à modifier
	 * @param mixed  $value Valeur à enregistrer, choisir NULL pour effacer cette clé de la configuration
	 * @return boolean 		TRUE si tout se passe bien
	 */
	public function setConfig($key, $value = null)
	{
		if (is_null($value))
		{
			unset($this->plugin->config->$key);
		}
		else
		{
			$this->plugin->config->$key = $value;
		}

		$this->config_changed = true;

		return true;
	}

	/**
	 * Remplace toute la config du plugin
	 * @param \stdClass $config Configuration complète du plugin
	 */
	public function setConfigAll(\stdClass $config)
	{
		$this->plugin->config = $config;
		$this->config_changed = true;
		return true;
	}

	/**
	 * Renvoie une information ou toutes les informations sur le plugin
	 * @param  string $key Clé de l'info à retourner, ou NULL pour recevoir toutes les infos
	 * @return mixed       Info demandée ou tableau des infos.
	 */
	public function getInfos($key = null)
	{
		if (is_null($key))
		{
			return $this->plugin;
		}

		if (property_exists($this->plugin, $key))
		{
			return $this->plugin->$key;
		}

		return null;
	}

	/**
	 * Renvoie l'identifiant du plugin
	 * @return string Identifiant du plugin
	 */
	public function id()
	{
		return $this->id;
	}

	public function route(bool $public, string $uri): void
	{
		if (!$uri || substr($uri, -1) == '/') {
			$uri .= 'index.php';
		}

		try {
			$this->call($public, $uri);
		}
		catch (\UnexpectedValueException $e) {
			http_response_code(404);
			throw new UserException($e->getMessage());
		}
	}

	/**
	 * Inclure un fichier depuis le plugin (dynamique ou statique)
	 * @param bool   $public TRUE si le fichier est situé dans 'public', sinon dans 'admin'
	 * @param string $file   Chemin du fichier à aller chercher : si c'est un .php il sera inclus,
	 * sinon il sera juste affiché
	 * @return void
	 * @throws UserException Si le fichier n'existe pas ou fait partie des fichiers qui ne peuvent
	 * être appelés que par des méthodes de Plugin.
	 * @throws \RuntimeException Si le chemin indiqué tente de sortir du contexte du PHAR
	 */
	public function call(bool $public, string $file)
	{
		$file = preg_replace('!^[./]*!', '', $file);

		if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file))
		{
			throw new \UnexpectedValueException('Chemin de fichier incorrect.');
		}

		$forbidden = ['install.php', 'garradin_plugin.ini', 'upgrade.php', 'uninstall.php'];

		if (in_array(basename($file), $forbidden))
		{
			throw new UserException('Le fichier ' . $file . ' ne peut être appelé par cette méthode.');
		}

		$path = $this->path();

		if (!$path) {
			throw new UserException('Cette extension n\'est pas disponible.');
		}

		$path .= $public ? '/public/' : '/admin/';
		$path .= $file;

		if (!file_exists($path)) {
			throw new UserException(sprintf('Le fichier "%s" n\'existe pas dans le plugin "%s"', substr($path, strlen($this->path())), $this->id));
		}

		if (is_dir($path)) {
			throw new UserException(sprintf('Sécurité : impossible de lister le répertoire "%s" du plugin "%s".', $file, $this->id));
		}

		if (substr($path, -4) === '.php')
		{
			define('Garradin\PLUGIN_ROOT', $this->path());
			define('Garradin\PLUGIN_URL', WWW_URL . 'p/' . $this->id() . '/');
			define('Garradin\PLUGIN_ADMIN_URL', WWW_URL .'admin/p/' . $this->id() . '/');
			define('Garradin\PLUGIN_QSP', '?');

			// Créer l'environnement d'exécution du plugin
			$plugin = $this;

			if (!$public) {
				require ROOT . '/www/admin/_inc.php';
			}

			$tpl = Template::getInstance();
			$tpl->assign('plugin', $this->getInfos());
			$tpl->assign('plugin_url', PLUGIN_URL);
			$tpl->assign('plugin_admin_url', PLUGIN_ADMIN_URL);
			$tpl->assign('plugin_root', PLUGIN_ROOT);

			include $path;
		}
		else
		{
			// Récupération du type MIME à partir de l'extension
			$pos = strrpos($path, '.');
			$ext = substr($path, $pos+1);

			if (isset($this->mimes[$ext]))
			{
				$mime = $this->mimes[$ext];
			}
			else
			{
				$mime = 'text/plain';
			}

			header('Content-Type: ' .$mime);
			header('Content-Length: ' . filesize($path));
			header('Cache-Control: public, max-age=3600');
			header('Last-Modified: ' . date(DATE_RFC7231, filemtime($path)));

			readfile($path);
		}
	}

	/**
	 * Désinstaller le plugin
	 * @return boolean TRUE si la suppression a fonctionné
	 */
	public function uninstall()
	{
		if (file_exists($this->path() . '/uninstall.php'))
		{
			$plugin = $this;
			include $this->path() . '/uninstall.php';
		}

		$db = DB::getInstance();
		$db->delete('plugins_signals', 'plugin = ?', $this->id);
		return $db->delete('plugins', 'id = ?', $this->id);
	}

	/**
	 * Renvoie TRUE si le plugin a besoin d'être mis à jour
	 * (si la version notée dans la DB est différente de la version notée dans garradin_plugin.ini)
	 * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon
	 */
	public function needUpgrade()
	{
		$infos = (object) parse_ini_file($this->path() . '/garradin_plugin.ini', false);

		if (version_compare($this->plugin->version, $infos->version, '!=')) {
			return true;
		}

		return false;
	}

	/**
	 * Mettre à jour le plugin
	 * Appelle le fichier upgrade.php dans l'archive si celui-ci existe.
	 * @return boolean TRUE si tout a fonctionné
	 */
	public function upgrade(): void
	{
		$infos = (object) parse_ini_file($this->path() . '/garradin_plugin.ini', false);

		if (!isset($infos->name)) {
			return;
		}

		if (file_exists($this->path() . '/upgrade.php'))
		{
			$plugin = $this;
			include $this->path() . '/upgrade.php';
		}

		$data = [
			'name'		=>	$infos->name,
			'description'=>	$infos->description,
			'author'	=>	$infos->author,
			'url'		=>	$infos->url,
			'version'	=>	$infos->version,
		];

		if ($config = self::getDefaultConfig($this->id, $this->path())) {
			$data['config'] = json_encode($config);
		}

		DB::getInstance()->update('plugins', $data, 'id = :id', ['id' => $this->id]);
	}

	/**
	 * Associer un signal à un callback du plugin
	 * @param  string $signal   Nom du signal (par exemple boucle.agenda pour la boucle de type AGENDA)
	 * @param  mixed  $callback Callback, sous forme d'un nom de fonction ou de méthode statique
	 * @return boolean TRUE
	 */
	public function registerSignal(string $signal, callable $callback): void
	{
		$callable_name = '';

		if (!is_callable($callback, true, $callable_name) || !is_string($callable_name))
		{
			throw new \LogicException('Le callback donné n\'est pas valide.');
		}

		// pour empêcher d'appeler des méthodes de Garradin après un import de base de données "hackée"
		if (strpos($callable_name, 'Garradin\\Plugin\\') !== 0)
		{
			throw new \LogicException('Le callback donné n\'utilise pas le namespace Garradin\\Plugin : ' . $callable_name);
		}

		$db = DB::getInstance();

		$callable_name = str_replace('Garradin\\Plugin\\', '', $callable_name);

		$db->preparedQuery('INSERT OR REPLACE INTO plugins_signals VALUES (?, ?, ?);', [$signal, $this->id, $callable_name]);
	}

	public function unregisterSignal(string $signal): void
	{
		DB::getInstance()->preparedQuery('DELETE FROM plugins_signals WHERE plugin = ? AND signal = ?;', [$this->id, $signal]);
	}

	/**
	 * Liste des plugins installés (en DB)
	 * @return array Liste des plugins triés par nom
	 */
	static public function listInstalled()
	{
		$db = DB::getInstance();
		$plugins = $db->getGrouped('SELECT id, * FROM plugins ORDER BY name;');

		foreach ($plugins as &$row)
		{
			$row->disabled = !self::getPath($row->id);
		}

		return $plugins;
	}

	/**
	 * Checks if a plugin requires an upgrade and upgrade it
	 * This is run after an upgrade, a database restoration, or in the Plugins page
	 */
	static public function upgradeAllIfRequired(): bool
	{
		$i = 0;

		// Mettre à jour les plugins si nécessaire
		foreach (self::listInstalled() as $id => $infos)
		{
			// Ne pas tenir compte des plugins dont le code n'est pas dispo
			if ($infos->disabled) {
				continue;
			}

			$plugin = new Plugin($id);

			if ($plugin->needUpgrade()) {
				$plugin->upgrade();
				$i++;
			}

			unset($plugin);
		}

		return $i > 0;
	}

	/**
	 * Liste les plugins qui doivent être affichés dans le menu
	 * @return array Tableau associatif id => nom (ou un tableau vide si aucun plugin ne doit être affiché)
	 */
	static public function listMenu(Session $session)
	{
		$list = [];

		// Let plugins handle their listing
		self::fireSignal('menu.item', compact('session'), $list);
		ksort($list);

		return $list;
	}

	/**
	 * Liste les plugins téléchargés mais non installés
	 * @return array Liste des plugins téléchargés
	 */
	static public function listDownloaded()
	{
		$installed = self::listInstalled();

		$list = [];
		$dir = dir(PLUGINS_ROOT);

		while ($file = $dir->read())
		{
			if (substr($file, 0, 1) == '.')
				continue;

			if (preg_match('!^(' . self::PLUGIN_ID_REGEXP . ')\.tar\.gz$!', $file, $match))
			{
				// Sélectionner les archives PHAR
				$file = $match[1];
			}
			elseif (is_dir(PLUGINS_ROOT . '/' . $file)
				&& preg_match('!^' . self::PLUGIN_ID_REGEXP . '$!', $file)
				&& is_file(sprintf('%s/%s/garradin_plugin.ini', PLUGINS_ROOT, $file)))
			{
				// Rien à faire, le nom valide du plugin est déjà dans "$file"
			}
			else
			{
				// ignorer tout ce qui n'est pas un répertoire ou une archive PHAR valides
				continue;
			}

			if (array_key_exists($file, $installed))
			{
				// Ignorer les plugins déjà installés
				continue;
			}

			$data = (object) parse_ini_file(self::getPath($file) . '/garradin_plugin.ini', false);;

			if (!isset($data->name)) {
				// Ignore old plugins
				continue;
			}

			$list[$file] = $data;
		}

		$dir->close();

		return $list;
	}

	/**
	 * Liste des plugins officiels depuis le repository signé
	 * @return array Liste des plugins
	 */
	static public function listOfficial()
	{
		// La liste est stockée en cache une heure pour ne pas tuer le serveur distant
		if (Static_Cache::expired('plugins_list', 3600 * 24))
		{
			$url = parse_url(PLUGINS_URL);

			$context_options = [
				'ssl' => [
					'verify_peer'   => TRUE,
					'verify_depth'  => 5,
					'CN_match'      => $url['host'],
					'SNI_enabled'	=> true,
					'SNI_server_name'		=>	$url['host'],
					'disable_compression'	=>	true,
				]
			];

			$context = stream_context_create($context_options);

			try {
				$result = file_get_contents(PLUGINS_URL, false, $context);
			}
			catch (\Exception $e)
			{
				throw new UserException('Le téléchargement de la liste des plugins a échoué : ' . $e->getMessage());
			}

			Static_Cache::store('plugins_list', $result);
		}
		else
		{
			$result = Static_Cache::get('plugins_list');
		}

		$list = json_decode($result, true);
		return $list;
	}

	static public function fetchOfficialList()
	{
		return []; // FIXME
	}

	/**
	 * Vérifier le hash du plugin $id pour voir s'il correspond au hash du fichier téléchargés
	 * @param  string $id Identifiant du plugin
	 * @return boolean    TRUE si le hash correspond (intégrité OK), sinon FALSE
	 */
	static public function checkHash($id)
	{
		$list = self::fetchOfficialList();

		if (!array_key_exists($id, $list))
			return null;

		$hash = sha1_file(PLUGINS_ROOT . '/' . $id . '.tar.gz');

		return ($hash === $list[$id]['hash']);
	}

	/**
	 * Est-ce que le plugin est officiel ?
	 * @param  string  $id Identifiant du plugin
	 * @return boolean     TRUE si le plugin est officiel, FALSE sinon
	 */
	static public function isOfficial($id)
	{
		$list = self::fetchOfficialList();
		return array_key_exists($id, $list);
	}

	/**
	 * Télécharge un plugin depuis le repository officiel, et l'installe
	 * @param  string $id Identifiant du plugin
	 * @return boolean    TRUE si ça marche
	 * @throws \LogicException Si le plugin n'est pas dans la liste des plugins officiels
	 * @throws UserException Si le plugin est déjà installé ou que le téléchargement a échoué
	 * @throws \RuntimeException Si l'archive téléchargée est corrompue (intégrité du hash ne correspond pas)
	 */
	static public function download($id)
	{
		$list = self::fetchOfficialList();

		if (!array_key_exists($id, $list))
		{
			throw new \LogicException($id . ' n\'est pas un plugin officiel (absent de la liste)');
		}

		if (file_exists(PLUGINS_ROOT . '/' . $id . '.tar.gz'))
		{
			throw new UserException('Le plugin '.$id.' existe déjà.');
		}

		$url = parse_url(PLUGINS_URL);

		$context_options = [
			'ssl' => [
				'verify_peer'   => TRUE,
				'cafile'        => ROOT . '/include/data/cacert.pem',
				'verify_depth'  => 5,
				'CN_match'      => $url['host'],
				'SNI_enabled'	=> true,
				'SNI_server_name'		=>	$url['host'],
				'disable_compression'	=>	true,
			]
		];

		$context = stream_context_create($context_options);

		try {
			copy($list[$id]['phar'], PLUGINS_ROOT . '/' . $id . '.tar.gz', $context);
		}
		catch (\Exception $e)
		{
			throw new UserException('Le téléchargement du plugin '.$id.' a échoué : ' . $e->getMessage());
		}

		if (!self::checkHash($id))
		{
			Utils::safe_unlink(PLUGINS_ROOT . '/' . $id . '.tar.gz');
			throw new \RuntimeException('L\'archive du plugin '.$id.' est corrompue (le hash SHA1 ne correspond pas).');
		}

		self::install($id, true);

		return true;
	}

	/**
	 * Installer un plugin
	 * @param  string  $id       Identifiant du plugin
	 * @param  boolean $official TRUE si le plugin est officiel
	 * @return boolean           TRUE si tout a fonctionné
	 */
	static public function install($id, $official = false)
	{
		$path = self::getPath($id);

		if (!file_exists($path . '/garradin_plugin.ini'))
		{
			throw new UserException(sprintf('Le plugin "%s" n\'est pas une extension Garradin : fichier garradin_plugin.ini manquant.', $id));
		}

		$infos = (object) parse_ini_file($path . '/garradin_plugin.ini', false);

		$required = ['name', 'description', 'author', 'url', 'version'];

		foreach ($required as $key)
		{
			if (!property_exists($infos, $key))
			{
				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.');
		}

		$config = self::getDefaultConfig($id, $path);

		$data = [
			'id' 		=> 	$id,
			'official' 	=> 	(int)(bool)$official,
			'name'		=>	$infos->name,
			'description'=>	$infos->description,
			'author'	=>	$infos->author,
			'url'		=>	$infos->url,
			'version'	=>	$infos->version,
			'config'	=>	json_encode($config),
		];

		$db = DB::getInstance();
		$db->begin();
		$db->insert('plugins', $data);

		if (file_exists($path . '/install.php'))
		{
			$plugin = new Plugin($id);
			require $plugin->path() . '/install.php';
		}

		$db->commit();

		return true;
	}

	static protected function getDefaultConfig(string $id, string $path)
	{
		$config = null;

		if (file_exists($path . '/config.json'))
		{
			if (!file_exists($path . '/admin/config.php'))
			{
				throw new \RuntimeException(sprintf('Le plugin "%s" ne comporte pas de fichier admin/config.php
					alors que le plugin nécessite le stockage d\'une configuration.', $id));
			}

			$config = json_decode(file_get_contents($path . '/config.json'));

			if (is_null($config))
			{
				throw new \RuntimeException('config.json invalide. Erreur JSON: ' . json_last_error_msg());
			}
		}

		return $config;
	}

	/**
	 * Renvoie la version installée d'un plugin ou FALSE s'il n'est pas installé
	 * @param  string $id Identifiant du plugin
	 * @return mixed      Numéro de version du plugin ou FALSE
	 */
	static public function getInstalledVersion($id)
	{
		return DB::getInstance()->first('SELECT version FROM plugins WHERE id = ?;', $id);
	}

	/**
	 * Déclenche le signal donné auprès des plugins enregistrés
	 * @param  string $signal Nom du signal
	 * @param  array  $params Paramètres du callback (array ou null)
	 * @return NULL 		  NULL si aucun plugin n'a été appelé,
	 * TRUE si un plugin a été appelé et a arrêté l'exécution,
	 * FALSE si des plugins ont été appelés mais aucun n'a stoppé l'exécution
	 */
	static public function fireSignal($signal, $params = null, &$callback_return = null)
	{
		if (!self::$signals) {
			return null;
		}

		// Process SYSTEM_SIGNALS first
		foreach (SYSTEM_SIGNALS as $system_signal) {
			if (key($system_signal) != $signal) {
				continue;
			}

			if (!is_callable(current($system_signal))) {
				throw new \LogicException(sprintf('System signal: cannot call "%s" for signal "%s"', current($system_signal), key($system_signal)));
			}

			if (true === call_user_func_array(current($system_signal), [&$params, &$callback_return])) {
				return true;
			}
		}

		$list = DB::getInstance()->get('SELECT * FROM plugins_signals WHERE signal = ?;', $signal);

		if (!count($list)) {
			return null;
		}

		if (null === $params) {
			$params = [];
		}

		foreach ($list as $row)
		{
			$path = self::getPath($row->plugin);

			// Ne pas appeler les plugins dont le code n'existe pas/plus,
			if (!$path)
			{
				continue;
			}

			$params['plugin_root'] = $path;

			$return = call_user_func_array('Garradin\\Plugin\\' . $row->callback, [&$params, &$callback_return]);

			if (true === $return) {
				return true;
			}
		}

		return false;
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Added src/include/lib/Garradin/Plugins.php version [7fc9edb137].





















































































































































































































































































































































































































































































































































































































































































































































































































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

namespace Garradin;

use Garradin\Entities\Plugin;
use Garradin\Entities\Module;

use Garradin\Users\Session;
use Garradin\DB;
use Garradin\UserTemplate\CommonFunctions;
use Garradin\UserTemplate\Modules;

use \KD2\DB\EntityManager as EM;

use const Garradin\{SYSTEM_SIGNALS, ADMIN_URL, WWW_URL, PLUGINS_ROOT};

class Plugins
{
	const NAME_REGEXP = '[a-z][a-z0-9]*(?:_[a-z0-9]+)*';

	/**
	 * Set to false to disable signal firing
	 * @var boolean
	 */
	static protected $signals = true;

	static public function toggleSignals(bool $enabled)
	{
		self::$signals = $enabled;
	}

	static public function getPrivateURL(string $id, string $path = '')
	{
		return ADMIN_URL . 'p/' . $id . '/' . ltrim($path, '/');
	}

	static public function getPublicURL(string $id, string $path = '')
	{
		return WWW_URL . 'p/' . $id . '/' . ltrim($path, '/');
	}

	static public function getPath(string $name): ?string
	{
		if (file_exists(PLUGINS_ROOT . '/' . $name)) {
			return PLUGINS_ROOT . '/' . $name;
		}
		elseif (file_exists(PLUGINS_ROOT . '/' . $name . '.tar.gz')) {
			return 'phar://' . PLUGINS_ROOT . '/' . $name . '.tar.gz';
		}

		return null;
	}

	static public function exists(string $name): bool
	{
		return self::getPath($name) !== null;
	}

	/**
	 * Déclenche le signal donné auprès des plugins enregistrés
	 * @param  string $signal Nom du signal
	 * @param  array  $params Paramètres du callback (array ou null)
	 * @return NULL 		  NULL si aucun plugin n'a été appelé,
	 * TRUE si un plugin a été appelé et a arrêté l'exécution,
	 * FALSE si des plugins ont été appelés mais aucun n'a stoppé l'exécution
	 */
	static public function fireSignal($signal, $params = null, &$callback_return = null)
	{
		if (!self::$signals) {
			return null;
		}

		// Process SYSTEM_SIGNALS first
		foreach (SYSTEM_SIGNALS as $system_signal) {
			if (key($system_signal) != $signal) {
				continue;
			}

			if (!is_callable(current($system_signal))) {
				throw new \LogicException(sprintf('System signal: cannot call "%s" for signal "%s"', current($system_signal), key($system_signal)));
			}

			if (true === call_user_func_array(current($system_signal), [&$params, &$callback_return])) {
				return true;
			}
		}

		$list = DB::getInstance()->get('SELECT s.* FROM plugins_signals AS s INNER JOIN plugins p ON p.name = s.plugin
			WHERE s.signal = ? AND p.enabled = 1;', $signal);

		if (!count($list)) {
			return null;
		}

		if (null === $params) {
			$params = [];
		}

		foreach ($list as $row)
		{
			$path = self::getPath($row->plugin);

			// Ne pas appeler les plugins dont le code n'existe pas/plus,
			if (!$path) {
				continue;
			}

			$callback = 'Garradin\\Plugin\\' . $row->callback;

			if (!is_callable($callback)) {
				continue;
			}

			$params['plugin_root'] = $path;

			$return = call_user_func_array($callback, [&$params, &$callback_return]);

			if (true === $return) {
				return true;
			}
		}

		return false;
	}

	static public function listModulesAndPlugins(bool $installable = false): array
	{
		$list = [];

		if ($installable) {
			foreach (EM::getInstance(Module::class)->iterate('SELECT * FROM @TABLE WHERE enabled = 0;') as $m) {
				$list[$m->name] = ['module' => $m];
			}

			foreach (self::listInstallable() as $name => $p) {
				$list[$name] = ['plugin'   => $p];
			}

			foreach (self::listInstalled() as $p) {
				if ($p->enabled) {
					continue;
				}

				$list[$p->name] = ['plugin'   => $p];
			}
		}
		else {
			foreach (EM::getInstance(Module::class)->iterate('SELECT * FROM @TABLE WHERE enabled = 1;') as $m) {
				$list[$m->name] = ['module' => $m];
			}

			foreach (self::listInstalled() as $p) {
				if (!$p->enabled) {
					continue;
				}

				if (!$p->hasCode()) {
					$p->set('enabled', false);
					$p->save();
					continue;
				}

				$list[$p->name] = ['plugin'   => $p];
			}
		}

		foreach ($list as &$item) {
			$c = isset($item['plugin']) ? $item['plugin'] : $item['module'];
			$item['icon_url'] = $c->icon_url();
			$item['name'] = $c->name;
			$item['label'] = $c->label;
			$item['description'] = $c->description;
			$item['author'] = $c->author;
			$item['author_url'] = $c->author_url;
			$item['config_url'] = $c->hasConfig() ? $c->url($c::CONFIG_FILE) : null;
			$item['readme_url'] = $c->hasFile($c::README_FILE) ? $c->url($c::README_FILE) : null;
			$item['enabled'] = $c->enabled;
			$item['installed'] = isset($item['plugin']) ? $c->exists() : true;
			$item['broken'] = isset($item['plugin']) ? !$c->hasCode() : false;
			$item['broken_message'] = isset($item['plugin']) ? $c->getBrokenMessage() : false;
			$item['restrict_section'] = $c->restrict_section;
			$item['restrict_level'] = $c->restrict_level;

			$item['url'] = null;

			if ($c->hasFile($c::INDEX_FILE)) {
				$item['url'] = $c->url($c::INDEX_FILE);
			}
		}

		unset($item);

		usort($list, fn ($a, $b) => strnatcasecmp($a['label'] ?? $a['name'], $b['label'] ?? $b['name']));

		return $list;
	}

	static public function listModulesAndPluginsMenu(Session $session): array
	{
		$list = [];

		$sql = 'SELECT \'module\' AS type, name, label, restrict_section, restrict_level FROM modules WHERE menu = 1 AND enabled = 1
			UNION ALL
			SELECT \'plugin\' AS type, name, label, restrict_section, restrict_level FROM plugins WHERE menu = 1 AND enabled = 1;';

		foreach (DB::getInstance()->get($sql) as $item) {
			if ($item->restrict_section && !$session->canAccess($item->restrict_section, $item->restrict_level)) {
				continue;
			}

			$list[$item->type . '_' . $item->name] = $item;
		}

		// Sort items by label
		uasort($list, fn ($a, $b) => strnatcasecmp($a->label, $b->label));

		foreach ($list as &$item) {
			$item = sprintf('<a href="%s/%s/">%s</a>',
				$item->type == 'plugin' ? ADMIN_URL . 'p' : WWW_URL  . 'm',
				$item->name,
				$item->label
			);
		}

		unset($item);

		// Append plugins from signals
		self::fireSignal('menu.item', compact('session'), $list);

		return $list;
	}

	static public function listModulesAndPluginsHomeButtons(Session $session): array
	{
		$list = [];

		$sql = 'SELECT \'module\' AS type, name, label, restrict_section, restrict_level FROM modules WHERE home_button = 1 AND enabled = 1
			UNION ALL
			SELECT \'plugin\' AS type, name, label, restrict_section, restrict_level FROM plugins WHERE home_button = 1 AND enabled = 1;';

		foreach (DB::getInstance()->get($sql) as $item) {
			if ($item->restrict_section && !$session->canAccess($item->restrict_section, $item->restrict_level)) {
				continue;
			}

			$list[$item->type . '_' . $item->name] = $item;
		}

		// Sort items by label
		uasort($list, fn ($a, $b) => strnatcasecmp($a->label, $b->label));

		foreach ($list as &$item) {
			$url = sprintf('%s/%s/', $item->type == 'plugin' ? ADMIN_URL . 'p' : WWW_URL  . 'm', $item->name);
			$item = CommonFunctions::linkButton([
				'label' => $item->label,
				'icon' => $url . 'icon.svg',
				'href' => $url,
			]);
		}

		unset($item);

		foreach (Modules::snippets(Modules::SNIPPET_HOME_BUTTON) as $name => $v) {
			$list['module_' . $name] = $v;
		}

		Plugins::fireSignal('home.button', ['user' => $session->getUser(), 'session' => $session], $list);

		return $list;
	}

	static public function get(string $name): ?Plugin
	{
		return EM::findOne(Plugin::class, 'SELECT * FROM @TABLE WHERE name = ?;', $name);
	}

	static public function listInstalled(): array
	{
		return EM::getInstance(Plugin::class)->all('SELECT * FROM @TABLE ORDER BY label COLLATE NOCASE ASC;');
	}

	/**
	 * Liste les plugins téléchargés mais non installés
	 */
	static public function listInstallable(bool $check_exists = true): array
	{
		$list = [];

		if ($check_exists) {
			$exists = DB::getInstance()->getAssoc('SELECT name, name FROM plugins;');
		}
		else {
			$exists = [];
		}

		foreach (glob(PLUGINS_ROOT . '/*') as $file)
		{
			if (substr($file, 0, 1) == '.') {
				continue;
			}

			if (is_dir($file) && file_exists($file . '/' . Plugin::META_FILE)) {
				$file = basename($file);
				$name = $file;
			}
			elseif (substr($file, -7) == '.tar.gz') {
				$file = basename($file);
				$name = substr($file, 0, -7);
			}
			else {
				continue;
			}

			// Ignore existing plugins
			if (in_array($name, $exists)) {
				continue;
			}

			$list[$file] = null;

			$p = new Plugin;
			$p->name = $name;
			$p->updateFromINI();
			$list[$name] = $p;

			try {
				$p->selfCheck();
			}
			catch (ValidationException $e) {
				$p->setBrokenMessage($e->getMessage());
			}
		}

		ksort($list);

		return $list;
	}

	static public function install(string $name): void
	{
		$plugin = self::get($name);

		if ($plugin) {
			$plugin->set('enabled', true);
			$plugin->save();
			return;
		}

		$p = new Plugin;
		$p->name = $name;

		if (!$p->hasFile($p::META_FILE)) {
			throw new UserException(sprintf('Le plugin "%s" n\'est pas une extension Paheko : fichier plugin.ini manquant.', $name));
		}

		$p->updateFromINI();

		$db = DB::getInstance();
		$db->begin();
		$p->set('enabled', true);
		$p->save();

		if ($p->hasFile($p::INSTALL_FILE)) {
			$p->call($p::INSTALL_FILE, true);
		}

		$db->commit();
	}

	/**
	 * Upgrade all plugins if required
	 * This is run after an upgrade, a database restoration, or in the Plugins page
	 */
	static public function upgradeAllIfRequired(): bool
	{
		$i = 0;

		foreach (self::listInstalled() as $plugin) {
			// Ignore plugins if code is no longer available
			if (!$plugin->isAvailable()) {
				continue;
			}

			if ($plugin->needUpgrade()) {
				$plugin->upgrade();
				$i++;
			}

			unset($plugin);
		}

		return $i > 0;
	}
}

Modified src/include/lib/Garradin/Sauvegarde.php from [02a4118000] to [59a4725000].

524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
			// If logged-in user no longer exists, then login to first admin account
			if (!$session->refresh()) {
				$session->forceLogin(-1);
				$return |= self::CHANGED_USER;
			}

			// Check and upgrade plugins, if a software upgrade is necessary, plugins will be upgraded after the upgrade
			Plugin::upgradeAllIfRequired();
		}

		return $return;
	}

	public function restore(string $file)
	{







|







524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
			// If logged-in user no longer exists, then login to first admin account
			if (!$session->refresh()) {
				$session->forceLogin(-1);
				$return |= self::CHANGED_USER;
			}

			// Check and upgrade plugins, if a software upgrade is necessary, plugins will be upgraded after the upgrade
			Plugins::upgradeAllIfRequired();
		}

		return $return;
	}

	public function restore(string $file)
	{

Modified src/include/lib/Garradin/Services/Reminders.php from [90a6e75124] to [8c425b7b83].

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

namespace Garradin\Services;

use Garradin\Config;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\Users\DynamicFields;
use Garradin\Email\Emails;
use Garradin\Entities\Services\Reminder;
use KD2\DB\EntityManager;

use const Garradin\WWW_URL;







|







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

namespace Garradin\Services;

use Garradin\Config;
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Plugins;
use Garradin\Utils;
use Garradin\Users\DynamicFields;
use Garradin\Email\Emails;
use Garradin\Entities\Services\Reminder;
use KD2\DB\EntityManager;

use const Garradin\WWW_URL;
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
		$db->insert('services_reminders_sent', [
			'id_service'  => $reminder->id_service,
			'id_user'     => $reminder->id_user,
			'id_reminder' => $reminder->id_reminder,
			'due_date'    => $reminder->reminder_date,
		]);

		Plugin::fireSignal('reminder.send.after', $reminder);

		return true;
	}

	/**
	 * Envoi des rappels automatiques par e-mail
	 * @return boolean TRUE en cas de succès







|







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
		$db->insert('services_reminders_sent', [
			'id_service'  => $reminder->id_service,
			'id_user'     => $reminder->id_user,
			'id_reminder' => $reminder->id_reminder,
			'due_date'    => $reminder->reminder_date,
		]);

		Plugins::fireSignal('reminder.send.after', $reminder);

		return true;
	}

	/**
	 * Envoi des rappels automatiques par e-mail
	 * @return boolean TRUE en cas de succès

Modified src/include/lib/Garradin/Template.php from [8cc6ebbbda] to [1f5a88a96f].

91
92
93
94
95
96
97



98
99
100
101
102
103
104

		$session = null;

		if (!defined('Garradin\INSTALL_PROCESS')) {
			$session = Session::getInstance();
			$this->assign('config', Config::getInstance());
		}




		$is_logged = $session ? $session->isLogged() : null;

		$this->assign('session', $session);
		$this->assign('is_logged', $is_logged);
		$this->assign('logged_user', $is_logged ? $session->getUser() : null);








>
>
>







91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

		$session = null;

		if (!defined('Garradin\INSTALL_PROCESS')) {
			$session = Session::getInstance();
			$this->assign('config', Config::getInstance());
		}
		else {
			$this->assign('config', null);
		}

		$is_logged = $session ? $session->isLogged() : null;

		$this->assign('session', $session);
		$this->assign('is_logged', $is_logged);
		$this->assign('logged_user', $is_logged ? $session->getUser() : null);

168
169
170
171
172
173
174




175
176
177
178
179
180
181
		}

		foreach (CommonFunctions::FUNCTIONS_LIST as $key => $name) {
			$this->register_function(is_int($key) ? $name : $key, is_int($key) ? [CommonFunctions::class, $name] : $name);
		}

		$this->register_modifier('local_url', [Utils::class, 'getLocalURL']);




	}


	protected function formErrors($params)
	{
		$form = $this->getTemplateVars('form');








>
>
>
>







171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
		}

		foreach (CommonFunctions::FUNCTIONS_LIST as $key => $name) {
			$this->register_function(is_int($key) ? $name : $key, is_int($key) ? [CommonFunctions::class, $name] : $name);
		}

		$this->register_modifier('local_url', [Utils::class, 'getLocalURL']);

		// Overwrite default money modifiers
		$this->register_modifier('money', [CommonModifiers::class, 'html_money']);
		$this->register_modifier('money_currency', [CommonModifiers::class, 'html_money_currency']);
	}


	protected function formErrors($params)
	{
		$form = $this->getTemplateVars('form');

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
		}

		return $n;
	}

	protected function customColors()
	{
		$config = Config::getInstance();

		$c1 = ADMIN_COLOR1;
		$c2 = ADMIN_COLOR2;
		$bg = ADMIN_BACKGROUND_IMAGE;

		if (!FORCE_CUSTOM_COLORS) {
			$c1 = $config->get('color1') ?: $c1;
			$c2 = $config->get('color2') ?: $c2;

			if ($url = $config->fileURL('admin_background')) {
				$bg = $url;
			}
		}

		$out = '
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
			--gBgImage: url("%s");
		}
		</style>';

		if ($url = $config->fileURL('admin_css')) {
			$out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url);
		}

		return sprintf($out, CommonModifiers::css_hex_to_rgb($c1), CommonModifiers::css_hex_to_rgb($c2), $bg);
	}

	protected function displayDynamicField(array $params): string







|





|

















|







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
		}

		return $n;
	}

	protected function customColors()
	{
		$config = defined('Garradin\INSTALL_PROCESS') ? null : Config::getInstance();

		$c1 = ADMIN_COLOR1;
		$c2 = ADMIN_COLOR2;
		$bg = ADMIN_BACKGROUND_IMAGE;

		if (!FORCE_CUSTOM_COLORS && $config) {
			$c1 = $config->get('color1') ?: $c1;
			$c2 = $config->get('color2') ?: $c2;

			if ($url = $config->fileURL('admin_background')) {
				$bg = $url;
			}
		}

		$out = '
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
			--gBgImage: url("%s");
		}
		</style>';

		if ($config && $url = $config->fileURL('admin_css')) {
			$out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url);
		}

		return sprintf($out, CommonModifiers::css_hex_to_rgb($c1), CommonModifiers::css_hex_to_rgb($c2), $bg);
	}

	protected function displayDynamicField(array $params): string
577
578
579
580
581
582
583







584

585
586
587
588
589
590
591
592
593
594
595
596

		$out .= '</table>';
		return $out;
	}

	protected function displayPermissions(array $params): string
	{







		$perms = $params['permissions'];


		$out = [];

		foreach (Category::PERMISSIONS as $name => $config) {
			$access = $perms->{'perm_' . $name};
			$label = sprintf('%s : %s', $config['label'], $config['options'][$access]);
			$out[$name] = sprintf('<b class="access_%s %s" title="%s">%s</b>', $access, $name, htmlspecialchars($label), $config['shape']);
		}

		return implode(' ', $out);
	}
}







>
>
>
>
>
>
>
|
>
|
<

|








584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600

601
602
603
604
605
606
607
608
609
610

		$out .= '</table>';
		return $out;
	}

	protected function displayPermissions(array $params): string
	{
		$out = [];

		if (isset($params['section'], $params['level'])) {
			$list = [$params['section'] => Category::PERMISSIONS[$params['section']]];
			$perms = (object) ['perm_' . $params['section'] => $params['level']];
		}
		else {
			$perms = $params['permissions'];
			$list = Category::PERMISSIONS;
		}


		foreach ($list as $name => $config) {
			$access = $perms->{'perm_' . $name};
			$label = sprintf('%s : %s', $config['label'], $config['options'][$access]);
			$out[$name] = sprintf('<b class="access_%s %s" title="%s">%s</b>', $access, $name, htmlspecialchars($label), $config['shape']);
		}

		return implode(' ', $out);
	}
}

Modified src/include/lib/Garradin/Upgrade.php from [c62ef681a0] to [71ae118229].

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

	static public function upgrade()
	{
		$db = DB::getInstance();
		$backup = new Sauvegarde;
		$v = $db->version();

		Plugin::toggleSignals(false);

		Static_Cache::store('upgrade', 'Updating');

		// Créer une sauvegarde automatique
		$backup_file = sprintf(DATA_ROOT . '/association.pre_upgrade-%s.sqlite', garradin_version());
		$backup->make($backup_file);








|







49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

	static public function upgrade()
	{
		$db = DB::getInstance();
		$backup = new Sauvegarde;
		$v = $db->version();

		Plugins::toggleSignals(false);

		Static_Cache::store('upgrade', 'Updating');

		// Créer une sauvegarde automatique
		$backup_file = sprintf(DATA_ROOT . '/association.pre_upgrade-%s.sqlite', garradin_version());
		$backup->make($backup_file);

148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
				require ROOT . '/include/migrations/1.2/1.2.2.php';
			}

			if (version_compare($v, '1.3.0', '<')) {
				require ROOT . '/include/migrations/1.3/1.3.0.php';
			}

			Plugin::upgradeAllIfRequired();

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







|







148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
				require ROOT . '/include/migrations/1.2/1.2.2.php';
			}

			if (version_compare($v, '1.3.0', '<')) {
				require ROOT . '/include/migrations/1.3/1.3.0.php';
			}

			Plugins::upgradeAllIfRequired();

			// 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/CommonFunctions.php from [bc6f2d4af1] to [0663e40e65].

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

		// Extract params and keep attributes separated
		$attributes = array_diff_key($params, array_flip($params_list));
		$params = array_intersect_key($params, array_flip($params_list));
		extract($params, \EXTR_SKIP);

		if (!isset($name, $type)) {
			throw new \InvalidArgumentException('Missing name or type');
		}

		$suffix = null;

		if ($type == 'datetime') {
			$type = 'date';
			$tparams = func_get_arg(0);







|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

		// Extract params and keep attributes separated
		$attributes = array_diff_key($params, array_flip($params_list));
		$params = array_intersect_key($params, array_flip($params_list));
		extract($params, \EXTR_SKIP);

		if (!isset($name, $type)) {
			throw new \RuntimeException('Missing name or type');
		}

		$suffix = null;

		if ($type == 'datetime') {
			$type = 'date';
			$tparams = func_get_arg(0);
178
179
180
181
182
183
184




185
186
187
188
189
190
191
192
193




194
195
196
197
198
199
200
			$out = sprintf('<dd class="radio-btn">%s
				<label for="f_%s_%s"><div><h3>%s</h3>%s</div></label>
			</dd>', $radio, htmlspecialchars((string)$name), htmlspecialchars((string)$value), htmlspecialchars((string)$label), isset($params['help']) ? '<p class="help">' . htmlspecialchars($params['help']) . '</p>' : '');
			return $out;
		}
		if ($type == 'select') {
			$input = sprintf('<select %s>', $attributes_string);





			foreach ($options as $_key => $_value) {
				$input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value));
			}

			$input .= '</select>';
		}
		elseif ($type == 'select_groups') {
			$input = sprintf('<select %s>', $attributes_string);





			foreach ($options as $optgroup => $suboptions) {
				if (is_array($suboptions)) {
					$input .= sprintf('<optgroup label="%s">', htmlspecialchars((string)$optgroup));

					foreach ($suboptions as $_key => $_value) {
						$input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value));







>
>
>
>









>
>
>
>







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
			$out = sprintf('<dd class="radio-btn">%s
				<label for="f_%s_%s"><div><h3>%s</h3>%s</div></label>
			</dd>', $radio, htmlspecialchars((string)$name), htmlspecialchars((string)$value), htmlspecialchars((string)$label), isset($params['help']) ? '<p class="help">' . htmlspecialchars($params['help']) . '</p>' : '');
			return $out;
		}
		if ($type == 'select') {
			$input = sprintf('<select %s>', $attributes_string);

			if (empty($attributes['required'])) {
				$input .= '<option value=""></option>';
			}

			foreach ($options as $_key => $_value) {
				$input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value));
			}

			$input .= '</select>';
		}
		elseif ($type == 'select_groups') {
			$input = sprintf('<select %s>', $attributes_string);

			if (empty($attributes['required'])) {
				$input .= '<option value=""></option>';
			}

			foreach ($options as $optgroup => $suboptions) {
				if (is_array($suboptions)) {
					$input .= sprintf('<optgroup label="%s">', htmlspecialchars((string)$optgroup));

					foreach ($suboptions as $_key => $_value) {
						$input .= sprintf('<option value="%s"%s>%s</option>', $_key, $current_value == $_key ? ' selected="selected"' : '', htmlspecialchars((string)$_value));
299
300
301
302
303
304
305
306
307
308
309











310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327





328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371







372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389





390
391
392
393
394
395
396
397
398











399
400
401
402
403
404
405
406
407
408
409
410
411
412
		}

		return $out;
	}

	static public function icon(array $params): string
	{
		if (isset($params['html']) && $params['html'] == false) {
			return Utils::iconUnicode($params['shape']);
		}












		$label = $params['label'] ?? '';
		unset($params['label']);

		self::setIconAttribute($params);

		$attributes = array_diff_key($params, ['shape']);
		$attributes = array_map(fn($v, $k) => sprintf('%s="%s"', $k, htmlspecialchars($v)),
			$attributes, array_keys($attributes));

		$attributes = implode(' ', $attributes);

		return sprintf('<span %s>%s</span>', $attributes, htmlspecialchars($label));
	}

	static public function link(array $params): string
	{
		$href = $params['href'];
		$label = $params['label'];






		// href can be prefixed with '!' to make the URL relative to ADMIN_URL
		if (substr($href, 0, 1) == '!') {
			$href = ADMIN_URL . substr($params['href'], 1);
		}

		// propagate _dialog param if we are in an iframe
		if (isset($_GET['_dialog']) && !isset($params['target'])) {
			$href .= (strpos($href, '?') === false ? '?' : '&') . '_dialog';
		}

		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		unset($params['href'], $params['label']);

		array_walk($params, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, htmlspecialchars($v));
		});

		$params = implode(' ', $params);

		return sprintf('<a href="%s" %s><span>%s</span></a>', htmlspecialchars($href), $params, htmlspecialchars($label));
	}

	static public function button(array $params): string
	{
		$label = isset($params['label']) ? htmlspecialchars($params['label']) : '';
		unset($params['label']);

		self::setIconAttribute($params);

		if (!isset($params['type'])) {
			$params['type'] = 'button';
		}

		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		if (isset($params['name']) && !isset($params['value'])) {
			$params['value'] = 1;
		}








		$params['class'] .= ' icn-btn';

		// Remove NULL params
		$params = array_filter($params);

		array_walk($params, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, htmlspecialchars($v));
		});

		$params = implode(' ', $params);

		return sprintf('<button %s>%s</button>', $params, $label);
	}

	static public function linkbutton(array $params): string
	{
		self::setIconAttribute($params);






		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		$params['class'] .= ' icn-btn';

		return self::link($params);
	}












	static protected function setIconAttribute(array &$params): void
	{
		if (isset($params['shape'])) {
			$params['data-icon'] = Utils::iconUnicode($params['shape']);
		}
		elseif (isset($params['icon'])) {
			$params['data-custom-icon'] = true;
			$params['style'] = sprintf('--custom-icon: url(\'%s\')', $params['icon']);
		}

		unset($params['icon'], $params['shape']);
	}
}







|



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










|






>
>
>
>
>















|







|




















>
>
>
>
>
>
>












|





>
>
>
>
>









>
>
>
>
>
>
>
>
>
>
>






<
<
<
|
<
|


307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451



452

453
454
455
		}

		return $out;
	}

	static public function icon(array $params): string
	{
		if (isset($params['shape']) && isset($params['html']) && $params['html'] == false) {
			return Utils::iconUnicode($params['shape']);
		}

		if (!isset($params['shape']) && !isset($params['url'])) {
			throw new \RuntimeException('Missing parameter: shape or url');
		}

		$html = '';

		if (isset($params['url'])) {
			$html = self::getIconHTML(['icon' => $params['url']]);
			unset($params['url']);
		}

		$html .= htmlspecialchars($params['label'] ?? '');
		unset($params['label']);

		self::setIconAttribute($params);

		$attributes = array_diff_key($params, ['shape']);
		$attributes = array_map(fn($v, $k) => sprintf('%s="%s"', $k, htmlspecialchars($v)),
			$attributes, array_keys($attributes));

		$attributes = implode(' ', $attributes);

		return sprintf('<span %s>%s</span>', $attributes, $html);
	}

	static public function link(array $params): string
	{
		$href = $params['href'];
		$label = $params['label'];
		$prefix = $params['prefix'] ?? '';

		if (!$href || !$label) {
			return '';
		}

		// href can be prefixed with '!' to make the URL relative to ADMIN_URL
		if (substr($href, 0, 1) == '!') {
			$href = ADMIN_URL . substr($params['href'], 1);
		}

		// propagate _dialog param if we are in an iframe
		if (isset($_GET['_dialog']) && !isset($params['target'])) {
			$href .= (strpos($href, '?') === false ? '?' : '&') . '_dialog';
		}

		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		unset($params['href'], $params['label'], $params['prefix']);

		array_walk($params, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, htmlspecialchars($v));
		});

		$params = implode(' ', $params);

		return sprintf('<a href="%s" %s>%s<span>%s</span></a>', htmlspecialchars($href), $params, $prefix, htmlspecialchars($label));
	}

	static public function button(array $params): string
	{
		$label = isset($params['label']) ? htmlspecialchars($params['label']) : '';
		unset($params['label']);

		self::setIconAttribute($params);

		if (!isset($params['type'])) {
			$params['type'] = 'button';
		}

		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		if (isset($params['name']) && !isset($params['value'])) {
			$params['value'] = 1;
		}

		$prefix = '';

		if (isset($params['icon'])) {
			$prefix = self::getIconHTML($params);
			unset($params['icon'], $params['icon_html']);
		}

		$params['class'] .= ' icn-btn';

		// Remove NULL params
		$params = array_filter($params);

		array_walk($params, function (&$v, $k) {
			$v = sprintf('%s="%s"', $k, htmlspecialchars($v));
		});

		$params = implode(' ', $params);

		return sprintf('<button %s>%s%s</button>', $params, $prefix, $label);
	}

	static public function linkbutton(array $params): string
	{
		self::setIconAttribute($params);

		if (isset($params['icon']) || isset($params['icon_html'])) {
			$params['prefix'] = self::getIconHTML($params);
			unset($params['icon'], $params['icon_html']);
		}

		if (!isset($params['class'])) {
			$params['class'] = '';
		}

		$params['class'] .= ' icn-btn';

		return self::link($params);
	}

	static protected function getIconHTML(array $params): string
	{
		if (isset($params['icon_html'])) {
			return '<i class="icon">' . $params['icon_html'] . '</i>';
		}

		return sprintf('<svg class="icon"><use xlink:href="%s#img" href="%1$s#img"></use></svg> ',
			htmlspecialchars(Utils::getLocalURL($params['icon']))
		);
	}

	static protected function setIconAttribute(array &$params): void
	{
		if (isset($params['shape'])) {
			$params['data-icon'] = Utils::iconUnicode($params['shape']);
		}





		unset($params['shape']);
	}
}

Modified src/include/lib/Garradin/UserTemplate/CommonModifiers.php from [0bf9c1d61a] to [1fa2bef475].

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
		return call_user_func_array($name, $arguments);
	}

	const MODIFIERS_LIST = [
		'money',
		'money_raw',
		'money_currency',


		'relative_date',
		'relative_date_short',
		'date_short',
		'date_long',
		'date_hour',
		'date',
		'strftime',
		'size_in_bytes' => [Utils::class, 'format_bytes'],
		'typo',
		'css_hex_to_rgb',
	];

	/**
	 * See also money/money_currency in UserTemplate (overriden)
	 */
	static public function money($number, bool $hide_empty = true, bool $force_sign = false): string
	{
		if ($hide_empty && !$number) {
			return '';
		}

		$sign = ($force_sign && $number > 0) ? '+' : '';




		return sprintf('<span class="money">%s</span>', $sign . Utils::money_format($number, ',', '&nbsp;', $hide_empty));



	}

	static public function money_raw($number, bool $hide_empty = true): string
	{
		return Utils::money_format($number, ',', '', $hide_empty);
	}

	static public function money_currency($number, bool $hide_empty = true): string
	{
		$out = self::money($number, $hide_empty);

		if ($out !== '') {
			$out .= '&nbsp;' . Config::getInstance()->get('currency');
		}

		return $out;
	}











	static public function date_long($ts, bool $with_hour = false): ?string
	{
		return Utils::strftime_fr($ts, '%A %e %B %Y' . ($with_hour ? ' à %Hh%M' : ''));
	}

	static public function date_short($ts, bool $with_hour = false): ?string
	{
		return Utils::date_fr($ts, 'd/m/Y' . ($with_hour ? ' à H\hi' : ''));
	}

	static public function date_hour($ts, bool $minutes_only_if_required = false): ?string
	{
		$ts = Utils::get_datetime($ts);

		if (null === $ts) {







>
>












<
<
<
|







>
>
>
|
>
>
>







|

|


|




>
>
>
>
>
>
>
>
>
>








|







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
		return call_user_func_array($name, $arguments);
	}

	const MODIFIERS_LIST = [
		'money',
		'money_raw',
		'money_currency',
		'money_html',
		'money_currency_html',
		'relative_date',
		'relative_date_short',
		'date_short',
		'date_long',
		'date_hour',
		'date',
		'strftime',
		'size_in_bytes' => [Utils::class, 'format_bytes'],
		'typo',
		'css_hex_to_rgb',
	];




	static public function money($number, bool $hide_empty = true, bool $force_sign = false, bool $html = false): string
	{
		if ($hide_empty && !$number) {
			return '';
		}

		$sign = ($force_sign && $number > 0) ? '+' : '';

		$out = $sign . Utils::money_format($number, ',', $html ? '&nbsp;' : ' ', $hide_empty);

		if ($html) {
			$out = sprintf('<span class="money">%s</span>', $out);
		}

		return $out;
	}

	static public function money_raw($number, bool $hide_empty = true): string
	{
		return Utils::money_format($number, ',', '', $hide_empty);
	}

	static public function money_currency($number, bool $hide_empty = true, bool $force_sign = false, bool $html = false): string
	{
		$out = self::money($number, $hide_empty, $force_sign, $html);

		if ($out !== '') {
			$out .= ($html ? '&nbsp;' : ' ') . Config::getInstance()->get('currency');
		}

		return $out;
	}

	static public function html_money($number, bool $hide_empty = true, bool $force_sign = false): string
	{
		return self::money($number, $hide_empty, $force_sign, false);
	}

	static public function html_money_currency($number, bool $hide_empty = true, bool $force_sign = false): string
	{
		return self::money_currency($number, $hide_empty, $force_sign, false);
	}

	static public function date_long($ts, bool $with_hour = false): ?string
	{
		return Utils::strftime_fr($ts, '%A %e %B %Y' . ($with_hour ? ' à %Hh%M' : ''));
	}

	static public function date_short($ts, bool $with_hour = false): ?string
	{
		return Utils::shortDate($ts, $with_hour);
	}

	static public function date_hour($ts, bool $minutes_only_if_required = false): ?string
	{
		$ts = Utils::get_datetime($ts);

		if (null === $ts) {

Modified src/include/lib/Garradin/UserTemplate/Functions.php from [eb3ad6a62e] to [4ba2ad1378].

1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
17
18


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

34
35
























36
37
38
39
40

41
42
43
44
45
46
47
<?php

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\ErrorManager;
use KD2\JSONSchema;

use Garradin\Config;
use Garradin\DB;

use Garradin\Template;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Email\Emails;
use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Entities\Module;



use const Garradin\{ROOT, WWW_URL};

class Functions
{
	const FUNCTIONS_LIST = [
		'include',
		'http',
		'debug',
		'error',
		'read',
		'save',
		'admin_header',
		'admin_footer',
		'signature',

		'mail',
	];

























	static public function admin_header(array $params): string
	{
		$tpl = Template::getInstance();
		$tpl->assign($params);

		return $tpl->fetch('_head.tpl');
	}

	static public function admin_footer(array $params): string
	{
		$tpl = Template::getInstance();
		$tpl->assign($params);











>







>
>















>


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





>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
<?php

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\ErrorManager;
use KD2\JSONSchema;

use Garradin\Config;
use Garradin\DB;
use Garradin\Plugins;
use Garradin\Template;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Email\Emails;
use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Entities\Module;
use Garradin\Entities\User\Email;
use Garradin\Users\Session;

use const Garradin\{ROOT, WWW_URL};

class Functions
{
	const FUNCTIONS_LIST = [
		'include',
		'http',
		'debug',
		'error',
		'read',
		'save',
		'admin_header',
		'admin_footer',
		'signature',
		'captcha',
		'mail',
	];

	const COMPILE_FUNCTIONS_LIST = [
		':break' => [self::class, 'break'],
	];

	/**
	 * Compile function to break inside a loop
	 */
	static public function break(string $name, string $params, Brindille $tpl, int $line)
	{
		$in_loop = false;
		foreach ($tpl->_stack as $element) {
			if ($element[0] == $tpl::SECTION) {
				$in_loop = true;
				break;
			}
		}

		if (!$in_loop) {
			throw new Brindille_Exception(sprintf('Error on line %d: break can only be used inside a section', $line));
		}

		return '<?php break; ?>';
	}

	static public function admin_header(array $params): string
	{
		$tpl = Template::getInstance();
		$tpl->assign($params);
		$tpl->assign('plugins_menu', Plugins::listModulesAndPluginsMenu(Session::getInstance()));
		return $tpl->fetch('_head.tpl');
	}

	static public function admin_footer(array $params): string
	{
		$tpl = Template::getInstance();
		$tpl->assign($params);
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
				$tpl->assign($assign_new_id, $db->lastInsertId());
			}
		}
		else {
			$db->update($table, compact('document'), sprintf('%s = :match', $field), ['match' => $where_value]);
		}
	}









































	static public function mail(array $params, Brindille $tpl, int $line)
	{
		if (empty($params['to'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: argument "to" manquant pour la fonction "mail"', $line));
		}

		if (empty($params['subject'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: argument "subject" manquant pour la fonction "mail"', $line));
		}

		if (empty($params['body'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: argument "body" manquant pour la fonction "mail"', $line));
		}












































		Emails::queue(Emails::CONTEXT_PRIVATE, [$params['to']], null, $params['subject'], $params['body']);



	}

	static public function debug(array $params, Brindille $tpl)
	{
		if (!count($params)) {
			$params = $tpl->getAllVariables();
		}







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















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







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
				$tpl->assign($assign_new_id, $db->lastInsertId());
			}
		}
		else {
			$db->update($table, compact('document'), sprintf('%s = :match', $field), ['match' => $where_value]);
		}
	}

	static public function captcha(array $params, Brindille $tpl, int $line)
	{
		$secret = md5(SECRET_KEY . Utils::getSelfURL(false));

		if (isset($params['html'])) {
			$c = Security::createCaptcha($secret, $params['lang'] ?? 'fr');
			return sprintf('<label for="f_c_42">Merci d\'écrire <strong><q>%s</q></strong> en chiffres&nbsp;:</label>
				<input type="text" name="f_c_42" id="f_c_42" placeholder="Exemple : 1234" />
				<input type="hidden" name="f_c_43" value="%s" />',
				$c['spellout'], $c['hash']);
		}
		elseif (isset($params['assign_hash']) && isset($params['assign_number'])) {
			$c = Security::createCaptcha($secret, $params['lang'] ?? 'fr');
			$tpl->assign($params['assign_hash'], $c['hash']);
			$tpl->assign($params['assign_number'], $c['spellout']);
		}
		elseif (isset($params['verify'])) {
			$hash = $_POST['f_c_43'] ?? '';
			$number = $_POST['f_c_42'] ?? '';
		}
		elseif (array_key_exists('verify_number', $params)) {
			$hash = $params['verify_hash'] ?? '';
			$number = $params['verify_number'] ?? '';
		}
		else {
			throw new Brindille_Exception(sprintf('Line %d: no valid arguments supplied for "captcha" function', $line));
		}

		$error = 'Réponse invalide à la vérification anti-robot';

		if (!Security::checkCaptcha($secret, trim($hash), trim($number))) {
			if (isset($params['assign_error'])) {
				$tpl->assign($params['assign_error'], $error);
			}
			else {
				throw new UserException($error);
			}
		}
	}

	static public function mail(array $params, Brindille $tpl, int $line)
	{
		if (empty($params['to'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: argument "to" manquant pour la fonction "mail"', $line));
		}

		if (empty($params['subject'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: argument "subject" manquant pour la fonction "mail"', $line));
		}

		if (empty($params['body'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: argument "body" manquant pour la fonction "mail"', $line));
		}

		if (!empty($params['block_urls']) && preg_match('!https?://!', $params['subject'] . $params['body'])) {
			throw new UserException('Merci de ne pas inclure d\'adresse web (http:…) dans le message');
		}

		static $external = 0;
		static $internal = 0;

		if (is_string($params['to'])) {
			$params['to'] = [$params['to']];
		}

		if (!count($params['to'])) {
			throw new Brindille_Exception(sprintf('Ligne %d: aucune adresse destinataire n\'a été précisée pour la fonction "mail"', $line));
		}

		foreach ($params['to'] as &$to) {
			$to = trim($to);
			Email::validateAddress($to);
		}

		unset($to);

		$db = DB::getInstance();
		$internal_count = $db->count('users', $db->where($email_field, 'IN', $params['to']));
		$external_count = count($params['to']) - $internal_count;

		if (($external_count + $external) > 1) {
			throw new Brindille_Exception(sprintf('Ligne %d: l\'envoi d\'email à une adresse externe est limité à un envoi par page', $line));
		}

		if (($internal_count + $internal) > 10) {
			throw new Brindille_Exception(sprintf('Ligne %d: l\'envoi d\'email à une adresse interne est limité à un envoi par page', $line));
		}

		if ($external_count && preg_match_all('!(https?://.*?)(?=\s|$)!', $params['subject'] . ' ' . $params['body'], $match, PREG_PATTERN_ORDER)) {
			foreach ($match[1] as $m) {
				if (0 !== strpos($m, WWW_URL) && 0 !== strpos($m, ADMIN_URL)) {
					throw new Brindille_Exception(sprintf('Ligne %d: l\'envoi d\'email à une adresse externe interdit l\'utilisation d\'une adresse web autre que le site de l\'association : %s', $line, $m));
				}
			}
		}

		$context = count($params['to']) == 1 ? Emails::CONTEXT_PRIVATE : Emails::CONTEXT_BULK;
		Emails::queue($context, $params['to'], null, $params['subject'], $params['body']);

		$internal += $internal_count;
		$external_count += $external_count;
	}

	static public function debug(array $params, Brindille $tpl)
	{
		if (!count($params)) {
			$params = $tpl->getAllVariables();
		}
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
		}

		$params['included_from'] = array_merge($from, [$path]);

		$include->assignArray(array_merge($ut->getAllVariables(), $params));

		if (!empty($params['capture']) && preg_match('/^[a-z0-9_]+$/', $params['capture'])) {
			$ut::__assign([$params['capture'] => $include->fetch()], $ut);
		}
		else {
			$include->display();
		}

		if (isset($params['keep'])) {
			$keep = explode(',', $params['keep']);
			$keep = array_map('trim', $keep);

			foreach ($keep as $name) {
				// Transmit variables
				$ut::__assign(['var' => $name, 'value' => $include->get($name)], $ut);
			}
		}

		// Transmit nocache to parent template
		if ($include->get('nocache')) {
			$ut::__assign(['nocache' => true], $ut);
		}
	}

	static public function http(array $params, UserTemplate $tpl): void
	{
		if (headers_sent()) {
			return;







|











|





|







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
		}

		$params['included_from'] = array_merge($from, [$path]);

		$include->assignArray(array_merge($ut->getAllVariables(), $params));

		if (!empty($params['capture']) && preg_match('/^[a-z0-9_]+$/', $params['capture'])) {
			$ut::__assign([$params['capture'] => $include->fetch()], $ut, $line);
		}
		else {
			$include->display();
		}

		if (isset($params['keep'])) {
			$keep = explode(',', $params['keep']);
			$keep = array_map('trim', $keep);

			foreach ($keep as $name) {
				// Transmit variables
				$ut::__assign(['var' => $name, 'value' => $include->get($name)], $ut, $line);
			}
		}

		// Transmit nocache to parent template
		if ($include->get('nocache')) {
			$ut::__assign(['nocache' => true], $ut, $line);
		}
	}

	static public function http(array $params, UserTemplate $tpl): void
	{
		if (headers_sent()) {
			return;

Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [c3be780789] to [d3236a70b7].

33
34
35
36
37
38
39


40
41
42
43
44
45
46
		'math',
		'money_int' => [Utils::class, 'moneyToInteger'],
		'array_transpose' => [Utils::class, 'array_transpose'],
		'check_email',
		'implode',
		'quote_sql_identifier',
		'quote_sql',


	];

	const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/';

	static public function replace($str, $find, $replace = null): string
	{
		if (is_array($find) && null === $replace) {







>
>







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
		'math',
		'money_int' => [Utils::class, 'moneyToInteger'],
		'array_transpose' => [Utils::class, 'array_transpose'],
		'check_email',
		'implode',
		'quote_sql_identifier',
		'quote_sql',
		'sql_where',
		'urlencode',
	];

	const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/';

	static public function replace($str, $find, $replace = null): string
	{
		if (is_array($find) && null === $replace) {
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










		if (!is_array($array)) {
			throw new Brindille_Exception('Supplied argument is not an array');
		}

		return implode($separator, $array);
	}

	static public function quote_sql_identifier($in): string
	{
		if (null === $in) {
			return '';
		}

		$db = DB::getInstance();

		if (is_array($in)) {
			return array_map([$db, 'quoteIdentifier'], $in);
		}

		return $db->quoteIdentifier($in);
	}

	static public function quote_sql($in): string
	{
		if (null === $in) {
			return '';
		}

		$db = DB::getInstance();

		if (is_array($in)) {
			return array_map([$db, 'quote'], $in);
		}

		return $db->quote($in);
	}
}

















|














|













|
>
>
>
>
>
>
>
>
>
>
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
		if (!is_array($array)) {
			throw new Brindille_Exception('Supplied argument is not an array');
		}

		return implode($separator, $array);
	}

	static public function quote_sql_identifier($in)
	{
		if (null === $in) {
			return '';
		}

		$db = DB::getInstance();

		if (is_array($in)) {
			return array_map([$db, 'quoteIdentifier'], $in);
		}

		return $db->quoteIdentifier($in);
	}

	static public function quote_sql($in)
	{
		if (null === $in) {
			return '';
		}

		$db = DB::getInstance();

		if (is_array($in)) {
			return array_map([$db, 'quote'], $in);
		}

		return $db->quote($in);
	}

	static public function sql_where(...$args)
	{
		return DB::getInstance()->where(...$args);
	}

	static public function urlencode($str): string
	{
		return rawurlencode($str ?? '');
	}
}

Modified src/include/lib/Garradin/UserTemplate/Modules.php from [e01ba62dfa] to [e1712e8d0b].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
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
<?php

namespace Garradin\UserTemplate;

use Garradin\Entities\Module;

use Garradin\Files\Files;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;


use const Garradin\ROOT;

use \KD2\DB\EntityManager as EM;

class Modules
{
	// Shortcuts so that code calling snippets method don't have to use Module entity
	const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION;
	const SNIPPET_USER = Module::SNIPPET_USER;
	const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON;

	/**
	 * Lists all modules from files and stores a cache
	 */
	static public function refresh(): void
	{
		$existing = DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s;', Module::TABLE));
		$list = [];

		foreach (Files::list(Module::ROOT) as $file) {
			if ($file->type != $file::TYPE_DIRECTORY) {
				continue;
			}

			$list[] = $file->name;
		}

		foreach (glob(Module::DIST_ROOT . '/*') as $file) {
			if (!is_dir($file)) {
				continue;
			}

			$list[] = Utils::basename($file);
		}

		$list = array_unique($list);
		sort($list);

		$create = array_diff($list, $existing);
		$delete = array_diff($existing, $list);
		$existing = array_diff($list, $create);

		foreach ($create as $name) {
			self::create($name);
		}

		foreach ($delete as $name) {
			self::get($name)->delete();
		}

		foreach ($existing as $name) {
			$f = self::get($name);
			$f->updateFromJSON();
			$f->save();
			$f->updateTemplates();
		}
	}
























































	static public function create(string $name): ?Module
	{
		$module = new Module;
		$module->name = $name;

		if (!$module->updateFromJSON()) {
			return null;
		}

		$module->save();
		$module->updateTemplates();
		return $module;
	}




	static public function list(): array
	{
		return EM::getInstance(Module::class)->all('SELECT * FROM @TABLE ORDER BY label COLLATE NOCASE ASC;');
	}

	static public function snippetsAsString(string $snippet, array $variables = []): string
	{
		return implode("\n", self::snippets($snippet, $variables));
	}

	static public function snippets(string $snippet, array $variables = []): array
	{
		$out = [];

		foreach (self::listForSnippet($snippet) as $module) {
			$out[] = $module->fetch($snippet, $variables);
		}

		return $out;
	}

	static public function listForSnippet(string $snippet): array
	{
		return EM::getInstance(Module::class)->all('SELECT f.* FROM @TABLE f
			INNER JOIN modules_templates t ON t.id_module = f.id
			WHERE t.name = ? AND f.enabled = 1










>


















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















|





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





|








>
>
>















|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
<?php

namespace Garradin\UserTemplate;

use Garradin\Entities\Module;

use Garradin\Files\Files;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Users\Session;

use const Garradin\ROOT;

use \KD2\DB\EntityManager as EM;

class Modules
{
	// Shortcuts so that code calling snippets method don't have to use Module entity
	const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION;
	const SNIPPET_USER = Module::SNIPPET_USER;
	const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON;

	/**
	 * Lists all modules from files and stores a cache
	 */
	static public function refresh(): void
	{
		$existing = DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s;', Module::TABLE));
		$list = self::listRaw();




















		$create = array_diff($list, $existing);
		$delete = array_diff($existing, $list);
		$existing = array_diff($list, $create);

		foreach ($create as $name) {
			self::create($name);
		}

		foreach ($delete as $name) {
			self::get($name)->delete();
		}

		foreach ($existing as $name) {
			$f = self::get($name);
			$f->updateFromINI();
			$f->save();
			$f->updateTemplates();
		}
	}

	/**
	 * List modules names from locally installed directories
	 */
	static public function listRaw(bool $include_installed = true): array
	{
		$list = [];

		// First list modules bundled
		foreach (glob(Module::DIST_ROOT . '/*') as $file) {
			if (!is_dir($file)) {
				continue;
			}

			$name = Utils::basename($file);
			$list[$name] = $name;
		}

		if ($include_installed) {
			// Then add modules in files
			foreach (Files::list(Module::ROOT) as $file) {
				if ($file->type != $file::TYPE_DIRECTORY) {
					continue;
				}

				$list[$file->name] = $file->name;
			}
		}

		sort($list);
		return $list;
	}

	/**
	 * List locally installed modules, directly from the filesystem, without creating them in the database cache
	 * (used in Install form)
	 */
	static public function listLocal(): array
	{
		$list = self::listRaw(false);
		$out = [];

		foreach ($list as $name) {
			$m = new Module;
			$m->name = $name;

			if (!$m->updateFromINI(false)) {
				continue;
			}

			$out[$name] = $m;
		}

		return $out;
	}

	static public function create(string $name): ?Module
	{
		$module = new Module;
		$module->name = $name;

		if (!$module->updateFromINI()) {
			return null;
		}

		$module->save();
		$module->updateTemplates();
		return $module;
	}

	/**
	 * List modules from the database
	 */
	static public function list(): array
	{
		return EM::getInstance(Module::class)->all('SELECT * FROM @TABLE ORDER BY label COLLATE NOCASE ASC;');
	}

	static public function snippetsAsString(string $snippet, array $variables = []): string
	{
		return implode("\n", self::snippets($snippet, $variables));
	}

	static public function snippets(string $snippet, array $variables = []): array
	{
		$out = [];

		foreach (self::listForSnippet($snippet) as $module) {
			$out[$module->name] = $module->fetch($snippet, $variables);
		}

		return array_filter($out, fn($a) => trim($a) !== '');
	}

	static public function listForSnippet(string $snippet): array
	{
		return EM::getInstance(Module::class)->all('SELECT f.* FROM @TABLE f
			INNER JOIN modules_templates t ON t.id_module = f.id
			WHERE t.name = ? AND f.enabled = 1

Modified src/include/lib/Garradin/UserTemplate/Sections.php from [544ac36926] to [12e48946c9].

36
37
38
39
40
41
42
43































44

































45
46
47
48
49
50
51
		'transaction_users',
		'accounts',
		'balances',
		'sql',
		'restrict',
		'module',
	];
































	static protected $_cache = [];


































	static protected function _debug(string $str): void
	{
		echo sprintf('<pre style="padding: 5px; margin: 5px; background: yellow; white-space: pre-wrap;">%s</pre>', htmlspecialchars($str));
	}

	static protected function _debugExplain(string $sql): void








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

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







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
		'transaction_users',
		'accounts',
		'balances',
		'sql',
		'restrict',
		'module',
	];

	const COMPILE_SECTIONS_LIST = [
		'#select' => [self::class, 'selectStart'],
		'/select' => [self::class, 'selectEnd'],
	];

	/**
	 * List of tables and columns that are restricted in SQL queries
	 *
	 * ~column means the column will always be returned as NULL
	 * -column or !table means trying to access this column or table will return an error
	 * see KD2/DB/SQLite3 code for details
	 *
	 * Note: column restrictions are only possible with PHP >= 8.0
	 */
	const SQL_TABLES = [
		// Allow access to all tables
		'*' => null,
		// Restrict access to private fields in users
		'users' => ['~password', '~pgp_key', '~otp_secret'],
		// Restrict access to some private tables
		'!emails' => null,
		'!emails_queue' => null,
		'!compromised_passwords_cache' => null,
		'!compromised_passwords_cache_ranges' => null,
		'!api_credentials' => null,
		'!plugins_signals' => null,
		'!config' => null,
		'!users_sessions' => null,
		'!logs' => null,
	];

	static protected $_cache = [];

	static public function selectStart(string $name, string $sql, UserTemplate $tpl, int $line): string
	{
		$sql = strtok($sql, ';');
		$extra_params = strtok(false);

		$i = 0;
		$params = '';

		$sql = preg_replace_callback('/\{(.*?)\}/', function ($match) use (&$params, &$i) {
			// Raw SQL
			if ('!' === substr($match[1], 0, 1)) {
				$params .= ' !' . $i . '=' . substr($match[1], 1);
				return '!' . $i++;
			}
			else {
				$params .= ' :p' . $i . '=' . $match[1];
				return ':p' . $i++;
			}
		}, $sql);

		$sql = 'SELECT ' . $sql;
		$sql = var_export($sql, true);

		$params .= ' sql=' . $sql . ' ' . $extra_params;

		return $tpl->_section('sql', $params, $line);
	}

	static public function selectEnd(string $name, string $params, UserTemplate $tpl, int $line): string
	{
		return $tpl->_close('sql', '{{/select}}');
	}

	static protected function _debug(string $str): void
	{
		echo sprintf('<pre style="padding: 5px; margin: 5px; background: yellow; white-space: pre-wrap;">%s</pre>', htmlspecialchars($str));
	}

	static protected function _debugExplain(string $sql): void
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
		}

		$id_field = DynamicFields::getNameFieldsSQL();
		$login_field = DynamicFields::getLoginField();
		$number_field = DynamicFields::getNumberField();

		if (empty($params['select'])) {
			$params['select'] = '1';
		}

		$params['select'] .= sprintf(', %s AS _name, %s AS _login, %s AS _number',
			$id_field, $login_field, $number_field);
		$params['tables'] = 'users';

		if (isset($params['id'])) {







|







519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
		}

		$id_field = DynamicFields::getNameFieldsSQL();
		$login_field = DynamicFields::getLoginField();
		$number_field = DynamicFields::getNumberField();

		if (empty($params['select'])) {
			$params['select'] = '*';
		}

		$params['select'] .= sprintf(', %s AS _name, %s AS _login, %s AS _number',
			$id_field, $login_field, $number_field);
		$params['tables'] = 'users';

		if (isset($params['id'])) {
591
592
593
594
595
596
597





598
599
600
601
602
603
604

	static public function restrict(array $params, UserTemplate $tpl, int $line): ?\Generator
	{
		$session = Session::getInstance();

		if (!$session->isLogged()) {
			if (!empty($params['block'])) {





				throw new UserException('Vous n\'avez pas accès à cette page.');
			}

			return null;
		}









>
>
>
>
>







655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673

	static public function restrict(array $params, UserTemplate $tpl, int $line): ?\Generator
	{
		$session = Session::getInstance();

		if (!$session->isLogged()) {
			if (!empty($params['block'])) {
				if (!headers_sent()) {
					// FIXME: implement redirect to correct URL after login
					Utils::redirect('!login.php');
				}

				throw new UserException('Vous n\'avez pas accès à cette page.');
			}

			return null;
		}


897
898
899
900
901
902
903












904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933

934
935
936
937
938
939
940
941
942
943
944
945
			'select' => '*',
			'order' => '1',
			'begin' => 0,
			'limit' => 100,
			'where' => '',
		];













		if (!isset($params['tables'])) {
			throw new Brindille_Exception('Missing parameter "tables"');
		}

		foreach ($defaults as $key => $default_value) {
			if (!isset($params[$key])) {
				$params[$key] = $default_value;
			}
		}

		// 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 %s ORDER BY %s LIMIT %d,%d;',
			$params['select'],
			$params['tables'],
			$params['where'] ?? '',
			isset($params['group']) ? 'GROUP BY ' . $params['group'] : '',
			isset($params['having']) ? 'HAVING ' . $params['having'] : '',
			$params['order'],
			$params['begin'],
			$params['limit']
		);


		$db = DB::getInstance();

		try {
			$statement = $db->protectSelect(null, $sql);

			$args = [];

			foreach ($params as $key => $value) {
				if (substr($key, 0, 1) == ':') {
					$args[$key] = $value;
				}







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

|
|
|
|
|

|
|
|
|
|

|
|
|

|
|
|
|
|
|
|
|
|
|
>




|







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
			'select' => '*',
			'order' => '1',
			'begin' => 0,
			'limit' => 100,
			'where' => '',
		];

		if (isset($params['sql'])) {
			$sql = $params['sql'];

			// Replace raw SQL parameters (undocumented feature, this is for #select section)
			foreach ($params as $k => $v) {
				if (substr($k, 0, 1) == '!') {
					$r = '/' . preg_quote($k, '/') . '\b/';
					$sql = preg_replace($r, $v, $sql);
				}
			}
		}
		else {
			if (empty($params['tables'])) {
				throw new Brindille_Exception(sprintf('"sql" section: missing parameter "tables" on line %d', $line));
			}

			foreach ($defaults as $key => $default_value) {
				if (!isset($params[$key])) {
					$params[$key] = $default_value;
				}
			}

			// 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 %s ORDER BY %s LIMIT %d,%d;',
				$params['select'],
				$params['tables'],
				$params['where'] ?? '',
				isset($params['group']) ? 'GROUP BY ' . $params['group'] : '',
				isset($params['having']) ? 'HAVING ' . $params['having'] : '',
				$params['order'],
				$params['begin'],
				$params['limit']
			);
		}

		$db = DB::getInstance();

		try {
			$statement = $db->protectSelect(self::SQL_TABLES, $sql);

			$args = [];

			foreach ($params as $key => $value) {
				if (substr($key, 0, 1) == ':') {
					$args[$key] = $value;
				}

Modified src/include/lib/Garradin/UserTemplate/UserTemplate.php from [293ac1234e] to [26f1142bcc].

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

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\Translate;

use Garradin\Config;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Users\Session;

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










|







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

namespace Garradin\UserTemplate;

use KD2\Brindille;
use KD2\Brindille_Exception;
use KD2\Translate;

use Garradin\Config;
use Garradin\Plugins;
use Garradin\Utils;
use Garradin\UserException;
use Garradin\Users\Session;

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

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
			'logged_user'  => $is_logged ? $session->getUser() : null,
			'dialog'       => isset($_GET['_dialog']) ? ($_GET['_dialog'] ?: true) : false,
		];

		return self::$root_variables;
	}

	public function __construct(string $path)
	{
		$this->_tpl_path = $path;

		if ($file = Files::get(File::CONTEXT_SKELETON . '/' . $path)) {
			if ($file->type != $file::TYPE_FILE) {
				throw new \LogicException('Cannot construct a UserTemplate with a directory');
			}

			$this->file = $file;
			$this->modified = $file->modified->getTimestamp();
		}
		else {
			$this->path = self::DIST_ROOT . $path;

			if (!($this->modified = @filemtime($this->path))) {
				throw new \InvalidArgumentException('File not found: ' . $this->path);
			}
		}

		$this->assignArray(self::getRootVariables());

		$this->registerAll();

		Plugin::fireSignal('usertemplate.init', ['template' => $this]);
	}

	/**
	 * Toggle safe mode
	 *
	 * If set to TRUE, then all functions and sections are removed, except foreach.
	 * Only modifiers can be used.







|



|







|











|







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
			'logged_user'  => $is_logged ? $session->getUser() : null,
			'dialog'       => isset($_GET['_dialog']) ? ($_GET['_dialog'] ?: true) : false,
		];

		return self::$root_variables;
	}

	public function __construct(?string $path)
	{
		$this->_tpl_path = $path;

		if ($path && $file = Files::get(File::CONTEXT_SKELETON . '/' . $path)) {
			if ($file->type != $file::TYPE_FILE) {
				throw new \LogicException('Cannot construct a UserTemplate with a directory');
			}

			$this->file = $file;
			$this->modified = $file->modified->getTimestamp();
		}
		elseif ($path) {
			$this->path = self::DIST_ROOT . $path;

			if (!($this->modified = @filemtime($this->path))) {
				throw new \InvalidArgumentException('File not found: ' . $this->path);
			}
		}

		$this->assignArray(self::getRootVariables());

		$this->registerAll();

		Plugins::fireSignal('usertemplate.init', ['template' => $this]);
	}

	/**
	 * Toggle safe mode
	 *
	 * If set to TRUE, then all functions and sections are removed, except foreach.
	 * Only modifiers can be used.
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
			$this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [Modifiers::class, $name] : $name);
		}

		// Local functions
		foreach (Functions::FUNCTIONS_LIST as $name) {
			$this->registerFunction($name, [Functions::class, $name]);
		}





		// Local sections
		foreach (Sections::SECTIONS_LIST as $name) {
			$this->registerSection($name, [Sections::class, $name]);
		}


		$this->registerCompileBlock(':break', function (string $name, string $params, Brindille $tpl, int $line) {
			$in_loop = false;
			foreach ($this->_stack as $element) {
				if ($element[0] == $this::SECTION) {
					$in_loop = true;
					break;
				}
			}

			if (!$in_loop) {
				throw new Brindille_Exception(sprintf('Error on line %d: break can only be used inside a section', $line));
			}

			return '<?php break; ?>';
		});
	}

	public function setSource(string $path)
	{
		$this->file = null;
		$this->path = $path;
		$this->modified = filemtime($path);







>
>
>
>






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







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
			$this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [Modifiers::class, $name] : $name);
		}

		// Local functions
		foreach (Functions::FUNCTIONS_LIST as $name) {
			$this->registerFunction($name, [Functions::class, $name]);
		}

		foreach (Functions::COMPILE_FUNCTIONS_LIST as $name => $callback) {
			$this->registerCompileBlock($name, $callback);
		}

		// Local sections
		foreach (Sections::SECTIONS_LIST as $name) {
			$this->registerSection($name, [Sections::class, $name]);
		}

		foreach (Sections::COMPILE_SECTIONS_LIST as $name => $callback) {
			$this->registerCompileBlock($name, $callback);





		}








	}

	public function setSource(string $path)
	{
		$this->file = null;
		$this->path = $path;
		$this->modified = filemtime($path);
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389

		if (!$is_web && $type != 'text/html' || !empty($this->_variables[0]['nocache'])) {
			$cache_as_uri = null;
		}

		if ($is_web && $type == 'text/html') {
			$scripts = [];
			Plugin::fireSignal('usertemplate.appendscript', ['template' => $this, 'content' => $content], $scripts);

			if (count($scripts)) {
				$scripts = array_map(fn($a) => sprintf('<script type="text/javascript" defer src="%s"></script>', $a), $scripts);
				$scripts = implode("\n", $scripts);
				$content = str_ireplace('</body', $scripts . '</body', $content);
			}
		}







|







367
368
369
370
371
372
373
374
375
376
377
378
379
380
381

		if (!$is_web && $type != 'text/html' || !empty($this->_variables[0]['nocache'])) {
			$cache_as_uri = null;
		}

		if ($is_web && $type == 'text/html') {
			$scripts = [];
			Plugins::fireSignal('usertemplate.appendscript', ['template' => $this, 'content' => $content], $scripts);

			if (count($scripts)) {
				$scripts = array_map(fn($a) => sprintf('<script type="text/javascript" defer src="%s"></script>', $a), $scripts);
				$scripts = implode("\n", $scripts);
				$content = str_ireplace('</body', $scripts . '</body', $content);
			}
		}

Modified src/include/lib/Garradin/Users/Session.php from [67546be240] to [18019a2edb].

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

namespace Garradin\Users;

use Garradin\Config;
use Garradin\DB;
use Garradin\Log;
use Garradin\Utils;
use Garradin\Plugin;
use Garradin\UserException;
use Garradin\ValidationException;

use Garradin\Users\Users;
use Garradin\Email\Templates as EmailsTemplates;
use Garradin\Files\Files;









|







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

namespace Garradin\Users;

use Garradin\Config;
use Garradin\DB;
use Garradin\Log;
use Garradin\Utils;
use Garradin\Plugins;
use Garradin\UserException;
use Garradin\ValidationException;

use Garradin\Users\Users;
use Garradin\Email\Templates as EmailsTemplates;
use Garradin\Files\Files;

39
40
41
42
43
44
45
46







47

48
49
50
51
52
53
54
	const SECTION_CONFIG = 'config';
	const SECTION_SUBSCRIBE = 'subscribe';

	const ACCESS_NONE = 0;
	const ACCESS_READ = 1;
	const ACCESS_WRITE = 2;
	const ACCESS_ADMIN = 9;








	// Personalisation de la config de UserSession

	protected $cookie_name = 'pko';
	protected $remember_me_cookie_name = 'pkop';
	protected $remember_me_expiry = '+3 months';

	protected ?User $_user;
	protected ?array $_permissions;
	protected ?array $_files_permissions;








>
>
>
>
>
>
>

>







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	const SECTION_CONFIG = 'config';
	const SECTION_SUBSCRIBE = 'subscribe';

	const ACCESS_NONE = 0;
	const ACCESS_READ = 1;
	const ACCESS_WRITE = 2;
	const ACCESS_ADMIN = 9;

	const ACCESS_WORDS = [
		'none' => self::ACCESS_NONE,
		'read' => self::ACCESS_READ,
		'write' => self::ACCESS_WRITE,
		'admin' => self::ACCESS_ADMIN,
	];

	// Personalisation de la config de UserSession
	protected bool $non_locking = true;
	protected $cookie_name = 'pko';
	protected $remember_me_cookie_name = 'pkop';
	protected $remember_me_expiry = '+3 months';

	protected ?User $_user;
	protected ?array $_permissions;
	protected ?array $_files_permissions;
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
			$this->http = new \KD2\HTTP;
		}

		// Vérifier s'il n'y a pas un plugin qui gère déjà cet aspect
		// notamment en installation mutualisée c'est plus efficace
		$return = ['is_compromised' => null];

		if (Plugin::fireSignal('password.check', ['password' => $password], $return) && isset($return['is_compromised'])) {
			return (bool) $return['is_compromised'];
		}

		return parent::isPasswordCompromised($password);
	}

	protected function getUserForLogin($login)







|







95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
			$this->http = new \KD2\HTTP;
		}

		// Vérifier s'il n'y a pas un plugin qui gère déjà cet aspect
		// notamment en installation mutualisée c'est plus efficace
		$return = ['is_compromised' => null];

		if (Plugins::fireSignal('password.check', ['password' => $password], $return) && isset($return['is_compromised'])) {
			return (bool) $return['is_compromised'];
		}

		return parent::isPasswordCompromised($password);
	}

	protected function getUserForLogin($login)
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
		elseif ($user = $this->getUserForLogin($login)) {
			Log::add(Log::LOGIN_FAIL, compact('user_agent'), $user->id);
		}
		else {
			Log::add(Log::LOGIN_FAIL, compact('user_agent'));
		}

		Plugin::fireSignal('user.login', compact('login', 'password', 'remember_me', 'success'));

		// Clean up logs
		Log::clean();

		return $success;
	}








|







232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
		elseif ($user = $this->getUserForLogin($login)) {
			Log::add(Log::LOGIN_FAIL, compact('user_agent'), $user->id);
		}
		else {
			Log::add(Log::LOGIN_FAIL, compact('user_agent'));
		}

		Plugins::fireSignal('user.login', compact('login', 'password', 'remember_me', 'success'));

		// Clean up logs
		Log::clean();

		return $success;
	}

251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
			// Mettre à jour la date de connexion
			$this->db->preparedQuery('UPDATE users SET date_login = datetime() WHERE id = ?;', [$user_id]);
		}
		else {
			Log::add(Log::LOGIN_FAIL, $details, $user_id);
		}

		Plugin::fireSignal('user.login.otp', compact('success', 'user_id'));

		return $success;
	}

	public function logout(bool $all = false)
	{
		$this->_user = null;







|







259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
			// Mettre à jour la date de connexion
			$this->db->preparedQuery('UPDATE users SET date_login = datetime() WHERE id = ?;', [$user_id]);
		}
		else {
			Log::add(Log::LOGIN_FAIL, $details, $user_id);
		}

		Plugins::fireSignal('user.login.otp', compact('success', 'user_id'));

		return $success;
	}

	public function logout(bool $all = false)
	{
		$this->_user = null;
405
406
407
408
409
410
411














412
413
414
415
416
417
418

		if (!$s->isLogged()) {
			return null;
		}

		return $s->getUser();
	}















	public function getUser()
	{
		if (isset($this->_user)) {
			return $this->_user;
		}








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







413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440

		if (!$s->isLogged()) {
			return null;
		}

		return $s->getUser();
	}

	/**
	 * Returns cookie string for PDF printing
	 */
	static public function getCookie(): ?string
	{
		$i = self::getInstance();

		if (!$i->isLogged()) {
			return null;
		}

		return sprintf('%s=%s', $i->cookie_name, $i->id());
	}

	public function getUser()
	{
		if (isset($this->_user)) {
			return $this->_user;
		}

Modified src/include/lib/Garradin/Utils.php from [0a5c6bfd7e] to [61432880f7].

1
2
3
4
5
6
7
8
9
10


11
12
13
14
15
16
17
<?php

namespace Garradin;

use KD2\Security;
use KD2\Form;
use KD2\HTTP;
use KD2\Translate;
use KD2\SMTP;



class Utils
{
    static protected $collator;
    static protected $transliterator;

    const ICONS = [
        'up'              => '↑',










>
>







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

namespace Garradin;

use KD2\Security;
use KD2\Form;
use KD2\HTTP;
use KD2\Translate;
use KD2\SMTP;

use Garradin\Users\Session;

class Utils
{
    static protected $collator;
    static protected $transliterator;

    const ICONS = [
        'up'              => '↑',
172
173
174
175
176
177
178





179
180
181
182
183
184
185
        }

        $date = $ts->format($format);

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






    /**
     * @deprecated
     */
    static public function checkDate($str)
    {
        if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match))







>
>
>
>
>







174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
        }

        $date = $ts->format($format);

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

    static public function shortDate($ts, bool $with_hour = false): ?string
    {
        return self::date_fr($ts, 'd/m/Y' . ($with_hour ? ' à H\hi' : ''));
    }

    /**
     * @deprecated
     */
    static public function checkDate($str)
    {
        if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match))
957
958
959
960
961
962
963





964




965






















966
967
968
969
970
971
972
        }

        // Check if string is already UTF-8 encoded or not
        if (preg_match('//u', $str)) {
            return $str;
        }






        // FIXME for PHP 9.0+ see https://php.watch/versions/8.2/utf8_encode-utf8_decode-deprecated




        return @utf8_encode($str);






















    }

    /**
     * Transforms a unicode string to lowercase AND removes all diacritics
     *
     * @see https://www.matthecat.com/supprimer-les-accents-d-une-chaine-avec-php.html
     */







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







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
        }

        // Check if string is already UTF-8 encoded or not
        if (preg_match('//u', $str)) {
            return $str;
        }

        return !preg_match('//u', $str) ? self::iso8859_1_to_utf8($str) : $str;
    }

    /**
     * Poly-fill to encode a ISO-8859-1 string to UTF-8 for PHP >= 9.0
     * @see https://php.watch/versions/8.2/utf8_encode-utf8_decode-deprecated
     */
    static public function iso8859_1_to_utf8(string $s): string
    {
        if (PHP_VERSION_ID < 90000) {
            return @utf8_encode($s);
        }

        $s .= $s;
        $len = strlen($s);

        for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
            switch (true) {
                case $s[$i] < "\x80":
                    $s[$j] = $s[$i];
                    break;
                case $s[$i] < "\xC0":
                    $s[$j] = "\xC2";
                    $s[++$j] = $s[$i];
                    break;
                default:
                    $s[$j] = "\xC3";
                    $s[++$j] = chr(ord($s[$i]) - 64);
                    break;
            }
        }

        return substr($s, 0, $j);
    }

    /**
     * Transforms a unicode string to lowercase AND removes all diacritics
     *
     * @see https://www.matthecat.com/supprimer-les-accents-d-une-chaine-avec-php.html
     */
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
    }

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

        if (defined('Garradin\PDF_LOG') && \Garradin\PDF_LOG) {
            file_put_contents(\Garradin\PDF_LOG, date("[d/m/Y H:i:s]\n"), FILE_APPEND);
        }
    }

    /**
     * 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)) {
                Utils::safe_unlink($source);
                return $target;
            }

            unset($in);

            // Try to find a local executable








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

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







>
>
>
>



|














|
<
<
<
<

>
|
<

<
<
<
|
<
<
<
|
<
<

<
<
|
<
<
<
|
<
<










<
<
<
<
<



>
>
>
>
>
>
>
>
>
>



|







1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184




1185
1186
1187

1188



1189



1190


1191


1192



1193


1194
1195
1196
1197
1198
1199
1200
1201
1202
1203





1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
    }

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

    static public function appendCookieToURLs(string $str): string
    {
        $cookie = Session::getCookie();

        if (!$cookie) {
            return $str;
        }

        // Append session cookie to URLs, so that <img> tags and others work
        $r = preg_quote(WWW_URL, '!');
        $r = '!(?<=["\'])(' . $r . '.*?)(?=["\'])!';
        $str = preg_replace_callback($r, function ($match) use ($cookie): string {
            if (false !== strpos($match[1], '?')) {
                $separator = '&amp;';
            }
            else {
                $separator = '?';
            }

            return $match[1] . $separator . $cookie;
        }, $str);

        return $str;
    }

    /**
     * Execute a system command with a timeout
     * @see https://blog.dubbelboer.com/2012/08/24/execute-with-timeout.html
     */
    static public function exec(string $cmd, int $timeout, ?callable $stdin, ?callable $stdout, ?callable $stderr = null): int
    {
        if (!function_exists('proc_open') || !function_exists('proc_terminate')
            || preg_match('/proc_(?:open|terminate|get_status|close)/', ini_get('disable_functions'))) {
            throw new \RuntimeException('Execution of system commands is disabled.');
        }

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

        $process = proc_open($cmd, $descriptorspec, $pipes);

        if (!is_resource($process)) {
            throw new \RuntimeException('Cannot execute command: ' . $cmd);
        }

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

        // Set to non-blocking
        stream_set_blocking($pipes[0], false);
        stream_set_blocking($pipes[1], false);
        stream_set_blocking($pipes[2], false);

        $timeout_ms = $timeout * 1000000; // in microseconds

        if (null !== $stdin) {
            // Send STDIN
            fwrite($pipes[0], $stdin());
        }

        fclose($pipes[0]);

        while ($timeout_ms > 0) {
            $start = microtime(true);

            // Wait until we have output or the timer expired.
            $read  = [$pipes[1]];
            $other = [];

            if (null !== $stderr) {
                $read[] = $pipes[2];
            }

            // Wait every 0.5 seconds
            stream_select($read, $other, $other, 0, 500000);

            // Get the status of the process.
            // Do this before we read from the stream,
            // this way we can't lose the last bit of output if the process dies between these     functions.
            $status = proc_get_status($process);

            // Read the contents from the buffer.
            // This function will always return immediately as the stream is none-blocking.
            $stdout(stream_get_contents($pipes[1]));

            if (null !== $stderr) {
                $stderr(stream_get_contents($pipes[2]));
            }

            if (!$status['running']) {
                // Break from this loop if the process exited before the timeout.
                break;
            }

            // Subtract the number of microseconds that we waited.
            $timeout_ms -= (microtime(true) - $start) * 1000000;
        }

        fclose($pipes[1]);
        fclose($pipes[2]);

        $status = proc_get_status($process);

        if ($status['running']) {
            proc_terminate($process, 9);
            throw new \RuntimeException(sprintf("Command killed after taking more than %d seconds: \n%s", $timeout, $cmd));
        }

        $status = proc_get_status($process);
        proc_close($process);

        return $status['exitcode'];
    }

    /**
     * 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) {
            return;
        }

        if (PDF_COMMAND == 'auto') {
            // Try to see if there's a plugin
            $in = ['string' => $str];

            if (Plugins::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;
        }

        $str = self::appendCookieToURLs($str);





        // 3 seconds is plenty enough to fetch resources, right?
        $cmd = 'prince --http-timeout=3 -o - -';





        // Prince is fast, right? Fingers crossed



        self::exec($cmd, 10, fn () => $str, fn ($data) => print($data));





        if (PDF_USAGE_LOG) {



            file_put_contents(PDF_USAGE_LOG, date("Y-m-d H:i:s\n"), FILE_APPEND);


        }
    }

    /**
     * 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
    {





        $cmd = PDF_COMMAND;

        if (!$cmd) {
            return null;
        }

        $source = sprintf('%s/print-%s.html', CACHE_ROOT, md5(random_bytes(16)));
        $target = str_replace('.html', '.pdf', $source);

        $str = self::appendCookieToURLs($str);
        file_put_contents($source, $str);

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

            if (Plugins::fireSignal('pdf.create', $in)) {
                Utils::safe_unlink($source);
                return $target;
            }

            unset($in);

            // Try to find a local executable
1089
1090
1091
1092
1093
1094
1095


1096
1097
1098

1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113

1114
1115
1116
1117





1118

1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
            }

            // We still haven't found anything
            if (!$cmd) {
                throw new \LogicException('Aucun programme de création de PDF trouvé, merci d\'en installer un : https://fossil.kd2.org/garradin/wiki?name=Configuration');
            }
        }



        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 -q --print-media-type --enable-local-file-access --disable-smart-shrinking --encoding "UTF-8" %s %s';
                break;
            case 'weasyprint':
                $cmd = 'weasyprint %1$s %2$s';
                break;
            default:
                break;
        }


        $cmd .= ' 2>&1';

        $cmd = sprintf($cmd, escapeshellarg($source), escapeshellarg($target));
        $output = shell_exec($cmd);





        Utils::safe_unlink($source);


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

        if (defined('Garradin\PDF_LOG') && \Garradin\PDF_LOG) {
            file_put_contents(\Garradin\PDF_LOG, date("[d/m/Y H:i:s]\n"), FILE_APPEND);
        }

        return $target;
    }

    /**
     * Integer to A-Z, AA-ZZ, AAA-ZZZ, etc.







>
>



>
|


|











>


<
|
>
>
>
>
>
|
>





|
|







1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265

1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
            }

            // We still haven't found anything
            if (!$cmd) {
                throw new \LogicException('Aucun programme de création de PDF trouvé, merci d\'en installer un : https://fossil.kd2.org/garradin/wiki?name=Configuration');
            }
        }

        $timeout = 25;

        switch ($cmd) {
            case 'prince':
                $timeout = 10;
                $cmd = 'prince --http-timeout=3 -o %2$s %1$s';
                break;
            case 'chromium':
                $cmd = 'chromium --headless --timeout=5000 --disable-gpu --run-all-compositor-stages-before-draw --print-to-pdf-no-header --print-to-pdf=%2$s %1$s';
                break;
            case 'wkhtmltopdf':
                $cmd = 'wkhtmltopdf -q --print-media-type --enable-local-file-access --disable-smart-shrinking --encoding "UTF-8" %s %s';
                break;
            case 'weasyprint':
                $cmd = 'weasyprint %1$s %2$s';
                break;
            default:
                break;
        }

        $cmd = sprintf($cmd, escapeshellarg($source), escapeshellarg($target));
        $cmd .= ' 2>&1';


        $output = '';

        try {
            self::exec($cmd, $timeout, null, fn ($data) => $output .= $data);
        }
        finally {
            Utils::safe_unlink($source);
        }

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

        if (PDF_USAGE_LOG) {
            file_put_contents(PDF_USAGE_LOG, date("Y-m-d H:i:s\n"), FILE_APPEND);
        }

        return $target;
    }

    /**
     * Integer to A-Z, AA-ZZ, AAA-ZZZ, etc.

Modified src/include/lib/Garradin/Web/Render/Markdown.php from [477f25b224] to [a1d7461fd1].

23
24
25
26
27
28
29
30
31
32
33
34
35
		$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">', htmlspecialchars($this->resolveLink($matches[1])));
		}, $str);

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







|





23
24
25
26
27
28
29
30
31
32
33
34
35
		$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">', htmlspecialchars($this->resolveLink(htmlspecialchars_decode($matches[1]))));
		}, $str);

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

Modified src/include/lib/Garradin/Web/Render/Parsedown.php from [4ec75d240a] to [29e329fac8].

20
21
22
23
24
25
26



27
28
29
30
31
32
33
34
35


















36
37
38
39
40
41
42
	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)) {
			$text = $this->skriv->callExtension($match);







>
>
>









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







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
	protected $skriv;
	protected $toc = [];

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

		// Make Skriv extensions also available inline, before anything else
		array_unshift($this->InlineTypes['<'], 'SkrivExtension');

		# 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 inlineSkrivExtension(array $str): ?array
	{
		if (preg_match('/<<<?([a-z_]+)((?:(?!>>>?).)*?)>>>?/i', $str['text'], $match)) {
			$text = $this->skriv->callExtension($match);

			return [
				'extent'    => strlen($match[0]),
				'element' => [
					'name'                   => 'div',
					'rawHtml'                => $text,
					'allowRawHtmlInSafeMode' => true,
				],
			];
		}

		return null;
	}

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

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

Modified src/include/lib/Garradin/Web/Render/Skriv.php from [962e942dae] to [c9c1334af0].

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

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;

use Garradin\Plugin;
use Garradin\UserTemplate\CommonModifiers;

use KD2\SkrivLite;
use KD2\Garbage2xhtml;

use const Garradin\{ADMIN_URL, WWW_URL};







|







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

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;

use Garradin\Plugins;
use Garradin\UserTemplate\CommonModifiers;

use KD2\SkrivLite;
use KD2\Garbage2xhtml;

use const Garradin\{ADMIN_URL, WWW_URL};

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
		$this->skriv = new SkrivLite;
		$this->skriv->registerExtension('file', [$this, 'SkrivFile']);
		$this->skriv->registerExtension('fichier', [$this, 'SkrivFile']);
		$this->skriv->registerExtension('image', [$this, 'SkrivImage']);
		$this->skriv->registerExtension('html', [$this, 'SkrivHTML']);

		// Enregistrer d'autres extensions éventuellement
		Plugin::fireSignal('skriv.init', ['skriv' => $this->skriv]);
	}

	public function render(?string $content = null): string
	{
		$skriv =& $this->skriv;

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

		$str = preg_replace_callback('/#file:\[([^\]\h]+)\]/', function ($match) {
			return $this->resolveAttachment($match[1]);
		}, $str);

		$str = $skriv->render($str);

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

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

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

	public function callExtension(array $match)
	{







|

















|







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
		$this->skriv = new SkrivLite;
		$this->skriv->registerExtension('file', [$this, 'SkrivFile']);
		$this->skriv->registerExtension('fichier', [$this, 'SkrivFile']);
		$this->skriv->registerExtension('image', [$this, 'SkrivImage']);
		$this->skriv->registerExtension('html', [$this, 'SkrivHTML']);

		// Enregistrer d'autres extensions éventuellement
		Plugins::fireSignal('skriv.init', ['skriv' => $this->skriv]);
	}

	public function render(?string $content = null): string
	{
		$skriv =& $this->skriv;

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

		$str = preg_replace_callback('/#file:\[([^\]\h]+)\]/', function ($match) {
			return $this->resolveAttachment($match[1]);
		}, $str);

		$str = $skriv->render($str);

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

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

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

	public function callExtension(array $match)
	{

Modified src/include/lib/Garradin/Web/Router.php from [3220c288b9] to [fdc58e1a88].

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

namespace Garradin\Web;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Files\WebDAV\Server as WebDAV_Server;

use Garradin\Web\Skeleton;
use Garradin\Web\Web;

use Garradin\API;
use Garradin\Config;
use Garradin\Plugin;
use Garradin\UserException;
use Garradin\Utils;

use Garradin\Users\Session;

use const Garradin\{WWW_URI, ADMIN_URL, ROOT, HTTP_LOG_FILE, ENABLE_XSENDFILE};














|







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

namespace Garradin\Web;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Files\WebDAV\Server as WebDAV_Server;

use Garradin\Web\Skeleton;
use Garradin\Web\Web;

use Garradin\API;
use Garradin\Config;
use Garradin\Plugins;
use Garradin\UserException;
use Garradin\Utils;

use Garradin\Users\Session;

use const Garradin\{WWW_URI, ADMIN_URL, ROOT, HTTP_LOG_FILE, ENABLE_XSENDFILE};

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
		if ($uri == 'feed/atom/') {
			Utils::redirect('/atom.xml');
		}
		elseif ($uri == 'favicon.ico') {
			header('Location: ' . Config::getInstance()->fileURL('favicon'), true);
			return;
		}



		elseif (preg_match('!^(admin/p|p)/(' . Plugin::PLUGIN_ID_REGEXP . ')/(.*)$!', $uri, $match)) {
			$plugin = new Plugin($match[2]);
			$public = $match[1] == 'p';
			$plugin->route($public, $match[3]);
			return;
		}

		elseif ('admin' == $first || 'p' == $first) {
			http_response_code(404);
			throw new UserException('Cette page n\'existe pas.');
		}
		elseif ('api' == $first) {
			API::dispatchURI(substr($uri, 4));
			return;
		}
		elseif ((in_array($uri, self::DAV_ROUTES) || in_array($first, self::DAV_ROUTES))
			&& WebDAV_Server::route($uri)) {
			return;
		}







>
>
>
|
|
|
|


>
|



|







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
		if ($uri == 'feed/atom/') {
			Utils::redirect('/atom.xml');
		}
		elseif ($uri == 'favicon.ico') {
			header('Location: ' . Config::getInstance()->fileURL('favicon'), true);
			return;
		}
		elseif (preg_match('!^(?:admin/p|p|m)/\w+$!', $uri)) {
			Utils::redirect('/' . $uri . '/');
		}
		elseif (preg_match('!^(admin/p|p)/(' . Plugins::NAME_REGEXP . ')/(.*)$!', $uri, $match)
			&& ($plugin = Plugins::get($match[2])) && $plugin->enabled) {
			$uri = ($match[1] == 'admin/p' ? 'admin/' : '') . $match[3];
			$plugin->route($uri);
			return;
		}
		// Other admin/plugin routes are not found
		elseif ($first === 'admin' || $first === 'p') {
			http_response_code(404);
			throw new UserException('Cette page n\'existe pas.');
		}
		elseif ('api' === $first) {
			API::dispatchURI(substr($uri, 4));
			return;
		}
		elseif ((in_array($uri, self::DAV_ROUTES) || in_array($first, self::DAV_ROUTES))
			&& WebDAV_Server::route($uri)) {
			return;
		}
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
						break;
					}
				}
			}

			$session = Session::getInstance();

			if (Plugin::fireSignal('http.request.file.before', compact('file', 'uri', 'session'))) {
				// If a plugin handled the request, let's stop here
				return;
			}

			if ($size) {
				$file->serveThumbnail($session, $size);
			}
			else {
				$file->serve($session, isset($_GET['download']), $_GET['s'] ?? null, $_POST['p'] ?? null);
			}

			Plugin::fireSignal('http.request.file.after', compact('file', 'uri', 'session'));

			return;
		}

		Skeleton::route($uri);
	}








|











|







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

			$session = Session::getInstance();

			if (Plugins::fireSignal('http.request.file.before', compact('file', 'uri', 'session'))) {
				// If a plugin handled the request, let's stop here
				return;
			}

			if ($size) {
				$file->serveThumbnail($session, $size);
			}
			else {
				$file->serve($session, isset($_GET['download']), $_GET['s'] ?? null, $_POST['p'] ?? null);
			}

			Plugins::fireSignal('http.request.file.after', compact('file', 'uri', 'session'));

			return;
		}

		Skeleton::route($uri);
	}

Modified src/include/lib/Garradin/Web/Skeleton.php from [b64003a79f] to [bf267b1aff].

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

namespace Garradin\Web;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;
use Garradin\UserException;
use Garradin\UserTemplate\Modules;
use Garradin\UserTemplate\UserTemplate;
use Garradin\Config;
use Garradin\Plugin;
use Garradin\Utils;

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

use const Garradin\{ROOT, ADMIN_URL};












|







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

namespace Garradin\Web;

use Garradin\Files\Files;
use Garradin\Entities\Files\File;
use Garradin\Entities\Web\Page;
use Garradin\UserException;
use Garradin\UserTemplate\Modules;
use Garradin\UserTemplate\UserTemplate;
use Garradin\Config;
use Garradin\Plugins;
use Garradin\Utils;

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

use const Garradin\{ROOT, ADMIN_URL};

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
		}

		return null;
	}

	public function serve(string $uri, array $params = []): void
	{
		if (Plugin::fireSignal('http.request.skeleton.before', $params)) {
			return;
		}

		$type = $this->type();

		if (!$type) {
			throw new \InvalidArgumentException('Invalid skeleton type');







|







98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
		}

		return null;
	}

	public function serve(string $uri, array $params = []): void
	{
		if (Plugins::fireSignal('http.request.skeleton.before', $params)) {
			return;
		}

		$type = $this->type();

		if (!$type) {
			throw new \InvalidArgumentException('Invalid skeleton type');
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
		else {
			Cache::link($uri, $this->defaultPath());
			header(sprintf('Content-Type: %s;charset=utf-8', $type), true);
			readfile($this->defaultPath());
			flush();
		}

		Plugin::fireSignal('http.request.skeleton.after', $params);
	}

	public function file(): ?File
	{
		return Files::get(File::CONTEXT_SKELETON . '/' . $this->path);
	}








|







141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
		else {
			Cache::link($uri, $this->defaultPath());
			header(sprintf('Content-Type: %s;charset=utf-8', $type), true);
			readfile($this->defaultPath());
			flush();
		}

		Plugins::fireSignal('http.request.skeleton.after', $params);
	}

	public function file(): ?File
	{
		return Files::get(File::CONTEXT_SKELETON . '/' . $this->path);
	}

Modified src/include/migrations/1.3/1.3.0.php from [0549342cfa] to [3c83dc2666].

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
















15
16
17
18
19
20
21
<?php

namespace Garradin;

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

$db->beginSchemaUpdate();

// Get old keys
$config = (object) $db->getAssoc('SELECT key, value FROM config WHERE key IN (\'champs_membres\', \'champ_identifiant\', \'champ_identite\');');

// Create config_users_fields table, and lots of stuff
$db->import(ROOT . '/include/migrations/1.3/schema.sql');

















// Migrate users table
$df = \Garradin\Users\DynamicFields::fromOldINI($config->champs_membres, $config->champ_identifiant, $config->champ_identite, 'numero');
$df->save(false);

// Migrate other stuff
$db->import(ROOT . '/include/migrations/1.3/1.3.0.sql');












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







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace Garradin;

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

$db->beginSchemaUpdate();

// Get old keys
$config = (object) $db->getAssoc('SELECT key, value FROM config WHERE key IN (\'champs_membres\', \'champ_identifiant\', \'champ_identite\');');

// Create config_users_fields table
$db->exec('
CREATE TABLE IF NOT EXISTS config_users_fields (
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    sort_order INTEGER NOT NULL,
    type TEXT NOT NULL,
    label TEXT NOT NULL,
    help TEXT NULL,
    required INTEGER NOT NULL DEFAULT 0,
    read_access INTEGER NOT NULL DEFAULT 0,
    write_access INTEGER NOT NULL DEFAULT 1,
    list_table INTEGER NOT NULL DEFAULT 0,
    options TEXT NULL,
    default_value TEXT NULL,
    sql TEXT NULL,
    system TEXT NULL
);');

// Migrate users table
$df = \Garradin\Users\DynamicFields::fromOldINI($config->champs_membres, $config->champ_identifiant, $config->champ_identite, 'numero');
$df->save(false);

// Migrate other stuff
$db->import(ROOT . '/include/migrations/1.3/1.3.0.sql');

Modified src/include/migrations/1.3/1.3.0.sql from [31fbb67a96] to [d138e6e6bc].

1
2
3
4
5
6
7
8
9
10
-- Already created before, so we need to drop it to migrate
DROP TABLE plugins_signals;

-- The new users table has already been created and copied
ALTER TABLE plugins RENAME TO plugins_old;
ALTER TABLE plugins_signaux RENAME TO plugins_signaux_old;

-- References old membres table
ALTER TABLE services_users RENAME TO services_users_old; -- Also take id_fee into account for unique key
ALTER TABLE services_reminders_sent RENAME TO services_reminders_sent_old;
<
<
<










1
2
3
4
5
6
7



-- The new users table has already been created and copied
ALTER TABLE plugins RENAME TO plugins_old;
ALTER TABLE plugins_signaux RENAME TO plugins_signaux_old;

-- References old membres table
ALTER TABLE services_users RENAME TO services_users_old; -- Also take id_fee into account for unique key
ALTER TABLE services_reminders_sent RENAME TO services_reminders_sent_old;
47
48
49
50
51
52
53






54



55
56
57
58
59
60
61
INSERT INTO acc_transactions_users SELECT * FROM acc_transactions_users_old;

DROP TABLE services_reminders_sent_old;
DROP TABLE acc_transactions_users_old;
DROP TABLE acc_transactions_old;
DROP TABLE services_users_old;







INSERT INTO plugins SELECT id, officiel, nom, description, auteur, url, version, config FROM plugins_old;



INSERT INTO plugins_signals SELECT * FROM plugins_signaux_old;

DROP TABLE plugins_signaux_old;
DROP TABLE plugins_old;

INSERT INTO searches SELECT * FROM recherches;
UPDATE searches SET target = 'accounting' WHERE target = 'compta';







>
>
>
>
>
>
|
>
>
>







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
INSERT INTO acc_transactions_users SELECT * FROM acc_transactions_users_old;

DROP TABLE services_reminders_sent_old;
DROP TABLE acc_transactions_users_old;
DROP TABLE acc_transactions_old;
DROP TABLE services_users_old;

-- Remove old plugin as it cannot be uninstalled as it no longer exists
DELETE FROM plugins_old WHERE id = 'ouvertures';
DELETE FROM plugins_signaux_old WHERE plugin = 'ouvertures';

-- Rename plugins table columns to English
INSERT INTO plugins (name, label, description, author, author_url, version, config, enabled, menu, restrict_level, restrict_section)
	SELECT id, nom, description, auteur, url, version, config, 1, menu,
	CASE WHEN menu_condition IS NOT NULL THEN 2 ELSE NULL END,
	CASE WHEN menu_condition IS NOT NULL THEN 'users' ELSE NULL END
	FROM plugins_old;
INSERT INTO plugins_signals SELECT * FROM plugins_signaux_old;

DROP TABLE plugins_signaux_old;
DROP TABLE plugins_old;

INSERT INTO searches SELECT * FROM recherches;
UPDATE searches SET target = 'accounting' WHERE target = 'compta';

Modified src/include/migrations/1.3/schema.sql from [4232e4cc2b] to [a912bda722].

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
    system TEXT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS config_users_fields_name ON config_users_fields (name);

CREATE TABLE IF NOT EXISTS plugins
(
    id TEXT NOT NULL PRIMARY KEY,
    official INTEGER NOT NULL DEFAULT 0, -- 1 if plugin is official
    name TEXT NOT NULL,

    description TEXT NULL,
    author TEXT NULL,
    url TEXT NULL,
    version TEXT NOT NULL,




    config TEXT NULL

);



CREATE TABLE IF NOT EXISTS plugins_signals
-- Link between plugins and signals
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (id),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);






























CREATE TABLE IF NOT EXISTS api_credentials
(
    id INTEGER NOT NULL PRIMARY KEY,
    label TEXT NOT NULL,
    key TEXT NOT NULL,
    secret TEXT NOT NULL,
    created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,







|
<

>


|

>
>
>
>
|
>

>
>





|




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







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
    system TEXT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS config_users_fields_name ON config_users_fields (name);

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

    name TEXT NOT NULL,
    label TEXT NOT NULL,
    description TEXT NULL,
    author TEXT NULL,
    author_url TEXT NULL,
    version TEXT NOT NULL,
    menu INT NOT NULL DEFAULT 0,
    home_button INT NOT NULL DEFAULT 0,
    restrict_section TEXT NULL,
    restrict_level INT NULL,
    config TEXT NULL,
    enabled INTEGER NOT NULL DEFAULT 0
);

CREATE UNIQUE INDEX IF NOT EXISTS plugins_name ON plugins (name);

CREATE TABLE IF NOT EXISTS plugins_signals
-- Link between plugins and signals
(
    signal TEXT NOT NULL,
    plugin TEXT NOT NULL REFERENCES plugins (name),
    callback TEXT NOT NULL,
    PRIMARY KEY (signal, plugin)
);

CREATE TABLE IF NOT EXISTS modules
-- List of modules
(
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    label TEXT NOT NULL,
    description TEXT NULL,
    author TEXT NULL,
    author_url TEXT NULL,
    menu INT NOT NULL DEFAULT 0,
    home_button INT NOT NULL DEFAULT 0,
    restrict_section TEXT NULL,
    restrict_level INT NULL,
    config TEXT NULL,
    enabled INTEGER NOT NULL DEFAULT 0
);

CREATE UNIQUE INDEX IF NOT EXISTS modules_name ON modules (name);

CREATE TABLE IF NOT EXISTS modules_templates
-- List of forms special templates
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_module INTEGER NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
    name TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS modules_templates_name ON modules_templates (id_module, name);

CREATE TABLE IF NOT EXISTS api_credentials
(
    id INTEGER NOT NULL PRIMARY KEY,
    label TEXT NOT NULL,
    key TEXT NOT NULL,
    secret TEXT NOT NULL,
    created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516

CREATE UNIQUE INDEX IF NOT EXISTS web_pages_path ON web_pages (path);
CREATE UNIQUE INDEX IF NOT EXISTS web_pages_uri ON web_pages (uri);
CREATE UNIQUE INDEX IF NOT EXISTS web_pages_file_path ON web_pages (file_path);
CREATE INDEX IF NOT EXISTS web_pages_parent ON web_pages (parent);
CREATE INDEX IF NOT EXISTS web_pages_published ON web_pages (published);
CREATE INDEX IF NOT EXISTS web_pages_title ON web_pages (title);

CREATE TABLE IF NOT EXISTS modules
-- List of modules
(
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    label TEXT NOT NULL,
    description TEXT NULL,
    config TEXT NULL,
    enabled INTEGER NOT NULL DEFAULT 0
);

CREATE UNIQUE INDEX IF NOT EXISTS modules_name ON modules (name);

CREATE TABLE IF NOT EXISTS modules_templates
-- List of forms special templates
(
    id INTEGER NOT NULL PRIMARY KEY,
    id_module INTEGER NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
    name TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS modules_templates_name ON modules_templates (id_module, name);







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
523
524
525
526
527
528
529
























CREATE UNIQUE INDEX IF NOT EXISTS web_pages_path ON web_pages (path);
CREATE UNIQUE INDEX IF NOT EXISTS web_pages_uri ON web_pages (uri);
CREATE UNIQUE INDEX IF NOT EXISTS web_pages_file_path ON web_pages (file_path);
CREATE INDEX IF NOT EXISTS web_pages_parent ON web_pages (parent);
CREATE INDEX IF NOT EXISTS web_pages_published ON web_pages (published);
CREATE INDEX IF NOT EXISTS web_pages_title ON web_pages (title);























Modified src/pubkey.asc from [7eb815c6b8] to [e8974cff9c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBEzkiQIBEACyW7c1IJXTxln18VSEXZJITqU4GHhS0lqOVxKTqQGmd6BIscu/
8NyDPdYpNhz3eQ090cCAk2dxxgj5rmLM21Wj8cwYCXLNOsZRGeSufyqgGYWdG5DY
7rJyUFfjwy+xPqoeqcy/W9aIcH1X1k+DfAsgsqhwM97lk6tEg4Zn0eKmWGjcKzla
pJ66Wdv+oRD6T01X7XJ/QQghxQmqupt1u1xbWEOXAaO6tSVEbpUDaODq0YsV3J+9
owX8gJb2B7UkegXcIaEsuzXsZmlWIL1COyElcTtxh4n+Wtu1dImn0Hv2oprzJ9ub
B3/zEmhNl2+dTjurfrQYt/f4LXuDB32AZlpB2SR8H2dvTwsd+RUuUMsv/nj4lAyl
Vovny1ihilIxZcxYSMadXYvOR7GLmDi2LSsasVJ8fpOQDhYZam7AyOs4lZ0gT4AC
rqVdQGvlEtWrkQGTGmyuMsyi4IUC4aMFJkxmS9f8Dy1Ogu0KN9nAak6PJEs483oQ
o8ip59dTAo55b7OVPUEeX6g68cIiHptMa8pV9RY16sP1RStqKj3XU3+pErEwqUk3
5HpXNOuO7ALFW29GEq/mjQ12FuTRawtPv6BBPtMRNFakx0V2iX78D5cDDUfze0k9
iVkcM+ZtXnmA/wbQzyVev/isWrrLG2+/iatxd4bQPSOddh1GDPn+sQIxGwARAQAB
tBpCb2h3YVogPGJvaHdhekBib2h3YXoubmV0PokCWAQTAQoAQgIbAwYLCQgHAwIG
FQgCCQoLBBYCAwECHgECF4ACGQEWIQTEmSR5ui6KXULBggGSydpxuIjqNAUCYAgA
HQUJFuXeGwAKCRCSydpxuIjqNGQTD/wJLpxo+vpOFrKEayvM8LPx6hMBQpbDJPed
Imm8vSE/rviOFy7TLQluJSCAm7LmZvBpt/BjI7x/0XQQ6TLbcFgPxIGAcLKTfjNR
CsYqEUYkQwCcNaeNWaIvtrU3j9N98kHqVWWK5Puvhb7Kn7WGxOpvZsww39u/fLV4
qlsIGbij2osTEon6C9Wg8vOo66y1YEBE/YCrOAYfwbUL/gmIn+dre1LYRRPExmOb
/iMpkiCAEb5FEaF7dM/c/+J1U1+7KxjTKMdSPhNYzp4DzPHH8QCv40SoxigU9Bx9
u/HEElXO4iTnlvwFxRra61Aujfs8YtbtbOs0piOnVd2IxzaQpf86vY8/BFhVRJ/V
mFjocXIIb3GEbXz87s4Mg36O67a1hJcw1vh4BYS72nU9x3C4A+sgXF1wI/4/Z5in
NXSkILLSfye1dbO6q7Fz155StWSlbWDQLkZUM8Q0NUYYqj6iWkgxamOisQaXbXDG
XAaSbi577a0H6TB6vKji7xtZesVuDrgke3vdaiiU4JNPVaHLAOHxqJ6xzL8b1yVh
j/m9bU7dl0JIIlliJmGXLnCzlafoZDi6nSprRIoouLux7hksewo/Tc50hLRPXOXv
r3jULYm4ycs99JVzKdwD3VmOHUQVcCUEUoeV/9YgglJelS++IeXn+Ff5HbzaTH6V
NXzVKwRJ6ohGBBARAgAGBQJM5IoSAAoJEIcBgRWB1GUopvMAn3mTVSM68dWRoSdM
h+c30PhNGhUTAJ9A/gcuObRY4PUiqy3YRqtC7s8OGokCQQQTAQIAKwIbAwUJCWYB
gAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAkzkieYCGQEACgkQksnacbiI6jQs
DA/+K6RXTJ4Ha7n+02su1wOw9NO5FhHcY8kX48PTvjn8ea/AiLLnc3d3NmYzj3dC
hMogGBoEEBfVQ3SbQkW+Lw1OrsBV88wPanvBPepobUVC1Oi9KARMaET3LRZg0GOH
IfWNJIi7IZyIyuBUUdaPjIENjBQTp2HEaLe4ZtHmSlIeZAjTEIlJJnKUFUQ9uNB4
awmtOP7VPwDU9kLFxFBLQHSCnBJoe3U40F4pv9CBUSyCY+bU5APcYFSvG6UTkD+m
KX6kCMKdADLf75N6DwoRdm+EX9rf3S9YJNNQalfNFBWZy1DG5m4oTmxl/uZAII7C
VN5c68n82GH/99YXQbH42TeU7j5A0HCDsmDM8D836UKpiBZU0E4JRjPhMzoelsMv
IWPnzsPNNrf43llOszT6vhSZ007XH7/p3XxznvsWoU8dzo4d9GFB6E1vDEpa6XFf
ptoFRJzfBXFlSrrvVqtDccG2Lnu+87tR/F9XPoQlRPPG98Akx//6DGNgKVeJTfkG
wSf0Qk6dIkF7bthbHw93hzeDsa0DuqBDIZY+5W8/4nqztzT0e4i0KbrhNNeHSpB3
pVamb3645JG4ZcQ7Ak6ISC//8A8WyyCXvL5SNlgoSBzeCZyFQB2JFmpohtVFpdrs
MXbipmpBFJ2ZrnM8iksCQPcBmddHMug1HLjXvjVeol74UrWJAkEEEwECACsCGwMG
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAhkBBQJP6l5zBQkMa9bmAAoJEJLJ2nG4
iOo0edoP/2V3W11XDV7L0j6cxuzu8DR3WZiOD7NY9rIVmwR05CYEUlfx2DwOq1BL
ORri11YXfYaQN8Kd3wYlyvjfanGCmvnovM5fRGAEf/k/ORuIeJG8e79zAtyfhq8W
pCMq6jqzI7eUPhuMlwC52Ysg1/g9Iklkdh7gOpTCwWLENlI9q9eVmxtCDgLNUQC1
z9xa5urBf/2q5agUsTPUBt0RjXQSy1RbSGxTA7tQ5zB59MHTOSkWDX1cX1VHbBXB
wKqxYhPrUDJdMBjDeSCfbh60xIh5gniNV24MQPe64QGnIe3eb2Qwo1Y0KrjMS83+
Y8KyMEo58O7rsGfsaInQqtWc35EMrRrOTXNRjqoRZNBeymnb1+3aRxFc+NJwO5jb
QRAXUD3H9bZG5Mj4C7T3NJXuhYyBqA+mDfoPzlBKgPr3vUFL6vRevjcmV49vnXvr
SFFaLAGGtkZLDt8YsvWXVlmfJYJNtQIa8PB+MkV4KYMfHRicFFeR0ms2LMmBJZBX
ClY2u2LHtgjU/z2eYe1fEgJVV3woRj5UvQlzge46/TJOjyWsgqm3U4qtn1e6fXQp
LAv65WVHOE0iapre/VBr2pVBAESTnxk0ioh1r4JQAb8GNuU1M77/14FtB8DsGRq3
3oIW4EYLHD5DjyOnzC8WUCZcauC7HyoJ9r/FxSfkGytGPA/4n+PkiQJBBBMBAgAr
AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAIZAQUCWwXfRQUJE8TwvwAKCRCS
ydpxuIjqNMEYD/9OpGPJ310KzYmD2uN4OQYwQX0kTiflJk5LZU0e6ho88leCZQvV
0e+lUM4wV4BqZkqVLi/+2BOQ8nhW4jw8phxFAp9AC2z0YoYa/MuLGu4Du9h7iy0D
06u0dWyNFbj+h0a0mfwmhbz/fiDQvLK+HIzViLv6kdCI20b8s2OHiTi97ALJAjAx
cWPydGJb/oii3TvFkE2RUPSCRE/ulsa/Ximuwo66Pi8TiDkEJiErnKmqyRGSgEgr
DJDXpiil9KKFltqGGdbxAxkshi8mKuojoOKCkV4oQftycC9lp2kK3Vlix6zL276A
pSI7P6sQt0eCXVAdjGb09F3uTfkynK090PlQTTjZJSGKpNuBC3vKYEHj9ZeUezpH
uNgXnjEPt3H7BOqk978wTi02OUZpu3McNubxf4IbwrsxKXfM1KYT6uNRStIgalMc
WDkzmpu6z/QY+fJxmMVrqbZMMRHAKu62PoOtdnkVjlEWpRHh0oDxQx+mu9MhnO94
NIXfg6UzYjum7t+vh7vZPjra8/1sBEUGHuxRo4mvK2SDTYgxSra55u5nXefjOvNN
0IVeI512kAWj+02zl74FIfQwv+zsAW8Cyg71ym1VNoDOsKuhJJOF/Ydnv2yrmpp0
oszNAUWEElssW1EUq7PsFUKVfPxIC8z/rrf8H7HqlXmhgPjIzoo4QWGr/rQXQm9o
d2FaIDxib2h3YXpAa2QyLm9yZz6JAlUEEwEKAD8CGwMGCwkIBwMCBhUIAgkKCwQW
AgMBAh4BAheAFiEExJkkebouil1CwYIBksnacbiI6jQFAmAIACIFCRbl3hsACgkQ
ksnacbiI6jT52g//c98FZhfgtDtyUTFqZDVnLqYnD7XTZ4S1zn9M4KfXQEAkuMD3
WaZxyGSnwnnaIjdhbUYW6mQlmzsdvZ6Rm0NFK5bewfjpNNKDlL754vMONqMubKZC
u7Wr184myhgwhV0noAb2qPpxcMUYAIP8P2j6LVafNTKmdlrLPymrPdpI++6iOp7S
7lYvcehVmIewbgfavGjFOgRHiKTC1KtLRN5CXxuRx6q1KEKmWc1m4s84qwAePN8/
nirq4fftPqmR4+MBrWFCTvuz9AkSg+frnzxx4juOkkohIF101lm0r4k4qAa73oZP
MajuZElV718Vew+88J3FpSXEHIoXw6qywHtcxlqn8LJA6SqRkNR1Q3BtIAvwT2L9
vCrQwo2DAXQ7ciZYWS9QCgWYRuCkxQOO5aBpRQXgE+W4FmFgSLjbR3tCcOAtplZ6
O/FwXdK9q1q0lARmO4Y/Vr1uWkzt1pR5M79fWRou8Cgodc0ZEVEQTvCNWZTIwAuk
VIMO1funQvitwTlGnSwJzHlb6ZaDcnh5Ri8q1ptjfX3hC53BurLWzCQ15GjK5p+R
m24YL+6Fc0zUCQGwp+iHG7olXYOJ2oGvJdVCXzuCe+xEA5SE2YNieoVEw2uGh0VJ
i53/xRFV8y/AYZvczfWqADDmOrM2Z0aUrfZxySel+EJIH6rnV0284ygsDNKIRgQQ
EQIABgUCTOSKEgAKCRCHAYEVgdRlKBnXAJ92m4YJsfcTBAAvPaF4iZElYqHN2gCf
bYf4UQcYBvRMyJoSTP2rTfDSUryJAj4EEwECACgFAkzkiXoCGwMFCQlmAYAGCwkI
BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJLJ2nG4iOo0hY0P+gNlJb3nBePD/mN4
6FdZwJKpcNwXbj+VVF67LHqYqIjZLaTy45L54f0ia8efHSdqUwG6AHiNUJFKA8Le
Jgxw9/xcUgFhpkZQlq5jYW+d/QfpNyrDJHmy1YG319RjiWexLO+TZTqQaqnoJO0L
REmmcExYqW0YGCJHQV63htiF9FLZhs0elX94XzePm22CHcsqJKuM4flnEwK1ZqiK
q4ScKs4vXoaUBX7mXJdBUSvFLKJTpq6abaF/G9mCZXaCOF0lcet3XiByh7vvvthr
5g0tiSyS8IKIPW+R6nmO3lqAwHvJt44YW5n+zJ0ZyUabtHM9qGJHAaB0DClzk6Z7
5J7xOncmJ8Iasx8/u7wEigGrBCbXdadFmKZA+KeEa2xGJuy/HwalzK0VQGk6FMhc
wWXsS41m6zYuSLGphtHKZltTVcHtCW9jIxMlPNybYGgZz5w/69ydUgFA4iyRsfXy
yIrqrvI79ddjA/Fa81EPfqSIj0bMWCMecsehehoT1c0mJOza7edx5F2AZqYfHBn9
PuGyjvKuS4aQjtw1KvB2iLsYd/HkGFypZDqb271ssu7rK+JojzirPktBikohzmge
7PSJ84DKRIC97ki3T5v/UtU3815aQAqFFcWCdddVABT9DPYU35iQQ/wTrKKulHux
m+E35BwqR+SD4muk4Q5D30MU8a/9iQI+BBMBAgAoAhsDBgsJCAcDAgYVCAIJCgsE
FgIDAQIeAQIXgAUCT+pedwUJDGvW5gAKCRCSydpxuIjqNIPFD/97ZEk6+WqhvrUy
MD2cqJfTCRXXl9ImodT6K1XZgOxsh6Co4qi0zyKHXCLbwXByJ4WZm3XhvBo+uhM4
qYIBruLrFfGVuk0IlXhaAdSZirOowCniO2PCJ2B7deEfeFmeJelb+KZfmuYe47Ln
jIjtDnVfHdPXo18JJu2OABkJzEcTvvyEkyH4hUpiIYRIvs8WtBzSQffZ48GL1DgY
ea7r7x8CUNfS59bsxJVg/BBt6Un5s63w5JiQF2Dlu49K3nBaEma4jfmrowNO5zbM
8mRMdKod23K1Y68EWu168tPj/bUbszV3TAca42DTarQZYnRn0xiqvC2I47PS2pIy
e/spQH4FbjQouD/543A7++nmd16gkLZoly2/Tg0oTaaFKy1EubiPXPRWnBlF9bit
J85uQNDmjid2Py2YCUGsHRUHBz7BtfZVHX6rnCN1BXNiyUEb1hgCvbaquYfCdjwV
N/c/W/anDtgsarqyvdAgpuDySe8+JEJ2RDggZBGjk1xXKLsPyOJ1McILPQ8Uk/lh
2rnGQ5hy9/RhZ7NvKi8yYban/6yemcYCw9YnK1odHpbeX/BTkQvWmzC2mOGUHN1b
DSmgidNISBxk4ukGhfxj3L3ytTBQMT3ZCRrHo6lOH614RxV1Y9JID7s82szARm3q
sKd1YWmk1fCdkt4b6JO06TologVz0IkCPgQTAQIAKAIbAwYLCQgHAwIGFQgCCQoL
BBYCAwECHgECF4AFAlsF30cFCRPE8L8ACgkQksnacbiI6jR6Vg//QaZ8JXj5BNtI
bp4IbqjGiKFQ0ofr4zKpyu+W5hDgMPoBhW3T1FVXYeBmoaWv1TsGjixwq0ucdBWn
y6+NvL09jBvq5LpKp2+r2KI9vfLjHbLc/cSiZI+fN2/6PJTZMFqf2rWmRCEAW6Gj
vwEVDHlHgMXVMuJiFX1bT8JPvYStZ2v0su2960rBYNA2NxVg0iYE1K8p3Xvugsop
pXuX5jltZNh1I2jPGN9wcYGV69N6Pu8mkSzGm+uiRWrkYKT+w6V2W4jYbuJiNQ7Z
b1lz6+lP07t3o+at3BaafxWjIhIX/rWvtedO32OFF4HO29sa2GWrVNdAa8ViITYW
XpeVHeqmAHPjamEettmKoE4dG3K/5H6sBaF8hJRxff8x4yEbjo0t2jod5hG16+Wx
VohU2JdAazI3rdFRQXgV+vnlRaYv5cb//tur4VnqyTHmpIjq7WRXVmdworCjMyZp
I6xtFZKePQrYm2fAl3aBdTZ9FexV3G3we/gYfD0Gfg2LLYC0TU+ifai0JdPhmuWl
b6IV1BNsnLpYnb+nRS+kY41blo+i/EuUaezL/rN85a4GIWz8YbLkhX/kmyAgks5t
wL7HsLJV+b4SMJnPyX9Yq7wvQICqFwXHEqhwfCV8KCE5VY4RaPVk9ToB84KS4NIF
hNtb8c6GTmpyVZeKp0bavh+ovlL+FhG5Ag0ETOSJAgEQAMQE81j1XU7qUWB7rk6b
UI1f18I1vnFapWGtnagfrEhk5hcP+zO+zIyQx30Vj7gqv1cjHQYn6/BwhL0vlRV8
iswZHfybIMGx/Zx4c5vkzJY9G88h/xg5GdE3nPAoh99bTXYNA1NcTWrQ3Nnads50
khhHyHsopaZI7LDDo8X2f+BE5J9DdLFxQo7wtH8JdUCVNmuF4QB+soMnT7luEgjA
Q0Vlud4mYnwdNrYFofu7BL0BlEO3lIGgB6NexBTife4BO7sxZQ9zqilSWh8I5DM9
W6A8uCwM5FThrByHY4rNf/++5USkXpdd5YfgPuqxktdH5NBF0gKnBfKSqMbZuA6W
NsxxzzTgdNstgd1yZACV5aR7T29/py8HyL9Uw/YyZTYGOgFiPbPstYRAEkvVWM14
1FNqTWaIXn/PxnKWiFB6IElUcuz9a5z6Jg0RSwV/loEHaJ6idGkMTDwLJ6N+DvXF
Hw7SoUALQnfdu+GU0SZANYzn00hYdzM2Z8E9NKU5fRMT9XERigubGEisn8p+912V
Binnfurhi5kboL/WfYEkYPoKbVkvOV+7kHjARWIDmWSpGpptAVYCfekVqI6jnI6q
THib/qA4xugt3TrFXftRpExJoemg4e7lL6JTLHF/J8BkzjbMef1PctrVI1PYiDQJ
0Shcuyrs814BYSwrT+5CL6ETABEBAAGJAjwEGAEKACYCGwwWIQTEmSR5ui6KXULB
ggGSydpxuIjqNAUCYAgAUgUJFuXeUAAKCRCSydpxuIjqNKWPEACvP9Q7zB4bgiAl
y64sj/jM0OiEMUPhB9u8ZRxzgWw0PqpetKYtiPww3kTF+xk7HCUjt0e12h11p3sX
OWwBfOstLohdb/XJx2hzpFA99SfxbdbDS+WIhHNeRSoTtun1RNGpnmBQAd07A8OM
iUkgP4PJmT9Lx7AcsVR5khAYaCNvAZfrdQrR3Urq6ZIR5r4JQM45/pJQ4Oq19yEq
d89hU3ej+iz2+19Uis4iHY2XpCcP6IZNPawalhlji/gWlKHbOgk454qIptcSvzCx
/gdmifkV6/kzKYWOy691PFpRT40Q9ipRllyajTFpYWe/dNw9tAICjTT6WNOwBMjB
rATutspRxiDDtdl41Kbbca7n8AW/aB4BISkvEZUnMBcxf+go88/fN8ZasyNEtwdT
443UrNnrC3ZVBgqUNSAPfTgEv2XAMlWAeBJOpVsRoZBiXnPsUDYs024toQCXpN2a
3Bn+fFL9BXPor2B0vd1YB/yhAvCWcJ2WhBzcZ01HCAanL1DBa8uvg4p2BwuHSUuW
R5HaZNMj6WJSj0Uk06LFTLaOfCv6ROANhvk37NRx6MX2QI4vEkbycYMBGp14l/g0
oNKtb3gzOgA4QElT3iu9za3JpYF19qCnS+yMtw7sMy4J33SRO2vJ8RLgbgZWZTqD
LXuQsMKeyq2w2PXDtjLNmCUMNnTR/A==
=6UFY
-----END PGP PUBLIC KEY BLOCK-----


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

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






















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
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBGPbkSYBEADJ6b2bY8Uva2JSdt/fsjhY0ZD/BEeD9ersNK1OOcZVyqI9Z+hR
J2BIvbEyCiPxmG2+A7/KVCOJrrgt2dmw1soXS4ePepYoiq36Oqjp/Nn2kVMOF6e1
XeS+O/KVIfLMLZOFItlGFsMOdZRLMzjbI3aUTsKPuIpdJ6A5NEodBZvfGHdXzV03
sGEw2T9uPi3AqR+ioKxyDmTvcAWiI9NsZXDhN0mRg/IQoAs9pkHhkbUV1BmxNOFi
uuaDbptH3jdYhiQb1E7BJJU5CNhr9Zv1F7PD0Gr4Q5OgoDnk/MZKv4MVsl+zP444
bHbuiAzfWkm7QbaM+NF+NCcb7ujULJH/Ujgbascatc5dNDF51cA4BDZekjeOUI7Z
DIsg1UtkB8d3VRKlw+J0Lt9ZyH7zwKB7Jzk6Gbn1/YSBnVWq0SZmomqiUVeBWBv7
gogFsbUkD35mafdBVdkRV4Yce9nrmDwog+5d7jriOKbYQ0MmSwcBeHbHnEGt72kx
Eov8YlzssdqNDTLZUFix5I0LZHAaNT8LmjvkVuyz18J8EDq+x150e4ThP4orhAkB
c/Of3B8OmYlG0fgM3zeawbvE16gnQ1InH2AGNLxBghizjSgHRDOo5gzUDWjXlGN3
/z2/7+yLSiUEUskz4gCVLuOFhXrCRyGcSpUyBA/nGlfJ/tC3HQTyqHDFrwARAQAB
tBlQYWhla28gPGRldkBwYWhla28uY2xvdWQ+iQJOBBMBCgA4FiEE2gznv2g1PQbF






















cEHuJiyrIeahkikFAmPbkSYCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ
JiyrIeahkikITA/+Lt+0Qwu7qIrvJ7Pgt5EgC3QGGSwlNl9lGErSuOQ0jpNExwyB
QhQRHA8lnidrFlRX5FMg6IeyEoyn7NXjY7dzA5lkeNn5mWePl4pkLPz7XXS7Pwqd
gyr5pc7Y9BpTjPsp2izZr5sbBlzYdFDXaMo69DdZHLILTI0LK0qALfBDcqQn4e/Y
/82m5nBqsmoGYup2HnTctBjgAamjhJMyGQ/JXDjBc6PV2O+Ew7n9kp8dwbtY4UpM
Oy8Gs4dzk4Ra/t6O4Zo0MporLZ6zulyWRVgvU4dVbtJFNJEKa4DXMxUOklilFX//
uu665gEAS+F6f/Cb6OFHM7IxdLp4ng6z4A+jVxj4vXDgq5CP1F/L8EUqtl2kcXdU
IQuzba7s8pVDM8ujLwR/s8VbgG1w+xYbZtqkEqpoAIIezvXmx2gnnuCAakw+JaP9
QSHmmD/5MW/GVXPw3L2+cgmI5axmkPP02A9BSlMcdV82Od5s2Nnat2AAkqPbjK3u
/TTFRAumSTv51ezBTRldSxgYHEoWQBdvv7i+SnZniffYdvxTKklIhK2xuXY1Bzku
5Dk9SR0oQ1DvQA+TFK5XFhaWl1iuuzoTD4TW6r5NLqEHRUdxHpe8QTAkQi4hhKyb
7nY2IgjREqnrLvdfm9KSWjrV26Fe04vcRSYbzcvr6EdDwGmT7PNbOx9gt+a5Ag0E






















Y9uRJgEQAKNek9274WllUatGVThIjZvpYtpu5Q53TEZozYTRp4WLJDNtH5W7D0vK
8icr9e7CUILIQKhL6peugJXLj1HXok2vuK0ITWLCseApuXyvdzX8+5Z3QC1gzC6z
AjA0r7Kc6cPg+nUL7SHSKuTY1+LdadC4AYYTFN9hq+QcEBtx+dunTxdjjsyu4aJc
h2g8565mRiccRY1LSgYpmM8Blypj+WjJ1KF7v+JNhvfiAoadPnVvUODMq1pdHW4E
ucIP7Supw+qIVlRmaK+STDPEDU8diy7YMiC+dvxJLV1Yl91qLx9EK5RcvkcUUlvD
w3ESotzEJSiRF1Rdv1bUAtIVi7ZJGYfqr2PFOnGLWJRWYxwFXpv9ADk1Onnu33T2
tybsVUN+zVHdo3q7vDbSYXbaeeFg4voaJuR9kn3nDKS5Lidh2HQiUWphVz0vScRD
a//nEYwwsu93aTPK/gW5BxAp/LLEjQgjXSH3DK3FNksy3gyf5zdg8WqzPqWocXGR
P6UqChW1ujXQ8dNDRH8arqz5q0kgoFgsY90Hf4aIB0HmCdlsPK8BtTMQUS4oZLNA
G6l/52PnOvkhodYzvWShS4e8Uo6GH8b/mmo0Z7qzlkpuBScYDx02PirHOqrto842
xUvlk252n4ZCc5zNLa1Zv93afpt+1abihki/i4nPherUtGkoCby5ABEBAAGJAjYE
GAEKACAWIQTaDOe/aDU9BsVwQe4mLKsh5qGSKQUCY9uRJgIbDAAKCRAmLKsh5qGS






















KZUPD/9CPZzOvWQCuJlPiDbENZRbLMudShseDlfddkDmAL/9mTprc7j0WWxnMVgC
mObW6t/wiOP4ARw5/KCr3xrZ/O7aO7Fn98WxyTYdEr0gmE8m+nlalHuIDnfktp9O
qyCZ2qjQaGKY0fJLUkyDCJRHa4jOST56LdpH/FxjAcJcP1MTJssy0LxgB2e+FUGy
JdT3+4jUMBO1NBiM84LaV47tygEYdVbO0KP/uRHK3cKGLhGwMT/LdOLc2nmxfoty
ZkM5nP0gxVizQOrXlDOEqRiZ/GyG8TZD91URzReZ8ssALbD+HQiuCllvodxqWiW8
ZsWi0/6Ht5mb1t4m1+Wy+Gukdh/a6/n/W4/ajWIOpUxN71e0wd+jqEll424DrLD6
t/M2rtz6BKHum2rSlIu9UOHDXmXKjEpz2XJP8kJsJc5AdOQS9gLn3aiNoyLgDAuf
RlSHkj9nx7XmxNR7m7UKap7Glp9RrHo4PuCczQYbgAWfoeoESBq6bqGS/4vl5c8E
FYB7uQ5YA71prEdDmuDsEq0tI7RkM68d4KqLM4Ag3OWVIDpDGhTtjizCe908o7zl
uhpLMHCE963KKx+zL7ktThmpHz8V8/sqfbDylmSC2suaJhJCi+i5RakThDdw5DaS
5yJfKGPwOjx8m2A6POwV4CGnhAcE9VXzlUCP2XU73JVcMJ3QuA==























=4SMJ
-----END PGP PUBLIC KEY BLOCK-----

Name change from src/pubkey.asc to src/pubkey_old.asc.

Added src/pubkey_signed.asc version [2005d5e930].















































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
-----BEGIN PGP MESSAGE-----

owF9V2vsNNVZBwoVJ9BStAhYIhWKNhOY3dmZnRmBNnO/7s59d2exkLnszs59Zmdn
dmcsGCG0XIoQwEiVUIFg2pRXaANN+9JWWzCNRai2sSi9KGjRIFVSUxoKFv9Fbfzk
+XbOk+fDye/yPL/bTn/LCcCJt//Fs9Vnb3gROfHYaZQHlI2XrLpL3Nr3v3X7RRf/
9FAsL07P13jtfM2mFJE+X2ad8ylFpeU3ywCQ6eKU4jUvMR2KJRlp7MGeg9utC0tm
sIPWdbxxBksGotgVQ6y29VQeqqq/nHWVSCzBjQFIMCW2HtvRkXbIeBgkMUie0aq0
3YY7OMj2w7pYmMhKW5VOEVWjsVrFJTTN4WQ2UbnxaggsViaoHvWIa2WiLFVO3KU8
V0/UYGkokz72xJFrW7WsNWIZSGMSnbJFQC3bNS8Ei342GAE1z+5hi2i0aERWBhgV
8qFjMqv1yXkkEtN6uWA200FmhJCoF2RNlImwSTx7NqSyw1TlIqBpXMYrd8IoDpxN
pHtDFqMkyUbp6WZLLNshh2nMgN8iOqqGBZMn0GQpt8hkVqdgryEIAniC10Rkv54n
GaZ77gSccuCU9j2siW1FEiA7Dr0jVNydjwZThkOHPolQzHKVxCvVFrElwIh1OLR3
CYUHo5khp3tQGig7YtkJWL+XKUzqkzHv5UPIMal8Nq8G5jIrsiqyZytqTrUYEBYh
V3t2wozQzF0H1CxIjBni+Csi32bMvghBNMDibaTKnqMPJpm596mV4Ak5y+8wODkA
bNHiTtrXdVBNGUtZ2lx0QMWBshRId2rhSha3yazp+iEu4SxTgYchOlgh1kZDiu2G
TCjAh9T1iMLVzEn5wTqcjPqVu/dadjgOc30o5gJM8lPlQIWbqI/NUDAYtUDD3mbm
8SLlpyMA6mEIAzvFjGzWrpMeCemZ0qjcZrGljY73zdLuKBLK+XQtQTt6JOhWVwkM
t92TBqmTFLCjUt2Zb1IXxkONN9Igofb/c28c+NAGcx2MdEmlqAlFhyTCRSwLh33e
wuFQ0z0O8FmhkaJuK67cTRIlHJm9qQua3084ep+I1F7kOb2QaLKmKcqhyT1LCyFL
cwhJ0mGiA/+nWbRICFR24EDfN1glblsJ08Idyob0SOd5c59OUyLl2a3ZqPogLqfs
Yd9RgL7RDYHE0zwKtlxqLFBuEo7FVccWXY5NF7GDBT2JpslqmqPZfKWlSJkoWo8t
Fiam7asACLstWvqYQ1ClFWt1CUf9covWHpX2TsAxC3dSjAkmWAqKqFhH8MqDilTW
FONXeo6sIAeAcDhDc6qqs4J3mhIWcsvfUXFIulm8kSYdr0PSgokpf6zNYBVk91hO
JCUe7L2dg9jlBFA7nK+RoE8Qw4V2YxVZFoNJWWyV5bhv0m5uzMLWRoKZt5O4qcTK
LsIsJgdbTdIo5RYQdKTG8RgNWdIEufEaor2xygkTTDwESonk4bhHSDCeHWKkXTBh
hdLakIMUnLWrXQon/iKwAVFves/FarycMRO8iZW9AdX4zAv54R48ON5yVyVsVRak
KK76dpEd4DDPG5p0kyPZuRoB6KaQZQyETuYQP1to+5ECg36Yiah7yBJNG8AkQZnp
xA9mOKwGaA1Pc3cHk2RSaV4sjxoAsizOIJvMtFp0uOop64iM5iF0BLaY61TQtlgE
mvkyj9ZrJ2gPlpyk4kaGD83CGVJ90gAokxCmMSj0IdPqJGhxMrrgNu48HUZN0xcW
g1jz8RadKhUrGHZwEMoVrltkokfIZiN3HoDlDiyGscFW+VZpg3VGyOY83s7gMbca
IK1vmI7X++12zAbMns8sTJt66oEId6CLkuGABRyiMaSQ1Ul5ukoIGEPmaWq7O35m
bcR42ZbOrmxQHR1Z7LLoHcsokbkiMdOdgM4xZtDKAB75W2KF0baoiLq8Ucblqgml
hRIPhUWRwG0jD0RrrtD1iiybRdcG/QIH0eVIp4dhT497gIzJwRaT/bGvhWBuK5gp
mHJjOUNQCdyARkjHsbgpsalA3Wep3QEMmtw6BHFcdw3iSj6wgUMcHaOZEfm+4QwV
M3TKbIJTaVfG4DyWhjKHtaA03bTriCzcQMtnra0yk2pYBsIcYYHGFzXMbMo9WImz
1MhcGTQtRmMZGw+iDnMmEQ0ewScps6GTEsNKORCsjBp+m/i2nbYMsB+xZrHrWcmM
DG5oBO3Qs8mdOIuwpcQ762oLa5ya88pcMubOYc8typYgmWSoHvFxNLJgYNd59cye
gv1MCIpRhbWMZzoLz12tuBBpC1dqDCLJRzkjm6gSBRtY0CN7Xm5m/aA1fYMBXAjK
WWe/rxti5FqaDIVzlDqQJaQobKyH8cIURow84qZJ3Y3Cbo32QYjPq16r5oW/4A1A
G9sVvZkPm3ih40fDyxBwd1v1aDVIwoILa4cYCGvEFamBkNFBWmsyTu2siW6bSLFU
piTAj1MIhbVcbZNNETh9Ozc3R/sAbhdjXsA9KMuKwRKr+jQpG8r0HeYwgLVoK6jV
dlfgCAwc7DZNYBTOkSXto/1UcYfL9ug363IHDl0vOnJbKEJybbPa2js+KWivQ0mK
pUiSl8jYYQGeZGWSJueibrmMuoJcxiaoerbXV0imyPUGrXhT1m36vykvegxJyrRB
/qwGyEtbYyCC1pa92s51upFSLWI8dro0PGXSBOamXjHpOggSJiMViMiscutj8WA+
P+STWUgDmerNxztoH6kaQhp7FJLp7eiwXUIq5qoYlxP4/NBZTsBuB2HG4hmYp24q
NCKTr5NdSahA1dFLuIp1l5edwVpS7KRjaMkQXCRWTQsdK0EpQNwhJn3J14YTS6rr
bqAcQgpegZzNd4AUWCMQie0JpQ6nVDTBEcWdIdiuC1nnyIvVgaxBjSHII1/mlc3R
wLMgJVAVH86zw7rYdcAymaC5NggPs6jX1e0iZVS2MqIlxHc8bi0ZYmgbvbFa4nVN
Kh4DHhGxodO0LYJDNY/mOLCs59EAGgs7NPOGOyQbgvMO5Jsk2EDuGMqhOQK58VxU
S/swxYarwT4A44pNUwRGmK3CjIEdNIG3u35MyUKTwVszFRvCVgVmkS3kmC17eCFp
eCLVko+SgaqbRKjkIzeaFp0SMmSzBozUFJKYyA/YkedPDSzDbNktMT4tCWMrFIjW
0H6vO15IztfFqmBNqhp7R/hDSJuiPs4CnENhjY46JDYst0fGmTVMzVaDnYgZyWSM
B4hcKROEDEfqfCYyJcNvrF0c9fSKGOAF1qdAsymViUCzxHgkywewV7BkZ22yUujx
GQ7V1dpjujQzabhuXGkj0REYoYabWBsm2KOMawJoJ61lXtur8QHPYHKsqfsZQvP5
hvRZYrboU5vW4IWNjaSZP5FGekNefjlwOWJOJODNfZ+dMv9PGLjxpNHJJ5wInHDe
We8++YmPXtgdv+SmD1BfuvZnKeOUk36aK04Afv6M/315DXj7Gx98lH0r/k/PH9ug
3/rM185453s/YX3hbOWJ9Nar3/HImVf9SbKovg5AyHmXqc33f+8/XfuGiHroffY9
F5782IWv9+e8ev2lymnvOu9ff/N3vnn6n37kb8+485Zb7p9f+aR+4kUP9RcNHn75
K+2jV9/0/AUZgn3xS+2df/XpC5++m5Xe03/qh8eeqYnqhf2xd5966d3i333+qQvu
jvQrz33xjx8/854P3Ptof9VnmWvf2D193Xc+f95Lp3/xbWdSLx37td/+3NmXG8de
W99y7gC/49rv3faJm7Cbnvvuae+PHzn+0Puf++YrZ93c5b/7k/Gnf/SW1amZ1J39
qPNHffjh7Onn/n3/Q3WZX3ff8t5XH3rjiotd4rKvvTRZN5c/8Ad3xZ+77VRuoH33
y4+9+HXntf6Fuz4yfvbWx79z6cG97eLfuHTc32dd8soDXf8f9y+ue9/P3Rid8snk
96+7+dn7r1Ku8kpC+LdP/uUz7/ryY+/snr7vK9/4Begfbzj/xhP++eMPX/mDf7ke
/BD7k2f6Pzzcds/bXn3g+Dkfa6/+s+df/sGvfGE/u+bEX/7V2+86xn3j+z9+9s7h
V/e33/Tko9c8+ev7S/KTTj7+5/Vf/+LbH3no49d9G7/1sgcffOuPmcEr7We6Ry+T
30sj3M1fff34BWDV31o+/J7Tz33ihXMe/NA5w7855Spg8aMrtI8W+seg+B2fuvfG
v3/gmtN+6x/UF+gr9NFVr+tnPN6j3149dR34vRueKl9If+ms48yZV9AfPC4un73j
5TuuJ/4L
=apuZ
-----END PGP MESSAGE-----

Modified src/scripts/cron.php from [4119a18488] to [3b05c3796a].

19
20
21
22
23
24
25
26
	$s = new Sauvegarde;
	$s->auto();
}

// Exécution des rappels automatiques
Reminders::sendPending();

Plugin::fireSignal('cron');







|
19
20
21
22
23
24
25
26
	$s = new Sauvegarde;
	$s->auto();
}

// Exécution des rappels automatiques
Reminders::sendPending();

Plugins::fireSignal('cron');

Modified src/skel-dist/modules/bilan_pc/icon.svg from [a7067a2e0b] to [c195a4c36b].

1
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" width="48" height="48" id="img" stroke-width="0"><path d="m224 50h-192a6.00029 6.00029 0 0 0 -6 6v136a14.01572 14.01572 0 0 0 14 14h176a14.01572 14.01572 0 0 0 14-14v-136a6.00029 6.00029 0 0 0 -6-6zm-186 60h44v36h-44zm56 0h124v36h-124zm124-48v36h-180v-36zm-180 130v-34h44v36h-42a2.002 2.002 0 0 1 -2-2zm178 2h-122v-36h124v34a2.002 2.002 0 0 1 -2 2z"/></svg>
|
1
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none"><path d="m224 50h-192a6.00029 6.00029 0 0 0 -6 6v136a14.01572 14.01572 0 0 0 14 14h176a14.01572 14.01572 0 0 0 14-14v-136a6.00029 6.00029 0 0 0 -6-6zm-186 60h44v36h-44zm56 0h124v36h-124zm124-48v36h-180v-36zm-180 130v-34h44v36h-42a2.002 2.002 0 0 1 -2-2zm178 2h-122v-36h124v34a2.002 2.002 0 0 1 -2 2z"/></svg>

Added src/skel-dist/modules/bilan_pc/module.ini version [8180b18867].









>
>
>
>
1
2
3
4
name="Bilan expert"
description="Bilan annuel selon le modèle du plan comptable des associations 2020"
author="Paheko"
author_url="https://paheko.cloud/"

Deleted src/skel-dist/modules/bilan_pc/module.json version [8f119bbb23].

1
2
3
4
{
	"label": "Bilan expert",
	"description": "Bilan annuel selon le modèle du plan comptable des associations 2020"
}
<
<
<
<








Modified src/skel-dist/modules/carte_membre/carte.html from [1045e75f52] to [c7da92959a].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{#restrict block=true section="users" level="read"}}
{{/restrict}}
{{if !$_GET.id}}

	{{:admin_header title="Carte de membre" current="users"}}

	{{:error message="Aucun numéro de membre n'a été fourni"}}

	{{:admin_footer}}

{{else}}
	{{#users id=$_GET.id}}
		{{:assign title="%s - Carte de membre"|args:$user_name}}

		{{if $_GET.print == 'pdf'}}
			{{:http type="pdf" download="%s.pdf"|args:$title}}
		{{/if}}
		<!DOCTYPE html>
		<html>
		<head>












|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{#restrict block=true section="users" level="read"}}
{{/restrict}}
{{if !$_GET.id}}

	{{:admin_header title="Carte de membre" current="users"}}

	{{:error message="Aucun numéro de membre n'a été fourni"}}

	{{:admin_footer}}

{{else}}
	{{#users id=$_GET.id}}
		{{:assign title="%s - Carte de membre"|args:$_name}}

		{{if $_GET.print == 'pdf'}}
			{{:http type="pdf" download="%s.pdf"|args:$title}}
		{{/if}}
		<!DOCTYPE html>
		<html>
		<head>
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
			</style>
		</head>

		<body>

		<main>
			{{if $photo}}
				<img src="{{$photo.0.url}}" alt="" class="logo" />
			{{else}}
				<img src="{{$config.files.logo}}?150px" alt="" class="logo" />
			{{/if}}

			<h1>{{$user_name}}</h1>

			<h2>N°{{$user_number}}</h2>

			{{#subscriptions user=$id active=true}}
			<h3>{{$label}} valide jusqu'au {{$expiry_date|date_short}}</h3>
			{{/subscriptions}}

			<div style="clear:both"></div>

		{{:include file="modules/_footer.html"}}
	{{else}}
		{{:error message="Le numéro de membre fourni n'existe pas."}}
	{{/users}}
{{/if}}







|




|

|












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
			</style>
		</head>

		<body>

		<main>
			{{if $photo}}
				<img src="{{$photo.0.url}}?150px" alt="" class="photo" />
			{{else}}
				<img src="{{$config.files.logo}}?150px" alt="" class="logo" />
			{{/if}}

			<h1>{{$_name}}</h1>

			<h2>N°{{$_number}}</h2>

			{{#subscriptions user=$id active=true}}
			<h3>{{$label}} valide jusqu'au {{$expiry_date|date_short}}</h3>
			{{/subscriptions}}

			<div style="clear:both"></div>

		{{:include file="modules/_footer.html"}}
	{{else}}
		{{:error message="Le numéro de membre fourni n'existe pas."}}
	{{/users}}
{{/if}}

Modified src/skel-dist/modules/carte_membre/icon.svg from [eb56079363] to [c561bd2bef].

1
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="48" height="48" id="img" stroke-width="0"><g><path d="m15 3v10h-14v-10zm1-1h-16v12h16z"/><path d="m8 5h6v1h-6z"/><path d="m8 7h6v1h-6z"/><path d="m8 9h3v1h-3z"/><path d="m5.4 7h-.4v-.1c.6-.2 1-.8 1-1.4 0-.8-.7-1.5-1.5-1.5s-1.5.7-1.5 1.5c0 .7.4 1.2 1 1.4v.1h-.4c-.9 0-1.6.7-1.6 1.6v2.4h5v-2.4c0-.9-.7-1.6-1.6-1.6z"/></g></svg>
|
1
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none"><g><path d="m15 3v10h-14v-10zm1-1h-16v12h16z"/><path d="m8 5h6v1h-6z"/><path d="m8 7h6v1h-6z"/><path d="m8 9h3v1h-3z"/><path d="m5.4 7h-.4v-.1c.6-.2 1-.8 1-1.4 0-.8-.7-1.5-1.5-1.5s-1.5.7-1.5 1.5c0 .7.4 1.2 1 1.4v.1h-.4c-.9 0-1.6.7-1.6 1.6v2.4h5v-2.4c0-.9-.7-1.6-1.6-1.6z"/></g></svg>

Added src/skel-dist/modules/carte_membre/module.ini version [6e56d61037].









>
>
>
>
1
2
3
4
name="Carte de membre"
description="Impression de carte de membre, à l'unité, par planche de plusieurs membres, ou export en PDF."
author="Paheko"
author_url="https://paheko.cloud/"

Deleted src/skel-dist/modules/carte_membre/module.json version [3b6ce77fe3].

1
2
3
4
{
	"label": "Carte de membre",
	"description": "Carte de membre, imprimable par membre ou en planche de plusieurs membres"
}
<
<
<
<








Modified src/skel-dist/modules/invoice/icon.svg from [20d1661429] to [d4539d8aff].

1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" stroke-width="3" fill="none" width="48" height="48" id="img"><path d="M52.35,57.08H11.65v-50A.11.11,0,0,1,11.81,7l4.11,3.85a.11.11,0,0,0,.13,0L19.35,7a.09.09,0,0,1,.12,0l3.72,3.89a.11.11,0,0,0,.13,0L26.61,7a.09.09,0,0,1,.13,0l2.86,3.87a.1.1,0,0,0,.14,0L33,7a.09.09,0,0,1,.13,0l2.69,3.85a.1.1,0,0,0,.14,0L38.86,7A.1.1,0,0,1,39,7l2.85,3.85a.1.1,0,0,0,.14,0L44.7,7a.09.09,0,0,1,.15,0l2.25,3.84a.09.09,0,0,0,.13,0L52.2,7a.1.1,0,0,1,.15.09Z" stroke-linecap="round"/><line x1="19.42" y1="43.04" x2="46.02" y2="43.04" stroke-linecap="round"/><line x1="19.42" y1="49.29" x2="46.02" y2="49.29" stroke-linecap="round"/><path d="M40.21,34.51a9,9,0,1,1-5.48-16.15,8.86,8.86,0,0,1,3.78.83"/><line x1="21.22" y1="25.18" x2="36.21" y2="25.18"/><line x1="21.22" y1="29.76" x2="34.4" y2="29.76"/></svg>
|
1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" stroke-width="3" fill="none" width="100%" height="100%" id="img"><path d="M52.35,57.08H11.65v-50A.11.11,0,0,1,11.81,7l4.11,3.85a.11.11,0,0,0,.13,0L19.35,7a.09.09,0,0,1,.12,0l3.72,3.89a.11.11,0,0,0,.13,0L26.61,7a.09.09,0,0,1,.13,0l2.86,3.87a.1.1,0,0,0,.14,0L33,7a.09.09,0,0,1,.13,0l2.69,3.85a.1.1,0,0,0,.14,0L38.86,7A.1.1,0,0,1,39,7l2.85,3.85a.1.1,0,0,0,.14,0L44.7,7a.09.09,0,0,1,.15,0l2.25,3.84a.09.09,0,0,0,.13,0L52.2,7a.1.1,0,0,1,.15.09Z" stroke-linecap="round"/><line x1="19.42" y1="43.04" x2="46.02" y2="43.04" stroke-linecap="round"/><line x1="19.42" y1="49.29" x2="46.02" y2="49.29" stroke-linecap="round"/><path d="M40.21,34.51a9,9,0,1,1-5.48-16.15,8.86,8.86,0,0,1,3.78.83"/><line x1="21.22" y1="25.18" x2="36.21" y2="25.18"/><line x1="21.22" y1="29.76" x2="34.4" y2="29.76"/></svg>

Modified src/skel-dist/modules/invoice/module.ini from [86ca7d801f] to [f6d67af1f5].

1
2
3
4

5
6
7
8
{
	"label": "Devis et factures",
	"description": "Permet de créer des devis et des factures, et de les imprimer",
	"author": "Paheko",

	"license": "GNU AGPL v3",
	"last_id": "3",
	"last_quotation_id": "555"
}
<
|
|
|
>
|
<
<
<

1
2
3
4
5




name="Devis et factures"
description="Permet de créer des devis et des factures, et de les imprimer"
author="Paheko"
author_url="https://paheko.cloud/"
license="GNU AGPL v3"



Modified src/skel-dist/modules/invoice/new_quotation.html from [ef5595bc77] to [0ef91b4f29].

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
<h1>Création d'un devis</h1>

{{if $_POST.quotation_submit}}
	{{:assign computed_total=0}}
	{{:assign errors=null}}
	{{:assign items=null}}
	{{#foreach from=$_POST.items key='index' item='item'}}
		{{if $item.name === ''}}{{:assign var='errors[]' value="Le nom de l'article est requis."}}{{/if}}
		{{if $item.unit_price < 0 || $item.unit_price != $item.unit_price|floatval|strtolower}} {{* Hack to check the data is a number *}}
			{{:assign var='errors[]' value='Le prix saisi pour "%s" est invalide.'|args:$item.name}}
		{{/if}}
		{{if $item.quantity < 0 || $item.quantity != $item.quantity|floatval|strtolower}}{{:assign var='errors[]' value='La quantité saisie pour "%s" est invalide.'|args:$item.name}}{{/if}}

		{{:assign var='computed_item' value=$item}}
		{{:assign var='computed_item[unit_price]' value='%d * 100'|math:$item.unit_price|floatval}}
		{{:assign var='computed_item[quantity]' value=$item.quantity|intval}}
		{{:assign var='items[]' value=$computed_item}}
		{{:assign var='computed_total' value="%d + %d * %d"|args:$computed_total:$item.unit_price:$item.quantity|math}}
	{{/foreach}}

	{{if ($computed_total != $_POST.quotation_total) || ($computed_total < 0)}}
		{{:assign var='errors[]' value='Erreur de calcul du total. Enregistrement du devis refusé.'}}
	{{/if}}

	{{if $errors|count}}
		{{#foreach from=$errors item='error'}}
			<p class="error block">{{$error}}</p>
		{{/foreach}}
	{{else}}







|

|

|


|
|
|




|







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
<h1>Création d'un devis</h1>

{{if $_POST.quotation_submit}}
	{{:assign computed_total=0}}
	{{:assign errors=null}}
	{{:assign items=null}}
	{{#foreach from=$_POST.items key='index' item='item'}}
		{{if $item.name === ''}}{{:assign var='errors.' value="Le nom de l'article est requis."}}{{/if}}
		{{if $item.unit_price < 0 || $item.unit_price != $item.unit_price|floatval|strtolower}} {{* Hack to check the data is a number *}}
			{{:assign var='errors.' value='Le prix saisi pour "%s" est invalide.'|args:$item.name}}
		{{/if}}
		{{if $item.quantity < 0 || $item.quantity != $item.quantity|floatval|strtolower}}{{:assign var='errors.' value='La quantité saisie pour "%s" est invalide.'|args:$item.name}}{{/if}}

		{{:assign var='computed_item' value=$item}}
		{{:assign var='computed_item.unit_price' value='%d * 100'|math:$item.unit_price|floatval}}
		{{:assign var='computed_item.quantity' value=$item.quantity|intval}}
		{{:assign var='items.' value=$computed_item}}
		{{:assign var='computed_total' value="%d + %d * %d"|args:$computed_total:$item.unit_price:$item.quantity|math}}
	{{/foreach}}

	{{if ($computed_total != $_POST.quotation_total) || ($computed_total < 0)}}
		{{:assign var='errors.' value='Erreur de calcul du total. Enregistrement du devis refusé.'}}
	{{/if}}

	{{if $errors|count}}
		{{#foreach from=$errors item='error'}}
			<p class="error block">{{$error}}</p>
		{{/foreach}}
	{{else}}

Modified src/skel-dist/modules/ouvertures/config.html from [b92435bfba] to [8bce58e3d1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{{:admin_header title="Configuration des ouvertures"}}

{{if $_POST.save}}
	{{#foreach from=$_POST.slots|array_transpose key="i" item="slot"}}
		{{:assign line="%d+1"|math:$i}}
		{{:assign var="slot"
			day=$slot.day
			frequency=$slot.frequency
			open='%02d:%02d'|args:$slot.open_hour:$slot.open_minutes
			close='%02d:%02d'|args:$slot.close_hour:$slot.close_minutes
		}}
		{{:assign var="slots[%d]"|args:$i value=$slot}}

		{{if !"%s %s"|args:$slot.frequency:$slot.day|trim|strtotime}}
			{{:assign error="Ouvertures - ligne %d : le sélecteur de jour est invalide: %s %s"|args:$line:$slot.frequency:$slot.day}}
			{{:break}}
		{{elseif !$slot.open|regexp_match:'/^(2[0-3]|[01][0-9]):([0-5][0-9])$/'}}
			{{:assign error="Ouvertures - ligne %d: heure d'ouverture invalide."|args:$line}}
			{{:break}}











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{{:admin_header title="Configuration des ouvertures"}}

{{if $_POST.save}}
	{{#foreach from=$_POST.slots|array_transpose key="i" item="slot"}}
		{{:assign line="%d+1"|math:$i}}
		{{:assign var="slot"
			day=$slot.day
			frequency=$slot.frequency
			open='%02d:%02d'|args:$slot.open_hour:$slot.open_minutes
			close='%02d:%02d'|args:$slot.close_hour:$slot.close_minutes
		}}
		{{:assign var="slots.%d"|args:$i value=$slot}}

		{{if !"%s %s"|args:$slot.frequency:$slot.day|trim|strtotime}}
			{{:assign error="Ouvertures - ligne %d : le sélecteur de jour est invalide: %s %s"|args:$line:$slot.frequency:$slot.day}}
			{{:break}}
		{{elseif !$slot.open|regexp_match:'/^(2[0-3]|[01][0-9]):([0-5][0-9])$/'}}
			{{:assign error="Ouvertures - ligne %d: heure d'ouverture invalide."|args:$line}}
			{{:break}}

Modified src/skel-dist/modules/ouvertures/icon.svg from [a760737d4d] to [f809b11fbe].

1
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="48" height="48" id="img" stroke-width="0" ><path d="m13 2.03v.02 2c4.39.54 7.5 4.53 6.96 8.92-.46 3.64-3.32 6.53-6.96 6.96v2c5.5-.55 9.5-5.43 8.95-10.93-.45-4.75-4.22-8.5-8.95-8.97m-2 .03c-1.95.19-3.81.94-5.33 2.2l1.43 1.48c1.12-.9 2.47-1.48 3.9-1.68zm-6.74 3.61c-1.26 1.52-2.01 3.37-2.21 5.33h2c.19-1.42.75-2.77 1.64-3.9zm-2.2 7.33c.2 1.96.97 3.81 2.21 5.33l1.42-1.43c-.88-1.13-1.45-2.48-1.63-3.9zm5.04 5.37-1.43 1.37c1.51 1.26 3.37 2.05 5.33 2.26v-2c-1.42-.18-2.77-.75-3.9-1.63m5.4-11.37v5.25l4.5 2.67-.75 1.23-5.25-3.15v-6z"/></svg>
|
1
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none" ><path d="m13 2.03v.02 2c4.39.54 7.5 4.53 6.96 8.92-.46 3.64-3.32 6.53-6.96 6.96v2c5.5-.55 9.5-5.43 8.95-10.93-.45-4.75-4.22-8.5-8.95-8.97m-2 .03c-1.95.19-3.81.94-5.33 2.2l1.43 1.48c1.12-.9 2.47-1.48 3.9-1.68zm-6.74 3.61c-1.26 1.52-2.01 3.37-2.21 5.33h2c.19-1.42.75-2.77 1.64-3.9zm-2.2 7.33c.2 1.96.97 3.81 2.21 5.33l1.42-1.43c-.88-1.13-1.45-2.48-1.63-3.9zm5.04 5.37-1.43 1.37c1.51 1.26 3.37 2.05 5.33 2.26v-2c-1.42-.18-2.77-.75-3.9-1.63m5.4-11.37v5.25l4.5 2.67-.75 1.23-5.25-3.15v-6z"/></svg>

Modified src/skel-dist/modules/ouvertures/module.ini from [80628e2296] to [f15df458dc].

1
2
3
4


{
	"label": "Horaires d'ouverture",
	"description": "Permet d'afficher sur la page d'accueil les jours et horaires d'ouverture"
}


<
|
|
<
>
>

1
2

3
4

name="Horaires d'ouverture"
description="Permet d'afficher sur la page d'accueil les jours et horaires d'ouverture"

author="Paheko"
author_url="https://paheko.cloud/"

Modified src/skel-dist/modules/recu_don/icon.svg from [dd01515720] to [adc409220d].

1
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" id="img" stroke-width="0" viewBox="0 0 24 24"><path d="M17.726 13.02L14 16H9v-1h4.065c.399 0 .638-.445.416-.777l-.888-1.332C12.223 12.334 11.599 12 10.93 12H9 3c-.553 0-1 .447-1 1v6c0 1.104.896 2 2 2h9.639c.865 0 1.688-.373 2.258-1.024L22 13l-1.452-.484C19.583 12.194 18.521 12.384 17.726 13.02zM19.258 7.39c.451-.465.73-1.108.73-1.818s-.279-1.353-.73-1.818C18.807 3.288 18.183 3 17.494 3c0 0-1.244-.003-2.494 1.286C13.75 2.997 12.506 3 12.506 3c-.689 0-1.313.288-1.764.753-.451.466-.73 1.108-.73 1.818s.279 1.354.73 1.818L15 12 19.258 7.39z"/></svg>
|
1
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="img" stroke="none" viewBox="0 0 24 24"><path d="M17.726 13.02L14 16H9v-1h4.065c.399 0 .638-.445.416-.777l-.888-1.332C12.223 12.334 11.599 12 10.93 12H9 3c-.553 0-1 .447-1 1v6c0 1.104.896 2 2 2h9.639c.865 0 1.688-.373 2.258-1.024L22 13l-1.452-.484C19.583 12.194 18.521 12.384 17.726 13.02zM19.258 7.39c.451-.465.73-1.108.73-1.818s-.279-1.353-.73-1.818C18.807 3.288 18.183 3 17.494 3c0 0-1.244-.003-2.494 1.286C13.75 2.997 12.506 3 12.506 3c-.689 0-1.313.288-1.764.753-.451.466-.73 1.108-.73 1.818s.279 1.354.73 1.818L15 12 19.258 7.39z"/></svg>

Modified src/skel-dist/modules/recu_don/module.ini from [cd920ccb31] to [780f358803].

1
2
3
4


{
	"label": "Reçu de don",
	"description": "Reçu de don simple, sans valeur fiscale"
}


<
|
|
<
>
>

1
2

3
4

label="Reçu de don"
description="Reçu de don simple, sans valeur fiscale"

author="Paheko"
author_url="https://paheko.cloud/"

Deleted src/skel-dist/modules/recu_fiscal/snippets/home_button.html version [99f1ef488a].

1
2
3
{{#restrict section="accounting" level="read"}}
	{{:linkbutton label="Reçus fiscaux" href=$module.url icon="%sicon.svg"|args:$module.url}}
{{/restrict}}
<
<
<






Modified src/skel-dist/modules/recu_paiement/icon.svg from [c86a21c140] to [19540df506].

1
<svg stroke-width="0" width="48" height="48" id="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="m21 16h2v2h-2z"/><path d="m9 16h8v2h-8z"/><path d="m21 12h2v2h-2z"/><path d="m9 12h8v2h-8z"/><path d="m9 8h14v2h-14z"/><path d="m25 2h-18a2.002 2.002 0 0 0 -2 2v25a1 1 0 0 0 1 1h1a.9987.9987 0 0 0 .8-.4l2.2-2.933 2.2 2.933a1.0353 1.0353 0 0 0 1.6 0l2.2-2.933 2.2 2.933a1.0353 1.0353 0 0 0 1.6 0l2.2-2.933 2.2 2.933a.9993.9993 0 0 0 .8.4h1a1 1 0 0 0 1-1v-25a2.0023 2.0023 0 0 0 -2-2zm0 25.333-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933v-23.333h18z"/><path d="m0 0h32v32h-32z" fill="none"/></svg>
|
1
<svg stroke="none" width="100%" height="100%" id="img" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="m21 16h2v2h-2z"/><path d="m9 16h8v2h-8z"/><path d="m21 12h2v2h-2z"/><path d="m9 12h8v2h-8z"/><path d="m9 8h14v2h-14z"/><path d="m25 2h-18a2.002 2.002 0 0 0 -2 2v25a1 1 0 0 0 1 1h1a.9987.9987 0 0 0 .8-.4l2.2-2.933 2.2 2.933a1.0353 1.0353 0 0 0 1.6 0l2.2-2.933 2.2 2.933a1.0353 1.0353 0 0 0 1.6 0l2.2-2.933 2.2 2.933a.9993.9993 0 0 0 .8.4h1a1 1 0 0 0 1-1v-25a2.0023 2.0023 0 0 0 -2-2zm0 25.333-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933-2.2-2.933a1.0353 1.0353 0 0 0 -1.6 0l-2.2 2.933v-23.333h18z"/><path d="m0 0h32v32h-32z" fill="none"/></svg>

Modified src/skel-dist/modules/recu_paiement/module.ini from [972c67d830] to [9fd092515c].

1
2
3
4


{
	"label": "Reçu de paiement",
	"description": "Reçu de paiement, pour les écritures liées à un membre"
}


<
|
|
<
>
>

1
2

3
4

name="Reçu de paiement"
description="Reçu de paiement, pour les écritures liées à un membre"

author="Paheko"
author_url="https://paheko.cloud/"

Modified src/skel-dist/modules/recus_fiscaux/_config_default.tpl from [89b94cb59d] to [27055a1caf].

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




14
15
16
17
18
19
{{if !$module.config}}
	{{* Valeurs par défaut *}}
	{{:assign var="module[config]"
		objet_asso=""
		type_asso=""
		comptes_don="754"
		comptes_don_nature="75412"
		comptes_especes="530"
		comptes_cheques="5112"
		art200=false
		art238=false
		art978=false
	}}




	{{:assign var="module[config][champs_adresse]"
		0="adresse"
		1="code_postal"
		2="ville"
	}}
{{/if}}


|


<
<
<
<




>
>
>
>
|





1
2
3
4
5




6
7
8
9
10
11
12
13
14
15
16
17
18
19
{{if !$module.config}}
	{{* Valeurs par défaut *}}
	{{:assign var="module.config"
		objet_asso=""
		type_asso=""




		art200=false
		art238=false
		art978=false
	}}
	{{:assign var="module.config.comptes_don." value="754"}}
	{{:assign var="module.config.comptes_don_nature." value="75412"}}
	{{:assign var="module.config.comptes_especes." value="530"}}
	{{:assign var="module.config.comptes_cheques." value="5112"}}
	{{:assign var="module.config.champs_adresse"
		0="adresse"
		1="code_postal"
		2="ville"
	}}
{{/if}}

Name change from src/skel-dist/modules/recu_fiscal/_recu.html to src/skel-dist/modules/recus_fiscaux/_recu.html.

Name change from src/skel-dist/modules/recu_fiscal/annuler.html to src/skel-dist/modules/recus_fiscaux/annuler.html.

Name change from src/skel-dist/modules/recu_fiscal/config.html to src/skel-dist/modules/recus_fiscaux/config.html.

Modified src/skel-dist/modules/recus_fiscaux/icon.svg from [0cc688f24c] to [9f7d289eef].

1
<svg width="48" height="48" stroke-width="0" version="1.1" viewBox="0 0 67.73 67.73" xmlns="http://www.w3.org/2000/svg" id="img"><path transform="scale(.2646)" d="m186.1 13.22c-2.185 0.02461-4.384 0.169-6.592 0.4355-25.08 3.014-36.49 18.45-52.77 36.29-19.54-19.99-32.16-35.68-62.16-36.1-33.58-0.4489-62.77 24.48-61.45 63.38 1.89 55.53 59.24 100.4 94.4 136l29.23 29.58 35.37-36.89c12.97-13.53 31.49-31.38 48.22-49.59 15.25-16.59 29.03-33.49 35.8-47.71 22.67-47.57-15.62-95.86-60.04-95.36zm-48.95 61.32c7.899 0 14.76 1.435 20.59 4.303l-4.16 19.54c-3.997-3.997-9.828-5.996-17.49-5.996-7.664 1e-6 -13.87 2.751-18.62 8.252-2.68 3.103-4.584 6.982-5.713 11.64h38.79l-2.258 10.93h-37.94c-0.04702 1.081-0.07032 2.467-0.07032 4.16 0 1.646 0.04659 3.362 0.1406 5.148h35.9l-2.256 10.93h-32.16c1.175 5.031 2.987 8.981 5.432 11.85 4.702 5.548 10.81 8.322 18.34 8.322 9.028 0 16.22-2.774 21.58-8.322v21.58c-6.112 3.056-13.23 4.586-21.37 4.586-13.73 0-25.01-4.702-33.85-14.11-6.018-6.395-9.992-14.37-11.92-23.91h-10.44l2.258-10.93h6.91c-0.04702-1.128-0.07031-2.328-0.07031-3.598 0-2.163 0.04659-4.065 0.1406-5.711h-9.238l2.258-10.93h8.393c1.975-9.357 5.9-17.18 11.78-23.48 8.887-9.498 20.57-14.25 35.05-14.25z" stroke-width="2.084"/></svg>
|
1
<svg width="100%" height="100%" stroke="none" version="1.1" viewBox="0 0 67.73 67.73" xmlns="http://www.w3.org/2000/svg" id="img"><path transform="scale(.2646)" d="m186.1 13.22c-2.185 0.02461-4.384 0.169-6.592 0.4355-25.08 3.014-36.49 18.45-52.77 36.29-19.54-19.99-32.16-35.68-62.16-36.1-33.58-0.4489-62.77 24.48-61.45 63.38 1.89 55.53 59.24 100.4 94.4 136l29.23 29.58 35.37-36.89c12.97-13.53 31.49-31.38 48.22-49.59 15.25-16.59 29.03-33.49 35.8-47.71 22.67-47.57-15.62-95.86-60.04-95.36zm-48.95 61.32c7.899 0 14.76 1.435 20.59 4.303l-4.16 19.54c-3.997-3.997-9.828-5.996-17.49-5.996-7.664 1e-6 -13.87 2.751-18.62 8.252-2.68 3.103-4.584 6.982-5.713 11.64h38.79l-2.258 10.93h-37.94c-0.04702 1.081-0.07032 2.467-0.07032 4.16 0 1.646 0.04659 3.362 0.1406 5.148h35.9l-2.256 10.93h-32.16c1.175 5.031 2.987 8.981 5.432 11.85 4.702 5.548 10.81 8.322 18.34 8.322 9.028 0 16.22-2.774 21.58-8.322v21.58c-6.112 3.056-13.23 4.586-21.37 4.586-13.73 0-25.01-4.702-33.85-14.11-6.018-6.395-9.992-14.37-11.92-23.91h-10.44l2.258-10.93h6.91c-0.04702-1.128-0.07031-2.328-0.07031-3.598 0-2.163 0.04659-4.065 0.1406-5.711h-9.238l2.258-10.93h8.393c1.975-9.357 5.9-17.18 11.78-23.48 8.887-9.498 20.57-14.25 35.05-14.25z" stroke-width="2.084"/></svg>

Modified src/skel-dist/modules/recus_fiscaux/index.html from [906e591f56] to [07a652d3c3].

8
9
10
11
12
13
14
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
<nav class="tabs">
	{{#restrict section="accounting" level="write"}}
	<aside>
		{{#restrict section="accounting" level="admin"}}
			{{:linkbutton href="config.html" label="Configuration" shape="settings" target="_dialog"}}
		{{/restrict}}
		{{*{{:linkbutton href="generer.html" label="Générer des reçus" shape="check"}}*}}
		{{:linkbutton href="nouveau.html?type=vierge" label="Nouveau reçu" shape="plus"}}
	</aside>
	{{/restrict}}

	<ul>
		<li class="current"><a href="./">Liste des reçus</a></li>
		<li><a href="./recap.html">Récapitulatif annuel pour déclaration</a></li>
	</ul>
</nav>

<div class="shortForms">
	<form method="get" action="">
		<fieldset>
			<legend>Filtrer par année</legend>
			<p>
				{{:assign var="years[]" value="— Voir toutes les années —"}}

				{{#load select="SUBSTR($$.date, 1, 4) AS year" group="SUBSTR($$.date, 1, 4)"}}
					{{:assign var="years[%d]"|args:$year value=$year}}
				{{/load}}

				{{:input type="select" name="year" options=$years default=$_GET.year onchange="this.form.submit();"}}
			</p>
		</fieldset>
	</form>








|














|


|







8
9
10
11
12
13
14
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
<nav class="tabs">
	{{#restrict section="accounting" level="write"}}
	<aside>
		{{#restrict section="accounting" level="admin"}}
			{{:linkbutton href="config.html" label="Configuration" shape="settings" target="_dialog"}}
		{{/restrict}}
		{{*{{:linkbutton href="generer.html" label="Générer des reçus" shape="check"}}*}}
		{{:linkbutton href="nouveau.html" label="Nouveau reçu" shape="plus"}}
	</aside>
	{{/restrict}}

	<ul>
		<li class="current"><a href="./">Liste des reçus</a></li>
		<li><a href="./recap.html">Récapitulatif annuel pour déclaration</a></li>
	</ul>
</nav>

<div class="shortForms">
	<form method="get" action="">
		<fieldset>
			<legend>Filtrer par année</legend>
			<p>
				{{:assign var="years." value="— Voir toutes les années —"}}

				{{#load select="SUBSTR($$.date, 1, 4) AS year" group="SUBSTR($$.date, 1, 4)"}}
					{{:assign var="years.%d"|args:$year value=$year}}
				{{/load}}

				{{:input type="select" name="year" options=$years default=$_GET.year onchange="this.form.submit();"}}
			</p>
		</fieldset>
	</form>

Modified src/skel-dist/modules/recus_fiscaux/module.ini from [4a0b870aaa] to [d8663c68d8].

1
2
3
4





{
	"label": "Reçus fiscaux",
	"description": "Permet de générer des reçus fiscaux. Conforme aux exigences fiscales de 2022. Seuls les membres ayant accès à la comptabilité auront accès à ce module."
}





<
|
|
<
>
>
>
>
>

1
2

3
4
5
6
7

name="Reçus fiscaux"
description="Permet de générer des reçus fiscaux. Conforme aux exigences fiscales de 2022."

author="Paheko"
author_url="https://paheko.cloud/"
home_button=true
restrict_section="accounting"
restrict_level="read"

Modified src/skel-dist/modules/recus_fiscaux/nouveau.html from [f05920986a] to [b88823b0b8].

1
2
3








4
5
6
7
8
9
10
{{#restrict block=true section="accounting" level="write"}}
{{/restrict}}
{{:include file="./_config_default.tpl" keep="module"}}









{{if $_POST}}
	{{if !$_POST.date|trim|parse_date}}
		{{:assign error="Date d'émission invalide ou vide."}}
	{{elseif !$_POST.nom|trim}}
		{{:assign error="Le nom du donateur ne peut être laissé vide."}}
	{{elseif !$_POST.adresse|trim}}



>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{{#restrict block=true section="accounting" level="write"}}
{{/restrict}}
{{:include file="./_config_default.tpl" keep="module"}}

{{if $_GET.user}}
	{{#foreach from=$_GET.user key="id" item="user"}}
		{{:assign id_user=$id|intval}}
	{{/foreach}}
{{elseif $_GET.id_user}}
	{{:assign id_user=$_GET.id_user|intval}}
{{/if}}

{{if $_POST}}
	{{if !$_POST.date|trim|parse_date}}
		{{:assign error="Date d'émission invalide ou vide."}}
	{{elseif !$_POST.nom|trim}}
		{{:assign error="Le nom du donateur ne peut être laissé vide."}}
	{{elseif !$_POST.adresse|trim}}
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
			linked_user=$_POST.id_user
			linked_transactions=$_POST.transactions
			annule=false
			recu=$recu
		}}
		{{:http redirect="voir.html?id=%d"|args:$new_id}}
	{{/if}}




{{elseif $_GET.id_user}}
	{{:assign var="champs_adresse" value=$module.config.champs_adresse|quote_sql_identifier|implode:" || ' — ' || "}}

	{{#users id=$_GET.id_user select="%s AS _adresse"|args:$champs_adresse}}
		{{:assign .="user"}}
	{{else}}
		{{:error message="Ce membre n'existe pas."}}
	{{/users}}













	{{:assign data.montant=0}}







	{{#sql select="SUM(l.credit) AS somme" tables=""}}
		{{:assign data.montant="%d+%d"|math:$data.montant:$somme}}


	{{/sql}}
{{elseif $_GET.id_transaction}}
{{/if}}

{{:include file="./_config_default.tpl"}}

{{:admin_header title="Créer un nouveau reçu fiscal" current="acc"}}

<nav class="tabs">
	{{:linkbutton href="./" label="Retour à la liste des reçus" shape="left"}}
</nav>

{{if $error}}
	<p class="error block">{{$error}}</p>
{{/if}}







>
>
>
>
|


|





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

>
>
>
>
|
<
>
>
|



<
<
<
<







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
			linked_user=$_POST.id_user
			linked_transactions=$_POST.transactions
			annule=false
			recu=$recu
		}}
		{{:http redirect="voir.html?id=%d"|args:$new_id}}
	{{/if}}
{{/if}}

{{:admin_header title="Créer un nouveau reçu fiscal" current="acc"}}

{{if $id_user}}
	{{:assign var="champs_adresse" value=$module.config.champs_adresse|quote_sql_identifier|implode:" || ' — ' || "}}

	{{#users id=$id_user select="%s AS _adresse"|args:$champs_adresse}}
		{{:assign .="user"}}
	{{else}}
		{{:error message="Ce membre n'existe pas."}}
	{{/users}}

	{{* Récupération des comptes et soldes *}}
	{{#select
		SUM(l.credit) AS total, strftime('%Y', t.date) AS year, a.code AS account
		FROM acc_transactions t
		INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
		INNER JOIN acc_transactions_users tu ON tu.id_transaction = t.id
		INNER JOIN acc_accounts a ON a.id = l.id_account AND a.!code_don
		WHERE tu.id_user = {$id_user}
		GROUP BY strftime('%Y', t.date), a.code;
		!code_don="code"|sql_where:"IN":$module.config.comptes_don
	}}
		{{:assign var="year_total" from="user_years.%d.total"|args:$year}}
		{{:assign .="user_years.%d"|args:$year}}
		{{:assign var="user_years.%d.total"|args:$year value="%d+%d"|math:$total:$year_total}}
	{{/select}}

	{{#foreach from=$user_years item="year"}}
		{{:assign total_money=$year.total|money_currency}}
		{{:assign var="user_select.%d"|args:$year.year value="%d (%s)"|args:$year.year:$total_money}}
	{{/foreach}}


	<script type="text/javascript">
	var user_years = {{$user_years|json_encode}};
	</script>
{{elseif $_GET.id_transaction}}
{{/if}}





<nav class="tabs">
	{{:linkbutton href="./" label="Retour à la liste des reçus" shape="left"}}
</nav>

{{if $error}}
	<p class="error block">{{$error}}</p>
{{/if}}
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
				{{:input type="radio-btn" name="type" value="transaction" label="Créer un reçu à partir d'une écriture"}}
				{{:input type="radio-btn" name="type" value="vierge" label="Créer un reçu vierge"}}
			</dl>
		</fieldset>
		<fieldset class="type-user hidden">
			<legend>Reçu pour un membre</legend>
			<dl>
				{{:input type="list" multiple=false name="id_user" label="Sélectionner un membre" target="!users/selector.php" required=true}}
			</dl>
		</fieldset>
		<fieldset class="type-transaction hidden">
			<legend>Reçu pour une écriture</legend>
			<dl>
				{{:input type="number" name="id_transaction" label="Indiquer le numéro de l'écriture" min=0 required=true}}
			</dl>







|







96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
				{{:input type="radio-btn" name="type" value="transaction" label="Créer un reçu à partir d'une écriture"}}
				{{:input type="radio-btn" name="type" value="vierge" label="Créer un reçu vierge"}}
			</dl>
		</fieldset>
		<fieldset class="type-user hidden">
			<legend>Reçu pour un membre</legend>
			<dl>
				{{:input type="list" multiple=false name="user" label="Sélectionner un membre" target="!users/selector.php" required=true}}
			</dl>
		</fieldset>
		<fieldset class="type-transaction hidden">
			<legend>Reçu pour une écriture</legend>
			<dl>
				{{:input type="number" name="id_transaction" label="Indiquer le numéro de l'écriture" min=0 required=true}}
			</dl>
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
		var type = $('[name="type"]:checked')[0].value;

		g.toggle('.type-user', type == 'user');
		g.toggle('.type-transaction', type == 'transaction');
		g.toggle('.submit', true);
	}
	</script>
{{elseif $_POST.preview}}
	<form method="post" action="">

{{else}}
	<form method="post" action="">
		<fieldset>
			<legend>Nouveau reçu</legend>
			<dl>
				{{:input type="text" name="nom" label="Nom du bénéficiaire" required=true default=$user._name}}
				{{:input type="textarea" cols="50" rows="3" name="adresse" label="Adresse du bénéficiaire" required=true default=$user._adresse}}
				{{if $user_years}}
					{{:input type="select" name="annees" label="Pour quelle année le reçu doit-il être généré ?" options=$user_years}}
				{{/if}}

				{{:input type="date" name="date" required=true label="Date du reçu" default=$now}}
				{{:input type="money" name="montant" required=true label="Montant des dons"}}

				<dt>Type de don</dt>
				{{:input type="radio" name="type" value="numeraire" label="Don en numéraire (en euros)"}}
				{{:input type="radio" name="type" value="nature" label="Don en nature" help="Par exemple abandon de frais par les bénévoles"}}
			</dl>
			<dl class="hidden type-numeraire">
				<dt>Moyens de paiement</dt>
				{{:input type="checkbox" name="moyens[especes]" value=1 label="Paiement en espèces"}}
				{{:input type="checkbox" name="moyens[cheques]" value=1 label="Paiement en chèques"}}
				{{:input type="checkbox" name="moyens[autres]" value=1 label="Paiement par virement, prélèvement, carte bancaire, ou autre"}}







<
<








|
|



|


|







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
		var type = $('[name="type"]:checked')[0].value;

		g.toggle('.type-user', type == 'user');
		g.toggle('.type-transaction', type == 'transaction');
		g.toggle('.submit', true);
	}
	</script>



{{else}}
	<form method="post" action="">
		<fieldset>
			<legend>Nouveau reçu</legend>
			<dl>
				{{:input type="text" name="nom" label="Nom du bénéficiaire" required=true default=$user._name}}
				{{:input type="textarea" cols="50" rows="3" name="adresse" label="Adresse du bénéficiaire" required=true default=$user._adresse}}
				{{if $user_select}}
					{{:input type="select" name="annees" label="Pour quelle année le reçu doit-il être généré ?" required=true options=$user_select}}
				{{/if}}

				{{:input type="date" name="date" required=true label="Date du reçu" default=$now}}
				{{:input type="money" name="montant" required=true label="Montant des dons" default=$user.total}}

				<dt>Type de don</dt>
				{{:input type="radio" name="type" value="numeraire" label="Don en numéraire (en euros)" default=$user.}}
				{{:input type="radio" name="type" value="nature" label="Don en nature" help="Par exemple abandon de frais par les bénévoles"}}
			</dl>
			<dl class="hidden type-numeraire">
				<dt>Moyens de paiement</dt>
				{{:input type="checkbox" name="moyens[especes]" value=1 label="Paiement en espèces"}}
				{{:input type="checkbox" name="moyens[cheques]" value=1 label="Paiement en chèques"}}
				{{:input type="checkbox" name="moyens[autres]" value=1 label="Paiement par virement, prélèvement, carte bancaire, ou autre"}}

Name change from src/skel-dist/modules/recu_fiscal/previsualiser.html to src/skel-dist/modules/recus_fiscaux/previsualiser.html.

Name change from src/skel-dist/modules/recu_fiscal/recap.html to src/skel-dist/modules/recus_fiscaux/recap.html.

Name change from src/skel-dist/modules/recu_fiscal/recu.schema.json to src/skel-dist/modules/recus_fiscaux/recu.schema.json.

Name change from src/skel-dist/modules/recu_fiscal/snippets/transaction_details.html to src/skel-dist/modules/recus_fiscaux/snippets/transaction_details.html.

Name change from src/skel-dist/modules/recu_fiscal/voir.html to src/skel-dist/modules/recus_fiscaux/voir.html.

Modified src/skel-dist/modules/remise_cheques/config.html from [8f62fcf985] to [83861a729a].

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<form method="post" action="">

<fieldset>
	<legend>Configuration</legend>
	<dl>
		{{:input required=true name="accounts" type="text" label="Numéros de comptes liés aux remises de chèques" source=$module.config default="5112"}}
		<dd class="help">
			Pour chaque numéro de compte indiqué dans ce champ, le reçu de don sera proposé (en dessous de la fiche de l'écriture).<br />
			Séparer les numéros de compte avec des virgules, par exemple : <tt>754, 756</tt>.<br />
			Laisser vide pour que le reçu de don soit proposé quel que soit le compte.
		</dd>
	</dl>
</fieldset>

<p class="submit">
	{{:button type="submit" name="save" label="Enregistrer" shape="right" class="main"}}
</p>

</form>

{{:admin_footer}}







|
|
<











19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35
36
37
38
<form method="post" action="">

<fieldset>
	<legend>Configuration</legend>
	<dl>
		{{:input required=true name="accounts" type="text" label="Numéros de comptes liés aux remises de chèques" source=$module.config default="5112"}}
		<dd class="help">
			Pour chaque numéro de compte indiqué dans ce champ, le formulaire de remise de chèque sera proposé (en dessous de la fiche de l'écriture).<br />
			Séparer les numéros de compte avec des virgules, par exemple : <tt>754, 756</tt>.

		</dd>
	</dl>
</fieldset>

<p class="submit">
	{{:button type="submit" name="save" label="Enregistrer" shape="right" class="main"}}
</p>

</form>

{{:admin_footer}}

Modified src/skel-dist/modules/remise_cheques/icon.svg from [9fd2725ba3] to [79a2654d42].

1
<svg width="48" height="48" id="img"  viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g><path d="m90.24 103.7h-90.24v-58.96h89.62v4h-85.62v50.96h86.24z"/><path d="m128 103.7h-25.5v-4h21.5v-50.96h-21.19v-4h25.19z"/><path d="m117.9 82.56h-6.966v-4h2.966v-7.414h-2.966v-4h6.966z"/><path d="m83.65 82.56h-73.59v-15.41h73.59v4h-69.59v7.414h69.59z"/><path d="m9.541 55.72h24.77v4h-24.77z"/><path d="m111 89.12h7.481v4h-7.481z"/><path d="m60.58 89.12h23.06v4h-23.06z"/><path d="m9.541 89.12h7.606v4h-7.606z"/><path d="m103.9 113h-4v-107h-7.256v107h-4v-111h15.26z"/><path d="m91.43 68.25h10.91v4h-10.91z"/><path d="m91.43 60.96h10.91v4h-10.91z"/><path d="m83.85 38.82h-4v-28.64h23.89v4h-19.89z"/><path d="m100.5 126.1h-8.525l-3.363-10.24v-4.713h15.26v4.713zm-5.629-4h2.734l2.262-6.951h-7.256v0.072z"/></g></svg>
|
1
<svg width="100%" height="100%" id="img"  viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g><path d="m90.24 103.7h-90.24v-58.96h89.62v4h-85.62v50.96h86.24z"/><path d="m128 103.7h-25.5v-4h21.5v-50.96h-21.19v-4h25.19z"/><path d="m117.9 82.56h-6.966v-4h2.966v-7.414h-2.966v-4h6.966z"/><path d="m83.65 82.56h-73.59v-15.41h73.59v4h-69.59v7.414h69.59z"/><path d="m9.541 55.72h24.77v4h-24.77z"/><path d="m111 89.12h7.481v4h-7.481z"/><path d="m60.58 89.12h23.06v4h-23.06z"/><path d="m9.541 89.12h7.606v4h-7.606z"/><path d="m103.9 113h-4v-107h-7.256v107h-4v-111h15.26z"/><path d="m91.43 68.25h10.91v4h-10.91z"/><path d="m91.43 60.96h10.91v4h-10.91z"/><path d="m83.85 38.82h-4v-28.64h23.89v4h-19.89z"/><path d="m100.5 126.1h-8.525l-3.363-10.24v-4.713h15.26v4.713zm-5.629-4h2.734l2.262-6.951h-7.256v0.072z"/></g></svg>

Modified src/skel-dist/modules/remise_cheques/module.ini from [a93b07bbd2] to [3f61fc17c1].

1
2
3
4





{
	"label": "Bordereau de remise de chèques",
	"description": "Permet d'imprimer un bordereau de remise de chèques à partir d'une écriture de dépôt."
}





<
|
|
<
>
>
>
>
>

1
2

3
4
5
6
7

name="Bordereau de remise de chèques"
description="Permet d'imprimer un bordereau de remise de chèques à partir d'une écriture de dépôt."

author="Paheko"
author_url="https://paheko.cloud/"
home_button=true
restrict_section="accounting"
restrict_level="read"

Modified src/skel-dist/modules/remise_cheques/snippets/transaction_details.html from [7ec3e581b2] to [2b7a9269fc].

1
2
3
4
5
6
7
{{if $module.config.accounts === null}}
	{{:assign module.config.accounts='5112'}}
{{/if}}

{{* FIXME: ne proposer le bordereau que pour les écritures de dépot *}}

{{:include file="modules/recu_don/snippets/transaction_details.html"}}

|





1
2
3
4
5
6
7
{{if $module.config.accounts === null}}
	{{:assign var="module.config.accounts[]" value='5112'}}
{{/if}}

{{* FIXME: ne proposer le bordereau que pour les écritures de dépot *}}

{{:include file="modules/recu_don/snippets/transaction_details.html"}}

Modified src/templates/_head.tpl from [634abc5df3] to [0fa72dd81e].

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
		{/foreach}
	{/if}
	<link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" />
	<link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" />
	<link rel="manifest" href="{$admin_url}manifest.php" />
	{if isset($config)}
		<link rel="icon" type="image/png" href="{$config->fileURL('favicon')}" />
		{custom_colors config=$config}
	{/if}

</head>

<body{if !empty($layout)} class="{$layout}"{/if}>

{if !array_key_exists('_dialog', $_GET) && empty($layout)}
<header class="header">
	<nav class="menu">
		{if isset($config)}
		<figure class="logo">
		{if $url = $config->fileURL('logo', '150px')}
			<a href="{$admin_url}"><img src="{$url}" alt="" /></a>
		{/if}
		</figure>
		{/if}
	<ul>
	{if $is_logged}
	<?php
	$current_parent = substr($current, 0, strpos($current, '/'));
	?>
		<li class="home{if $current == 'home'} current{elseif $current_parent == 'home'} current_parent{/if}"><h3><a href="{$admin_url}">{icon shape="home"}<b>Accueil</b></a></h3>
			{if !empty($plugins_menu)}







<

>







<

|
|


<







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
		{/foreach}
	{/if}
	<link rel="stylesheet" type="text/css" href="{$admin_url}static/print.css?{$version_hash}" media="print" />
	<link rel="stylesheet" type="text/css" href="{$admin_url}static/handheld.css?{$version_hash}" media="handheld,screen and (max-width:981px)" />
	<link rel="manifest" href="{$admin_url}manifest.php" />
	{if isset($config)}
		<link rel="icon" type="image/png" href="{$config->fileURL('favicon')}" />

	{/if}
	{custom_colors config=$config}
</head>

<body{if !empty($layout)} class="{$layout}"{/if}>

{if !array_key_exists('_dialog', $_GET) && empty($layout)}
<header class="header">
	<nav class="menu">

		<figure class="logo">
		{if isset($config) && ($url = $config->fileURL('logo', '150px'))}
				<a href="{$admin_url}"><img src="{$url}" alt="" /></a>
		{/if}
		</figure>

	<ul>
	{if $is_logged}
	<?php
	$current_parent = substr($current, 0, strpos($current, '/'));
	?>
		<li class="home{if $current == 'home'} current{elseif $current_parent == 'home'} current_parent{/if}"><h3><a href="{$admin_url}">{icon shape="home"}<b>Accueil</b></a></h3>
			{if !empty($plugins_menu)}

Modified src/templates/acc/accounts/deposit.tpl from [db6e5c4575] to [5717951cb8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
{include file="_head.tpl" title="Dépôt en banque : %s — %s"|args:$account.code,$account.label current="acc/accounts"}

{form_errors}

{if $missing_balance > 0}
<p class="alert block">
	Il existe une différence de {$missing_balance|raw|money_currency} entre la liste des écritures à déposer
	et le solde du compte.<br />
	Cette situation est généralement dûe à des écritures de dépôt qui ont été supprimées.<br />
	{linkbutton shape="plus" label="Faire un virement pour régulariser" href="!acc/transactions/new.php?a0=%d&l=Régularisation%%20dépôt&account=%d"|args:$missing_balance,$account.id}
</p>
{/if}

{if !$journal_count}
	<p class="alert block">Il n'y a aucune écriture qui nécessiterait un dépôt.
	</p>
{else}
	<p class="help">
		Cocher les cases correspondant aux montants à déposer, une nouvelle écriture sera générée.
	</p>

	<form method="post" action="{$self_url}" data-focus="1">
		<table class="list">
			<thead>
				<tr>
					<td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td>
					<td></td>
					<td>Date</td>
					<td>Réf. écriture</td>
					<td>Réf. ligne</td>
					<th>Libellé</th>
					<td class="money">Montant</td>
					<td class="money">Solde cumulé</td>
				</tr>
			</thead>
			<tbody>
				{foreach from=$journal item="line"}
				{if isset($line.sum)}
				<tr>
					<td colspan="5"></td>
					<td class="money">{if $line.sum > 0}-{/if}{$line.sum|abs|raw|money:false}</td>
					<th>Solde au {$line.date|date_short}</th>
					<td colspan="2"></td>
				</tr>
				{else}
				<tr>
					<td class="check">
						{input type="checkbox" name="deposit[%d]"|args:$line.id_line value="1" data-debit=$line.debit|abs data-credit=$line.credit default=$line.checked}
					</td>
					<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
					<td>{$line.date|date_short}</td>
					<td>{$line.reference}</td>
					<td>{$line.line_reference}</td>
					<th>{$line.label}</th>
					<td class="money">{$line.debit|raw|money}</td>
					<td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|money:false}</td>
				</tr>
				{/if}
			{/foreach}
			</tbody>
		</table>

		<fieldset>
			<legend>Détails de l'écriture de dépôt</legend>
			<dl>









|



|








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












<







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

23
24











25








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

38
39
40
41
42
43
44
{include file="_head.tpl" title="Dépôt en banque : %s — %s"|args:$account.code,$account.label current="acc/accounts"}

{form_errors}

{if $missing_balance > 0}
<p class="alert block">
	Il existe une différence de {$missing_balance|raw|money_currency} entre la liste des écritures à déposer
	et le solde du compte.<br />
	Cette situation est généralement dûe à des écritures de dépôt qui ont été supprimées.<br />
	{linkbutton shape="plus" label="Faire un virement pour régulariser" href="!acc/transactions/new.php?0=%d&l=Régularisation%%20dépôt&account=%d"|args:$missing_balance,$account.id}
</p>
{/if}

{if !$journal->count()}
	<p class="alert block">Il n'y a aucune écriture qui nécessiterait un dépôt.
	</p>
{else}
	<p class="help">
		Cocher les cases correspondant aux montants à déposer, une nouvelle écriture sera générée.
	</p>

	<form method="post" action="{$self_url}" data-focus="1">

		{include file="common/dynamic_list_head.tpl" check=true list=$journal}












		{foreach from=$journal->iterate() item="line"}








				<tr>
					<td class="check">
						{input type="checkbox" name="deposit[%d]"|args:$line.id_line value="1" data-debit=$line.debit|abs data-credit=$line.credit default=$line.checked}
					</td>
					<td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td>
					<td>{$line.date|date_short}</td>
					<td>{$line.reference}</td>
					<td>{$line.line_reference}</td>
					<th>{$line.label}</th>
					<td class="money">{$line.debit|raw|money}</td>
					<td class="money">{if $line.running_sum > 0}-{/if}{$line.running_sum|abs|raw|money:false}</td>
				</tr>

			{/foreach}
			</tbody>
		</table>

		<fieldset>
			<legend>Détails de l'écriture de dépôt</legend>
			<dl>

Modified src/templates/acc/accounts/index.tpl from [a9042469ec] to [90fb7ee1d9].

9
10
11
12
13
14
15




16
17
18
19
20
21
22


{if isset($_GET['chart_change'])}
<p class="block error">
	L'exercice sélectionné utilise un plan comptable différent, merci de sélectionner un autre compte.
</p>
{/if}





{if !empty($grouped_accounts)}
	<table class="list">
		<thead>
			<tr>
				<td></td>
				<td class="num">Numéro</td>







>
>
>
>







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


{if isset($_GET['chart_change'])}
<p class="block error">
	L'exercice sélectionné utilise un plan comptable différent, merci de sélectionner un autre compte.
</p>
{/if}

{if $pending_count}
	{include file="acc/transactions/_pending_message.tpl"}
{/if}

{if !empty($grouped_accounts)}
	<table class="list">
		<thead>
			<tr>
				<td></td>
				<td class="num">Numéro</td>

Modified src/templates/acc/accounts/simple.tpl from [b7a0a4a36b] to [c912366262].

11
12
13
14
15
16
17




18
19
20
21
22
23
24
	</aside>
	<ul>
		{foreach from=$types key="key" item="label"}
		<li{if $type == $key} class="current"{/if}><a href="?type={$key}">{$label}</a></li>
		{/foreach}
	</ul>
</nav>





{if !$list->count()}
	<p class="alert block">
		Aucune écriture à afficher.
	</p>
{else}
	<form method="post" action="{$admin_url}acc/transactions/actions.php">







>
>
>
>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	</aside>
	<ul>
		{foreach from=$types key="key" item="label"}
		<li{if $type == $key} class="current"{/if}><a href="?type={$key}">{$label}</a></li>
		{/foreach}
	</ul>
</nav>

{if $pending_count}
	{include file="acc/transactions/_pending_message.tpl"}
{/if}

{if !$list->count()}
	<p class="alert block">
		Aucune écriture à afficher.
	</p>
{else}
	<form method="post" action="{$admin_url}acc/transactions/actions.php">

Modified src/templates/acc/projects/index.tpl from [3427ad1e47] to [e53c25b3a2].

13
14
15
16
17
18
19

20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
			{linkbutton href="?by_year=1" label="Grouper par exercice" shape="right"}
		{/if}
	</p>


	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>

		{linkbutton shape="download" href="%s?by_year=%d&_pdf"|args:$self_url_no_qs,$by_year label="Télécharger en PDF"}

	</p>
{/if}
</div>

{if $projects_count}


	<table class="list projects">
		<thead>
			<tr>
				<td>Année</td>
				<td></td>
				<td class="money">Charges</td>
				<td class="money">Produits</td>
				<td class="money">Débits</td>
				<td class="money">Crédits</td>
				<td class="money">Solde</td>
			</tr>







>
|
>










|







13
14
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
			{linkbutton href="?by_year=1" label="Grouper par exercice" shape="right"}
		{/if}
	</p>


	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>
		{if PDF_COMMAND}
			{linkbutton shape="download" href="%s?by_year=%d&_pdf"|args:$self_url_no_qs,$by_year label="Télécharger en PDF"}
		{/if}
	</p>
{/if}
</div>

{if $projects_count}


	<table class="list projects">
		<thead>
			<tr>
				<td>Projet</td>
				<td></td>
				<td class="money">Charges</td>
				<td class="money">Produits</td>
				<td class="money">Débits</td>
				<td class="money">Crédits</td>
				<td class="money">Solde</td>
			</tr>

Modified src/templates/acc/reports/_header.tpl from [e558c75a8e] to [8250056d24].

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
					<input type="submit" value="Annuler" onclick="this.form.querySelectorAll('input:not([type=hidden]), select').forEach((a) => a.disabled = true); this.form.submit();" />
				</p>
			</fieldset>
		</form>
		{/if}
	</div>
	{/if}





	<h2>{$config.org_name} — {$title}</h2>
	{if isset($project)}
		<h3>Projet&nbsp;: {if $project.code}{$project.code} — {/if}{$project.label}{if $project.archived} <em>(archivé)</em>{/if}</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}<br />
			<em>Document généré le {$close_date|date_short}</em>
		</p>
	{/if}

	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>
		{if $current != 'graphs'}
		{linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"}
		{/if}
	</p>
</div>

	{if !empty($allow_filter) && isset($year) && $criterias.before && $criterias.after}
		<p class="alert block">
			Attention, seules les écritures du {$criterias.after|date_short} au {$criterias.before|date_short} sont prises en compte.
		</p>
	{/if}







>
>
>
>






|


|





|










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
					<input type="submit" value="Annuler" onclick="this.form.querySelectorAll('input:not([type=hidden]), select').forEach((a) => a.disabled = true); this.form.submit();" />
				</p>
			</fieldset>
		</form>
		{/if}
	</div>
	{/if}

	{if $config.files.logo}
	<figure class="logo print-only"><img src="{$config->fileURL('logo', '150px')}" alt="" /></figure>
	{/if}

	<h2>{$config.org_name} — {$title}</h2>
	{if isset($project)}
		<h3>Projet&nbsp;: {if $project.code}{$project.code} — {/if}{$project.label}{if $project.archived} <em>(archivé)</em>{/if}</h3>
	{/if}
	{if isset($year)}
		<p>Exercice&nbsp;: {$year.label} ({if $year.closed}clôturé{else}<strong>en cours</strong>{/if})
			— du {$year.start_date|date_short}
			— au {$year.end_date|date_short}<br />
			<em>Document généré le {$now|date_short}</em>
		</p>
	{/if}

	<p class="noprint print-btn">
		<button onclick="window.print(); return false;" class="icn-btn" data-icon="⎙">Imprimer</button>
		{if $current != 'graphs' && PDF_COMMAND}
		{linkbutton shape="download" href="%s&_pdf"|args:$self_url label="Télécharger en PDF"}
		{/if}
	</p>
</div>

	{if !empty($allow_filter) && isset($year) && $criterias.before && $criterias.after}
		<p class="alert block">
			Attention, seules les écritures du {$criterias.after|date_short} au {$criterias.before|date_short} sont prises en compte.
		</p>
	{/if}

Added src/templates/acc/transactions/_pending_message.tpl version [9d2d58c5c3].

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
<p class="alert block">
	{
		{Il y a une dette ou créance à régler dans les exercices clos.}
		{Il y a %n dettes ou créances à régler dans les exercices clos.}
		n=$pending_count
	}<br />
	{linkbutton href="!acc/transactions/pending.php" label="Voir les dettes et créances en attente" shape="menu"}
</p>

Modified src/templates/acc/transactions/details.tpl from [d7af5f3327] to [9f94121eb9].

11
12
13
14
15
16
17

18
19
20
21
22
23

24
25
26
27
28
29
30
{if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction_year->closed}
	{linkbutton href="edit.php?id=%d"|args:$transaction.id shape="edit" label="Modifier cette écriture"}
	{linkbutton href="delete.php?id=%d"|args:$transaction.id shape="delete" label="Supprimer cette écriture"}
{/if}
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
	{linkbutton href="new.php?copy=%d"|args:$transaction.id shape="plus" label="Dupliquer cette écriture"}
{/if}

	<aside>
		{if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
			{linkbutton href="lock.php?id=%d"|args:$transaction.id shape="lock" label="Verrouiller" target="_dialog"}
		{/if}
		{linkbutton href="?id=%d&_pdf"|args:$transaction.id shape="download" label="Télécharger en PDF"}
	</aside>

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







>






>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$transaction_year->closed}
	{linkbutton href="edit.php?id=%d"|args:$transaction.id shape="edit" label="Modifier cette écriture"}
	{linkbutton href="delete.php?id=%d"|args:$transaction.id shape="delete" label="Supprimer cette écriture"}
{/if}
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
	{linkbutton href="new.php?copy=%d"|args:$transaction.id shape="plus" label="Dupliquer cette écriture"}
{/if}
{if PDF_COMMAND}
	<aside>
		{if !$transaction.hash && $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
			{linkbutton href="lock.php?id=%d"|args:$transaction.id shape="lock" label="Verrouiller" target="_dialog"}
		{/if}
		{linkbutton href="?id=%d&_pdf"|args:$transaction.id shape="download" label="Télécharger en PDF"}
	</aside>
{/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>

Added src/templates/acc/transactions/pending.tpl version [3033e884d5].



























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{include file="admin/_head.tpl" title="Dettes et créances non réglées sur les exercices clos" current="acc/simple"}

<nav class="tabs">
	<aside>
		{exportmenu href="?export="}
		{linkbutton shape="search" href="!acc/search.php" label="Recherche"}
	</aside>
</nav>

{if !$list->count()}
	<p class="alert block">
		Aucune écriture à afficher.
	</p>
{else}
	{include file="common/dynamic_list_head.tpl"}

		{foreach from=$list->iterate() item="line"}
			<tr>
				<td>{$line.year_label}</td>
				<td>{$line.type_label}</td>
				<td class="num">{link href="!acc/transactions/details.php?id=%d"|args:$line.id label="#%d"|args:$line.id}</td>
				<td>{$line.date|date_short}</td>
				<td class="money">{$line.change|abs|raw|money}</td>
				<td>{$line.reference}</td>
				<th>{$line.label}</th>
				<td class="actions">
					{if $line.type == Entities\Accounting\Transaction::TYPE_DEBT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)}
						{linkbutton shape="check" label="Régler cette dette" href="!acc/transactions/payoff.php?for=%d"|args:$line.id}
					{elseif $line.type == Entities\Accounting\Transaction::TYPE_CREDIT && ($line.status & Entities\Accounting\Transaction::STATUS_WAITING)}
						{linkbutton shape="export" label="Régler cette créance" href="!acc/transactions/payoff.php?for=%d"|args:$line.id}
					{/if}

					{linkbutton href="!acc/transactions/details.php?id=%d"|args:$line.id label="Détails" shape="search"}
				</td>
			</tr>
		{/foreach}
		</tbody>
	</table>

	</form>

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

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

Modified src/templates/acc/years/balance.tpl from [3b8dda4a1a] to [52c16ded8e].

1
2
3
4
5
6
7
8
9
10







11
12
13
14
15
16


17
18
19
20
21
22
23
24
25
26
27
28
29
30
{include file="_head.tpl" title="Balance d'ouverture" current="acc/years"}

{form_errors}

{if !empty($_GET.from) && empty($_POST)}
<p class="block confirm">
	L'exercice a bien été créé.
</p>
{/if}








{if $year->countTransactions()}
<p class="block alert">
	<strong>Attention&nbsp;!</strong>
	Cet exercice a déjà des écritures, peut-être avez-vous déjà renseigné la balance d'ouverture&nbsp;?
</p>
{/if}



<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Exercice&nbsp;: «&nbsp;{$year.label}&nbsp;» du {$year.start_date|date_short} au {$year.end_date|date_short}</legend>

		{if !$year_selected}
		<dl>
			<dt><label for="f_from_year">Reporter les soldes de fermeture d'un exercice</label></dt>
			<dd class="help">Pour reprendre les soldes des comptes de l'exercice précédent.</dd>
			<dd>
				<select id="f_from_year" name="from_year">
					<option value="">-- Aucun</option>
					{foreach from=$years item="year"}










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






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{include file="_head.tpl" title="Balance d'ouverture" current="acc/years"}

{form_errors}

{if !empty($_GET.from) && empty($_POST)}
<p class="block confirm">
	L'exercice a bien été créé.
</p>
{/if}

{if $year_selected}
	{if $has_balance}
	<p class="block alert">
		<strong>Attention&nbsp;!</strong>
		Une balance d'ouverture existe déjà dans cet exercice.<br />
		En validant ce formulaire, les écritures de balance et d'affectation du résultat qui existent <strong>seront supprimées et remplacées</strong>.
	</p>
	{elseif $year->countTransactions()}
	<p class="block alert">
		<strong>Attention&nbsp;!</strong>
		Cet exercice a déjà des écritures, peut-être avez-vous déjà renseigné manuellement la balance d'ouverture&nbsp;?
	</p>
	{/if}
{/if}


<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Exercice&nbsp;: «&nbsp;{$year.label}&nbsp;» du {$year.start_date|date_short} au {$year.end_date|date_short}</legend>

	{if !$year_selected}
		<dl>
			<dt><label for="f_from_year">Reporter les soldes de fermeture d'un exercice</label></dt>
			<dd class="help">Pour reprendre les soldes des comptes de l'exercice précédent.</dd>
			<dd>
				<select id="f_from_year" name="from_year">
					<option value="">-- Aucun</option>
					{foreach from=$years item="year"}
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
			let v = s.options[s.selectedIndex].dataset.closed;
			g.toggle('.warn-not-closed', v === '0' ? true : false);
		};
		s.onchange = checkOpen;
		checkOpen();
		</script>
		{/literal}
		{else}
		<p class="help">
			Renseigner ici les soldes d'ouverture (débiteur ou créditeur) des comptes.
		</p>
		{if !empty($_GET.from)}
		<p class="help">
			Normalement il suffit de valider ce formulaire pour faire le report à nouveau des soldes de comptes.
		</p>







|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
			let v = s.options[s.selectedIndex].dataset.closed;
			g.toggle('.warn-not-closed', v === '0' ? true : false);
		};
		s.onchange = checkOpen;
		checkOpen();
		</script>
		{/literal}
	{else}
		<p class="help">
			Renseigner ici les soldes d'ouverture (débiteur ou créditeur) des comptes.
		</p>
		{if !empty($_GET.from)}
		<p class="help">
			Normalement il suffit de valider ce formulaire pour faire le report à nouveau des soldes de comptes.
		</p>

Modified src/templates/acc/years/close.tpl from [6910fe6591] to [f4f3668820].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="_head.tpl" title="Clôturer un exercice" current="acc/years"}

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Clôturer un exercice</legend>
		<h3 class="warning">
			Êtes-vous sûr de vouloir clôturer l'exercice «&nbsp;{$year.label}&nbsp;» ?
		</h3>
		<p class="block alert">
			Un exercice clôturé ne peut plus être rouvert ou modifié&nbsp;!<br />
			Il ne sera plus possible de modifier ou supprimer les écritures de l'exercice clôturé.
		</p>
		<dl>
			<dt>Début de l'exercice</dt>
			<dd>{$year.start_date|date_short}</dd>
			<dt>Fin de l'exercice</dt>
			<dd>{$year.end_date|date_short}</dd>












|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{include file="_head.tpl" title="Clôturer un exercice" current="acc/years"}

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Clôturer un exercice</legend>
		<h3 class="warning">
			Êtes-vous sûr de vouloir clôturer l'exercice «&nbsp;{$year.label}&nbsp;» ?
		</h3>
		<p class="block alert">
			<strong>Un exercice clôturé ne peut plus être modifié&nbsp;!</strong><br />
			Il ne sera plus possible de modifier ou supprimer les écritures de l'exercice clôturé.
		</p>
		<dl>
			<dt>Début de l'exercice</dt>
			<dd>{$year.start_date|date_short}</dd>
			<dt>Fin de l'exercice</dt>
			<dd>{$year.end_date|date_short}</dd>

Modified src/templates/acc/years/export.tpl from [0b6f6ce320] to [ca9b7dfdac].

1
2
3
4
5
6
7


8
9
10
11
12
13
14
15


16
17
18
19
20
21
22
{include file="_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">
	<ul>
		{if !$year.closed}
		<li><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li>
		{/if}
		<li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Export</a></li>
	</ul>
</nav>



{form_errors}

<form method="get" action="{$self_url}">

<fieldset>
	<legend>Export du journal général</legend>







>
>








>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{include file="_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>

{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year.closed}

<nav class="tabs">
	<ul>
		{if !$year.closed}
		<li><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li>
		{/if}
		<li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Export</a></li>
	</ul>
</nav>

{/if}

{form_errors}

<form method="get" action="{$self_url}">

<fieldset>
	<legend>Export du journal général</legend>

Modified src/templates/acc/years/index.tpl from [2990fb262c] to [ef8d0fd588].

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
					| <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a>
					| <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a>
				</td>
			</tr>
			<tr>
				<td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td>
				<td>
				{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
					{linkbutton label="Export" shape="export" href="export.php?year=%d"|args:$year.id}
					{if !$year.closed}
						{linkbutton label="Import" shape="upload" href="import.php?year=%d"|args:$year.id}
						{linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id}
						{linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id}
						{linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id}
						{linkbutton label="Supprimer" shape="delete" href="delete.php?id=%d"|args:$year.id}
					{/if}
				{/if}
				</td>
			</tr>
		</tbody>
	{/foreach}
	</table>
{else}
	<p class="block alert">
		Il n'y a pas d'exercice en cours.
	</p>
{/if}

{include file="_foot.tpl"}







<
|
|
|
|
|
|
|
<













79
80
81
82
83
84
85

86
87
88
89
90
91
92

93
94
95
96
97
98
99
100
101
102
103
104
105
					| <a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a>
					| <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a>
				</td>
			</tr>
			<tr>
				<td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td>
				<td>

				{linkbutton label="Export" shape="export" href="export.php?year=%d"|args:$year.id}
				{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year.closed}
					{linkbutton label="Import" shape="upload" href="import.php?year=%d"|args:$year.id}
					{linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id}
					{linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id}
					{linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id}
					{linkbutton label="Supprimer" shape="delete" href="delete.php?id=%d"|args:$year.id}

				{/if}
				</td>
			</tr>
		</tbody>
	{/foreach}
	</table>
{else}
	<p class="block alert">
		Il n'y a pas d'exercice en cours.
	</p>
{/if}

{include file="_foot.tpl"}

Modified src/templates/acc/years/new.tpl from [52a805b785] to [6309d13cdc].

1
2
3
4
5
6
7
8
9
10
11
{include file="_head.tpl" title="Commencer un exercice" current="acc/years"}

{if isset($_GET.from)}
	<p class="confirm block"><strong>L'exercice a bien été clôturé.</strong><br />Vous pouvez cer un nouvel exercice ci-dessous.</p>
{/if}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>



|







1
2
3
4
5
6
7
8
9
10
11
{include file="_head.tpl" title="Commencer un exercice" current="acc/years"}

{if isset($_GET.from)}
	<p class="confirm block"><strong>L'exercice a bien été clôturé.</strong><br />Vous pouvez commencer un nouvel exercice ci-dessous.</p>
{/if}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
22
23
24
25
26
27
28



29

30
31
32
33
34
35
			{input type="date" label="Début de l'exercice" name="start_date" required=true  source=$year}
			{input type="date" label="Fin de l'exercice" name="end_date" required=true source=$year}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_years_new"}



		{linkbutton shape="left" href="./" label="Annuler"}

		{button type="submit" name="new" label="Créer ce nouvel exercice" shape="right" class="main"}
	</p>

</form>

{include file="_foot.tpl"}







>
>
>
|
>






22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
			{input type="date" label="Début de l'exercice" name="start_date" required=true  source=$year}
			{input type="date" label="Fin de l'exercice" name="end_date" required=true source=$year}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_years_new"}
		{if isset($_GET.from)}
			{linkbutton shape="left" href="./" label="Ne pas créer de nouvel exercice"}
		{else}
			{linkbutton shape="left" href="./" label="Annuler"}
		{/if}
		{button type="submit" name="new" label="Créer ce nouvel exercice" shape="right" class="main"}
	</p>

</form>

{include file="_foot.tpl"}

Modified src/templates/common/_csv_match_columns.tpl from [f3ffdaa36b] to [4bffb2b7bc].

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
					<tr>
						<th>{$csv_field}</th>
						<td class="help">{icon shape="right"}</td>
						<td>
							<select name="translation_table[{$index}]">
								<option value="">-- Ne pas importer cette colonne</option>
								{foreach from=$csv->getColumnsWithDefaults() item="column"}
									<option value="{$column.key}" {if $csv_field == $column.match}selected="selected"{/if}>{$column.label}</option>
								{/foreach}
							</select>
						</td>
					</tr>
				{/foreach}
				</tbody>
			</table>
		</dd>
	</dl>
</fieldset>







|










19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
					<tr>
						<th>{$csv_field}</th>
						<td class="help">{icon shape="right"}</td>
						<td>
							<select name="translation_table[{$index}]">
								<option value="">-- Ne pas importer cette colonne</option>
								{foreach from=$csv->getColumnsWithDefaults() item="column"}
									<option value="{$column.key}" {if $csv_field == $column.match || $csv_field == $column.label}selected="selected"{/if}>{$column.label}</option>
								{/foreach}
							</select>
						</td>
					</tr>
				{/foreach}
				</tbody>
			</table>
		</dd>
	</dl>
</fieldset>

Modified src/templates/common/files/edit_code.tpl from [2da57d0129] to [267be0c0b9].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{include file="_head.tpl" title="Édition de fichier"}

<form method="post" action="{$self_url}">
	<p>
		{input type="textarea" name="content" cols="90" rows="50" default=$content}
	</p>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript" src="{$admin_url}static/scripts/code_editor.js?{$version_hash}"></script>

{include file="_foot.tpl"}









|

<





1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
{include file="_head.tpl" title="Édition de fichier"}

<form method="post" action="{$self_url}">
	<p>
		{input type="textarea" name="content" cols="90" rows="50" default=$content}
	</p>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer et fermer" shape="right" class="main"}
	</p>

</form>

<script type="text/javascript" src="{$admin_url}static/scripts/code_editor.js?{$version_hash}"></script>

{include file="_foot.tpl"}

Modified src/templates/common/files/edit_web.tpl from [5cdea9a946] to [cf8c700b8a].

1
2
3

4
5
6
7
8
9
10
11
12
13
14
15
{include file="_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:$path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$format}
	</p>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{include file="_foot.tpl"}



>






|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="_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:$path data-fullscreen="1" data-attachments="0" data-savebtn="1" data-format=$format}
	</p>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer et fermer" shape="right" class="main"}
	</p>

</form>

{include file="_foot.tpl"}

Modified src/templates/common/search/advanced.tpl from [03c3decebc] to [3f17655e98].

80
81
82
83
84
85
86
87
88

89
90
91
92
93
94
95
	"is not equal to": "n'est pas égal à",
	"is greater than": "est supérieur à",
	"is greater than or equal to": "est supérieur ou égal à",
	"is less than": "est inférieur à",
	"is less than or equal to": "est inférieur ou égal à",
	"is between": "est situé entre",
	"is not between": "n'est pas situé entre",
	"is null": "est renseigné",
	"is not null": "n'est pas renseigné",

	"begins with": "commence par",
	"doesn't begin with": "ne commence pas par",
	"ends with": "se termine par",
	"doesn't end with": "ne se termine pas par",
	"contains": "contient",
	"doesn't contain": "ne contient pas",
	"matches one of": "correspond à",







<
|
>







80
81
82
83
84
85
86

87
88
89
90
91
92
93
94
95
	"is not equal to": "n'est pas égal à",
	"is greater than": "est supérieur à",
	"is greater than or equal to": "est supérieur ou égal à",
	"is less than": "est inférieur à",
	"is less than or equal to": "est inférieur ou égal à",
	"is between": "est situé entre",
	"is not between": "n'est pas situé entre",

	"is null": "n'est pas renseigné",
	"is not null": "est renseigné",
	"begins with": "commence par",
	"doesn't begin with": "ne commence pas par",
	"ends with": "se termine par",
	"doesn't end with": "ne se termine pas par",
	"contains": "contient",
	"doesn't contain": "ne contient pas",
	"matches one of": "correspond à",
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
q.__ = function (str) {
	return translations[str];
};
q.loadDefaultOperators();
q.default_operator = "1";

// Add specific condition just to have the column show up in result
q.operators["1"] = "afficher cette colonne dans le résultat";

for (var i in q.types_operators) {
	q.types_operators[i]["1"] = q.operators["1"];
}

q.buildInput = function (type, label, column) {
	if (label == '+')







|







105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
q.__ = function (str) {
	return translations[str];
};
q.loadDefaultOperators();
q.default_operator = "1";

// Add specific condition just to have the column show up in result
q.operators["1"] = "afficher cette colonne";

for (var i in q.types_operators) {
	q.types_operators[i]["1"] = q.operators["1"];
}

q.buildInput = function (type, label, column) {
	if (label == '+')

Modified src/templates/common/search/saved_searches.tpl from [9f71eb4c85] to [7706bc4791].

18
19
20
21
22
23
24

25

26
27
28
29
30
31
32
		<fieldset>
			<legend>Modifier une recherche enregistrée</legend>
			<dl>
				{input type="text" name="label" label="Intitulé" required=1 source=$search}
				<dt>Statut</dt>
				<?php $checked = (int)(bool)$search->id_membre; ?>
				{input type="radio" name="prive" value="1" default=$checked label="Recherche privée" help="Visible seulement par moi-même"}

				{input type="radio" name="prive" value="0" default=$checked label="Recherche publique" help="Visible et exécutable par tous les membres ayant accès à la gestion %s"|args:$target}

				<dt>Type</dt>
				<dd>
					{if $search.type == $search::TYPE_JSON}
						Avancée
					{elseif $search.type == $search::TYPE_SQL_UNPROTECTED}
						SQL non protégée
					{else}







>
|
>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
		<fieldset>
			<legend>Modifier une recherche enregistrée</legend>
			<dl>
				{input type="text" name="label" label="Intitulé" required=1 source=$search}
				<dt>Statut</dt>
				<?php $checked = (int)(bool)$search->id_membre; ?>
				{input type="radio" name="prive" value="1" default=$checked label="Recherche privée" help="Visible seulement par moi-même"}
				{if $session->canAccess($access_section, $session::ACCESS_WRITE)}
					{input type="radio" name="prive" value="0" default=$checked label="Recherche publique" help="Visible et exécutable par tous les membres ayant accès à la gestion %s"|args:$target}
				{/if}
				<dt>Type</dt>
				<dd>
					{if $search.type == $search::TYPE_JSON}
						Avancée
					{elseif $search.type == $search::TYPE_SQL_UNPROTECTED}
						SQL non protégée
					{else}

Modified src/templates/config/_menu.tpl from [e93a463d0d] to [48f28499f7].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{if !$dialog}
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Configuration</a></li>
		<li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li>
		<li{if $current == 'users'} class="current"{/if}><a href="{$admin_url}config/users/">Membres</a></li>
		<li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li>
		<li{if $current == 'modules'} class="current"{/if}><a href="{$admin_url}config/modules/">Modules</a></li>
		<li{if $current == 'plugins'} class="current"{/if}><a href="{$admin_url}config/plugins.php">Extensions</a></li>
		<li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>
	</ul>

	{if $current == 'users'}
	<ul class="sub">
		<li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/users/">Préférences</a></li>
		<li{if $sub_current == 'fields'} class="current"{/if}><a href="{$admin_url}config/fields/">Fiche des membres</a></li>







|
<







1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
{if !$dialog}
<nav class="tabs">
	<ul>
		<li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Configuration</a></li>
		<li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li>
		<li{if $current == 'users'} class="current"{/if}><a href="{$admin_url}config/users/">Membres</a></li>
		<li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li>
		<li{if $current == 'ext'} class="current"{/if}><a href="{$admin_url}config/ext/">Extensions</a></li>

		<li{if $current == 'advanced'} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li>
	</ul>

	{if $current == 'users'}
	<ul class="sub">
		<li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/users/">Préférences</a></li>
		<li{if $sub_current == 'fields'} class="current"{/if}><a href="{$admin_url}config/fields/">Fiche des membres</a></li>

Modified src/templates/config/advanced/errors.tpl from [f239e8b6ce] to [549263fb78].

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
				{/foreach}
			</table>
		</article>
		{/foreach}
	</section>
{elseif isset($errors)}
	<p class="help">
		Liste des erreurs système et de code rencontrées par Garradin.
		Cliquer sur un des bugs pour le rapporter aux développeur⋅euses de Garradin.
	</p>

	{if !count($errors)}
		<p class="block alert">Aucune erreur n'a été trouvée dans le journal error.log</p>
	{else}
		<table class="list">
			<thead>







|
<







45
46
47
48
49
50
51
52

53
54
55
56
57
58
59
				{/foreach}
			</table>
		</article>
		{/foreach}
	</section>
{elseif isset($errors)}
	<p class="help">
		Liste des erreurs système et de code rencontrées par Paheko.

	</p>

	{if !count($errors)}
		<p class="block alert">Aucune erreur n'a été trouvée dans le journal error.log</p>
	{else}
		<table class="list">
			<thead>

Modified src/templates/config/backup/restore.tpl from [9249fbb327] to [db1206bb35].

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

{if $ok}
	<p class="block confirm">
		{if $ok == 'restore'}La restauration a bien été effectuée.
			{if $ok_code & Sauvegarde::NOT_AN_ADMIN}
			</p>
			<p class="block alert">
				<strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Garradin a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter.
				Merci de corriger les droits des catégories maintenant.
			{elseif $ok_code & Sauvegarde::CHANGED_USER}
			</p>
			<p class="block alert">
				<strong>Votre compte membre n'existait pas dans la sauvegarde qui a été restaurée, vous êtes désormais connecté avec le premier compte administrateur.</strong>
			</p>
			{/if}







|







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

{if $ok}
	<p class="block confirm">
		{if $ok == 'restore'}La restauration a bien été effectuée.
			{if $ok_code & Sauvegarde::NOT_AN_ADMIN}
			</p>
			<p class="block alert">
				<strong>Vous n'êtes pas administrateur dans cette sauvegarde.</strong> Paheko a donné les droits d'administration à toutes les catégories afin d'empêcher de ne plus pouvoir se connecter.
				Merci de corriger les droits des catégories maintenant.
			{elseif $ok_code & Sauvegarde::CHANGED_USER}
			</p>
			<p class="block alert">
				<strong>Votre compte membre n'existait pas dans la sauvegarde qui a été restaurée, vous êtes désormais connecté avec le premier compte administrateur.</strong>
			</p>
			{/if}

Added src/templates/config/ext/delete.tpl version [af1fe35dc5].



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{include file="_head.tpl" title="Désinstaller une extension" current="config"}

{if $plugin}
	{include file="common/delete_form.tpl"
		legend="Supprimer une extension"
		confirm="Cocher cette case pour confirmer la suppression de toutes les données liées à cette extension"
		warning="Êtes-vous sûr de vouloir supprimer l'extension « %s » ?"|args:$plugin.label
		alert="Attention, cela supprimera toutes les données liées à l'extension !"}
{else}
	{include file="common/delete_form.tpl"
		legend="Supprimer une extension"
		confirm="Cocher cette case pour confirmer la suppression de toutes les données liées à cette extension"
		warning="Êtes-vous sûr de vouloir supprimer l'extension « %s » ?"|args:$module.label
		alert="Attention, cela supprimera toutes les données liées à l'extension, y compris les modifications apportées !"}
{/if}

{include file="_foot.tpl"}

Added src/templates/config/ext/index.tpl version [969da88b84].



















































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
{include file="_head.tpl" title="Extensions" current="config"}

{include file="config/_menu.tpl" current="ext"}

<nav class="tabs">

	<ul class="sub">
		<li{if !$installable} class="current"{/if}><a href="./">Activées</a></li>
		<li{if $installable} class="current"{/if}><a href="./?install=1">Inactives</a></li>
	</ul>
</nav>

{if !empty($url_plugins)}
<p class="actions">
	{linkbutton shape="help" href=$url_plugins label="Trouver d'autres extensions à installer" target="_blank"}
</p>
{/if}

<p class="help">Les extensions apportent des fonctionnalités supplémentaires, et peuvent être activées selon vos besoins.</p>

{form_errors}

<form method="post" action="">
	<table class="list">
		<thead>
			<td></td>
			<td>Extension</td>
			<td>Accès restreint</td>
			<td></td>
			<td></td>
			<td></td>
			<td></td>
		</thead>
		<tbody>
			{foreach from=$list item="item"}
			<tr {if $_GET.focus == $item.name}class="highlight"{/if}>
			{if $item.broken_message}
				<td></td>
				<td colspan="6">
					<strong class="error">Extension cassée : {$item.name}</strong><br />
					{$item.broken_message}
				</td>
			{else}
				<td class="icon">
					{if $item.icon_url}
						<svg><use xlink:href='{$item.icon_url}#img' href="{$item.icon_url}#img"></use></svg>
						{if $item.url}
							</a>
						{/if}
					{/if}
				</td>
				<td>
					<h3>{if $item.label}{$item.label}{else}{$item.name}{/if}
						{if $item.module && $item.module->canDelete()}
							<strong class="tag">Modifiée</strong>
						{elseif $item.module}
							<span class="tag">Modifiable</span>
						{/if}
					</h3>
					<small>{$item.description|escape|nl2br}</small><br />
					<small class="help">
						{if $item.author}
							Par {link label=$item.author href=$item.url target="_blank"}
						{/if}
						{if $item.plugin && $item.plugin.version}— Version {$item.plugin.version}{/if}
						{if $item.readme_url}
							— {link href=$item.readme_url label="Documentation" target="_dialog"}
						{/if}
					</small>
				</td>
				{if $item.broken}
					<td colspan="5">
						{if ENABLE_TECH_DETAILS}
							<strong>Le code source de l'extension est absent du répertoire <tt>…/data/plugins/</tt></strong>
						{else}
							<strong>Cette extension n'est pas installée sur ce serveur.</strong><br />
						{/if}
						<br />
						<small>Il n'est pas possible de la supprimer non plus, le code source est nécessaire pour pouvoir la supprimer.</small>
					</td>
				{else}
					<td>
						{if $item.restrict_section}
							<span class="permissions">{display_permissions section=$item.restrict_section level=$item.restrict_level}</span>
						{/if}
					</td>
					<td>
						{if $item.enabled && $item.url}
							{linkbutton shape="right" label="Ouvrir" href=$item.url}
						{/if}
					</td>
					<td class="actions">
						{if $item.module && $item.enabled}
							{if $item.module->hasLocal() && $item.module->hasDist()}
								{linkbutton label="Remettre à zéro" href="delete.php?module=%s"|args:$item.name shape="reset" target="_dialog"}
							{/if}
							{*FIXME{linkbutton label="Modifier" href="edit.php?module=%s"|args:$item.name shape="edit" target="_dialog"}*}
						{elseif $item.module && !$item.enabled && $item.module->canDelete()}
							{linkbutton label="Supprimer" href="delete.php?module=%s"|args:$item.name shape="delete" target="_dialog"}
						{elseif $item.plugin && !$item.enabled && $item.installed}
							{linkbutton label="Supprimer" href="delete.php?plugin=%s"|args:$item.name shape="delete" target="_dialog"}
						{/if}
					</td>
					<td class="actions">
						{if $item.config_url && $item.enabled}
							{linkbutton label="Configurer" href=$item.config_url shape="settings" target="_dialog"}
						{/if}
					</td>
					<td class="actions">
						{if $item.module}
							{if $item.enabled}
								{button type="submit" label="Désactiver" shape="eye-off" name="disable_module" value=$item.name}
							{else}
								{button type="submit" label="Activer" shape="eye" name="enable" value=$item.name}
							{/if}
						{else}
							{if $item.enabled}
								{button type="submit" label="Désactiver" shape="eye-off" name="disable_plugin" value=$item.name}
							{else}
								{button type="submit" label="Activer" shape="eye" name="install" value=$item.name}
							{/if}
						{/if}
					</td>
				{/if}
			{/if}
			</tr>
			{/foreach}
		</tbody>
	</table>
	{csrf_field key=$csrf_key}
</form>

<p class="help">
	La mention <em class="tag">Modifiable</em> indique que cette extension est un module que vous pouvez modifier. {linkbutton shape="help" label="Documentation des modules" href=$url_help_modules target="_dialog"}
</p>

{include file="_foot.tpl"}

Modified src/templates/config/index.tpl from [dfd5e20e88] to [98c661b3c1].

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	<fieldset>
		<legend>Informations</legend>
		<dl>
			<dt>Version installée</dt>
			<dd>Paheko {$garradin_version}</dd>
			{if CONTRIBUTOR_LICENSE === null}
			<dd class="help">
				Le développement et le support de Garradin ne sont possibles que grâce à votre soutien&nbsp;!<br />
				{linkbutton href="https://kd2.org/soutien.html" label="Faire un don pour soutenir le développement" target="_blank" shape="export"} :-)
			</dd>
			{/if}
			{if $new_version}
			<dd><p class="block alert">
				Une nouvelle version <strong>{$new_version}</strong> est disponible !<br />
				{if ENABLE_UPGRADES}







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	<fieldset>
		<legend>Informations</legend>
		<dl>
			<dt>Version installée</dt>
			<dd>Paheko {$garradin_version}</dd>
			{if CONTRIBUTOR_LICENSE === null}
			<dd class="help">
				Le développement et le support de Paheko ne sont possibles que grâce à votre soutien&nbsp;!<br />
				{linkbutton href="https://kd2.org/soutien.html" label="Faire un don pour soutenir le développement" target="_blank" shape="export"} :-)
			</dd>
			{/if}
			{if $new_version}
			<dd><p class="block alert">
				Une nouvelle version <strong>{$new_version}</strong> est disponible !<br />
				{if ENABLE_UPGRADES}

Deleted src/templates/config/modules/index.tpl version [14240b61b4].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{include file="_head.tpl" title="Modules" current="config"}

{include file="config/_menu.tpl" current="modules"}

<table class="list">
	<tbody>
		{foreach from=$list item="module"}
		<tr>
			<td class="icon">
				{if $url = $module->icon_url()}
				<svg>
					<use xlink:href='{$url}#img' href="{$url}#img"></use>
				</svg>
				{/if}
			</td>
			<td><h3>{$module.label}</h3>{$module.description|escape|nl2br}</td>
			<td class="actions">
				{*{linkbutton label="Modifier" href="edit.php?module=%s"|args:$module.name shape="edit" target="_dialog"}*}
				{if $module->hasConfig() && $module.enabled}
					{linkbutton label="Configurer" href=$module->url($module::CONFIG_TEMPLATE) shape="settings" target="_dialog"}
				{/if}
				{if $module->canDelete()}
					{if $module->hasDist()}
						{linkbutton label="Remettre à zéro" href="delete.php?module=%s"|args:$module.name shape="reset" target="_dialog"}
					{else}
						{linkbutton label="Supprimer" href="delete.php?module=%s"|args:$module.name shape="delete" target="_dialog"}
					{/if}
				{/if}
			</td>
			<td class="actions">
				{if $module.enabled}
					{linkbutton label="Désactiver" shape="eye-off" href="?disable=%s"|args:$module.name}
				{else}
					{linkbutton label="Activer" shape="eye" href="?enable=%s"|args:$module.name}
				{/if}
			</td>
		</tr>
		{/foreach}
	</tbody>
</table>

{include file="_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































































Deleted src/templates/config/plugins.tpl version [8b8b529bed].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
{include file="_head.tpl" title="Extensions" current="config"}

{include file="config/_menu.tpl" current="plugins"}

{form_errors}

{if !empty($delete)}
	<form method="post" action="{$self_url}">

		<fieldset>
			<legend>Désinstaller une extension</legend>
			<h3 class="warning">
				Êtes-vous sûr de vouloir supprimer l'extension «&nbsp;{$plugin.name}&nbsp;» ?
			</h3>
			<p class="block alert">
				<strong>Attention</strong> : cette action est irréversible et effacera toutes les
				données associées à l'extension.
			</p>
		</fieldset>

		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="delete" label="Désinstaller" shape="delete" class="main"}
		</p>
	</form>
{else}
	{if !empty($list_installed)}
		<table class="list">
			<thead>
				<tr>
					<th>Extension</th>
					<td></td>
					<td>Version installée</td>
					<td></td>
				</tr>
			</thead>
			<tbody>
				{foreach from=$list_installed item="plugin"}
				<tr{if $plugin.disabled} class="disabled"{/if}>
					<th>
						<h4>{$plugin.name}</h4>
						<small>{$plugin.description}</small>
					</th>
					{if $plugin.disabled}
					<td colspan="3">
						<span class="alert">Code source du plugin non trouvé dans le répertoire <em>plugins</em>&nbsp;!</span><br />
						Ce plugin ne peut fonctionner ou être désinstallé.
					</td>
					{else}
					<td>
						<a href="{$plugin.url}" onclick="return !window.open(this.href);">{$plugin.auteur}</a>
					</td>
					<td>
						{$plugin.version}
					</td>
					<td class="actions">
						{if !empty($plugin.config)}
							{linkbutton shape="settings" label="Configurer" href="!p/%s/config.php"|args:$plugin.id}
						{/if}
						{linkbutton shape="delete" href="!config/plugins.php?delete=%s"|args:$plugin.id label="Désinstaller"}
					</td>
					{/if}
				</tr>
				{/foreach}
			</tbody>
		</table>
	{else}
		<p class="help">
			Aucune extension n'est installée.
			Vous pouvez consulter <a href="{$garradin_website}">le site de Garradin</a> pour obtenir
			des extensions à télécharger.
		</p>
	{/if}

	{if !empty($list_available)}
	<form method="post" action="{$self_url}">

		<fieldset>
			<legend>Extensions à installer</legend>
			<dl>
				{foreach from=$list_available item="plugin" key="id"}
				{input type="radio" name="plugin" value=$id label=$plugin.name help="version %s"|args:$plugin.version}
				<dd>[<a href="{$plugin.url}" onclick="return !window.open(this.href);">{$plugin.author}</a>] {$plugin.description}</dd>
				{/foreach}
			</dl>
		</fieldset>

		<p class="help">
			Attention : installer une extension non officielle peut présenter des risques de sécurité
			et de stabilité.
		</p>

		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="install" label="Installer" shape="right" class="main"}
		</p>
	</form>
	{/if}
{/if}

{include file="_foot.tpl"}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































































































































































Modified src/templates/install.tpl from [6849a1fd7c] to [723c5234c8].

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




































30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{include file="_head.tpl" title="Paheko - Installation" menu=false}

<p class="help">
    Bienvenue dans Paheko !
    Veuillez remplir les quelques informations suivantes pour terminer
    l'installation.
</p>

{form_errors}

<form method="post" action="{$self_url}">

<fieldset>
    <legend>Informations sur l'association</legend>
    <dl>
        {input type="select" required=true label="Pays (pour la comptabilité)" options=$countries default="FR" help="Ce choix permet de configurer les règles comptables en fonction du pays de l'association." name="country"}
        {input type="text" label="Nom de l'association" required=true name="name"}
    </dl>
</fieldset>

<fieldset>
    <legend>Création du compte administrateur</legend>
    <dl>
        {input type="text" label="Nom et prénom" required=true name="user_name"}
        {input type="email" label="Adresse E-Mail" required=true name="user_email"}
        {include file="users/_password_form.tpl" field="password" required=true}
    </dl>
</fieldset>





































<p class="submit">
    {csrf_field key="install"}
    {button type="submit" name="save" label="Terminer l'installation" shape="right" class="main"}
</p>

<script type="text/javascript" src="{$admin_url}static/scripts/loader.js"></script>

<script type="text/javascript">
{literal}
var form = $('form')[0];
form.onsubmit = function () {
    $('#f_submit').style.opacity = 0;
    var loader = document.createElement('div');
    loader.className = 'loader install';
    loader.innerHTML = '<b>Paheko est en cours d\'installation…</b>';
    $('#f_submit').parentNode.appendChild(loader);
    animatedLoader(loader, 5);
};
{/literal}
</script>

</form>


{include file="_foot.tpl"}
|


|
|
<







|
|
|
|
|



|
|
|
|
|
|


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

|
|


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




1
2
3
4
5

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
















70
71
72
73
{include file="_head.tpl" title="Démarrer avec Paheko" menu=false}

<p class="help">
	<strong>Bienvenue dans Paheko !</strong><br />
	Veuillez remplir les informations suivantes pour démarrer la gestion de votre association.

</p>

{form_errors}

<form method="post" action="{$self_url}">

<fieldset>
	<legend>Informations sur l'association</legend>
	<dl>
		{input type="select" required=true label="Pays (pour la comptabilité)" options=$countries default="FR" help="Ce choix permet de configurer les règles comptables en fonction du pays de l'association." name="country"}
		{input type="text" label="Nom de l'association" required=true name="name"}
	</dl>
</fieldset>

<fieldset>
	<legend>Création du compte administrateur</legend>
	<dl>
		{input type="text" label="Nom et prénom" required=true name="user_name"}
		{input type="email" label="Adresse E-Mail" required=true name="user_email"}
		{include file="users/_password_form.tpl" field="password" required=true}
	</dl>
</fieldset>

{if count($installable)}
<fieldset>
	<legend>Activer des extensions</legend>
	<p class="help">Les extensions apportent des fonctionnalités supplémentaires, et peuvent être activées selon vos besoins.</p>
	<table class="list">
		<tr>
			<td>
			</td>
			<th>Nom</th>
		</tr>
		{foreach from=$installable key="name" item="data"}
		<tr>
			{if $data.plugin}
				<td class="check">
					{input type="checkbox" name="plugins[%s]"|args:$name value=1}
				</td>
				<td>
					<label for={"f_plugins%s_1"|args:$name}><strong>{$data.plugin.label}</strong><br />
					<small>{$data.plugin.description|escape|nl2br}</small></label>
				</td>
			{else}
				<td class="check">
					{input type="checkbox" name="modules[%s]"|args:$name value=1}
				</td>
				<td>
					<label for={"f_modules%s_1"|args:$name}><strong>{$data.module.label}</strong><br />
					<small>{$data.module.description|escape|nl2br}</small></label>
				</td>
			{/if}
		</tr>
		{/foreach}
	</table>
	<p class="help">Note : il sera ensuite possible d'activer ou désactiver les extensions dans la <strong>Configuration</strong>.</p>
</fieldset>
{/if}

<p class="submit">
	{csrf_field key="install"}
	{button type="submit" name="save" label="Terminer l'installation" shape="right" class="main"}
</p>

















</form>


{include file="_foot.tpl"}

Modified src/templates/login.tpl from [c9e8fc3116] to [75add3ccf0].

57
58
59
60
61
62
63





64
65
66
67
68
69
70
71
72
73
74
75
		{csrf_field key="login"}
		{button type="submit" name="login" label="Se connecter" shape="right" class="main"}
		{if !DISABLE_EMAIL && !$app_token}
			{linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"}
			{linkbutton href="!password.php?new" label="Première connexion ?" shape="user"}
		{/if}
	</p>






</form>

{literal}
<script type="text/javascript" async="async">
if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) {
	document.getElementById('old_browser').style.display = 'block';
}
</script>
{/literal}

{include file="_foot.tpl"}







>
>
>
>
>












57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
		{csrf_field key="login"}
		{button type="submit" name="login" label="Se connecter" shape="right" class="main"}
		{if !DISABLE_EMAIL && !$app_token}
			{linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"}
			{linkbutton href="!password.php?new" label="Première connexion ?" shape="user"}
		{/if}
	</p>

    <p class="help">
        Suggestion : mettez cette page dans vos favoris pour la retrouver facilement :-)<br />
        <small>(Sur ordinateur appuyez sur <tt>Ctrl</tt> + <tt>D</tt>. Aide&nbsp;: <a href="https://support.mozilla.org/fr/kb/marque-pages-firefox#w_marquer-une-page" target="_blank">Firefox</a>, <a href="https://support.google.com/chrome/answer/188842?hl=fr&co=GENIE.Platform%3DDesktop&oco=0" target="_blank">Chrome</a>)</small>
    </p>

</form>

{literal}
<script type="text/javascript" async="async">
if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) {
	document.getElementById('old_browser').style.display = 'block';
}
</script>
{/literal}

{include file="_foot.tpl"}

Added src/templates/me/_nav.tpl version [680aee5229].















>
>
>
>
>
>
>
1
2
3
4
5
6
7
<nav class="tabs">
	<ul>
		<li{if $current == 'me'} class="current"{/if}><a href="{$admin_url}me/">Mes informations personnelles</a></li>
		<li{if $current == 'security'} class="current"{/if}><a href="{$admin_url}me/security.php">Mot de passe et options de sécurité</a></li>
		<li{if $current == 'preferences'} class="current"{/if}><a href="{$admin_url}me/preferences.php">Préférences</a></li>
	</ul>
</nav>

Modified src/templates/me/edit.tpl from [0d731c955e] to [d63c67d1c3].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="_head.tpl" title="Mes informations personnelles" current="me"}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}me/">Mes informations</a></li>
		<li><a href="{$admin_url}me/security.php">Mot de passe et options de sécurité</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Informations personnelles</legend>


<
<
<
<
<
|







1
2





3
4
5
6
7
8
9
10
{include file="_head.tpl" title="Mes informations personnelles" current="me"}






{include file="./_nav.tpl" current="me"}

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Informations personnelles</legend>

Modified src/templates/me/index.tpl from [c8a4df60cf] to [78eaa6c657].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="_head.tpl" title="Mes informations personnelles" current="me"}

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}me/">Mes informations</a></li>
		<li><a href="{$admin_url}me/security.php">Mot de passe et sécurité</a></li>
		<li><a href="{$admin_url}me/preferences.php">Préférences</a></li>
	</ul>
</nav>

{if $ok !== null}
<p class="confirm block">
	Les modifications ont bien été enregistrées.
</p>
{/if}



<
<
<
<
<
<
|







1
2






3
4
5
6
7
8
9
10
{include file="_head.tpl" title="Mes informations personnelles" current="me"}







{include file="./_nav.tpl" current="me"}

{if $ok !== null}
<p class="confirm block">
	Les modifications ont bien été enregistrées.
</p>
{/if}

Modified src/templates/me/preferences.tpl from [d5439de3ce] to [b574cf9be4].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="_head.tpl" title="Mes préférences" current="me"}

<nav class="tabs">
	<ul>
		<li><a href="{$admin_url}me/">Mes informations</a></li>
		<li><a href="{$admin_url}me/security.php">Mot de passe et sécurité</a></li>
		<li class="current"><a href="{$admin_url}me/preferences.php">Préférences</a></li>
	</ul>
</nav>

{if $ok !== null}
<p class="confirm block">
	Les modifications ont bien été enregistrées.
</p>
{/if}



<
<
<
<
<
<
|







1
2






3
4
5
6
7
8
9
10
{include file="_head.tpl" title="Mes préférences" current="me"}







{include file="./_nav.tpl" current="preferences"}

{if $ok !== null}
<p class="confirm block">
	Les modifications ont bien été enregistrées.
</p>
{/if}

Modified src/templates/me/security.tpl from [dc16dd7875] to [1e057f4224].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{include file="_head.tpl" title="Mes informations de connexion et sécurité" current="me"}

<nav class="tabs">
	<ul>
		<li><a href="{$admin_url}me/">Mes informations personnelles</a></li>
		<li class="current"><a href="{$admin_url}me/security.php">Mot de passe et options de sécurité</a></li>
	</ul>
</nav>

{if $ok}
<p class="block confirm">
	Changements enregistrés.
</p>
{/if}



<
<
<
|
<
<







1
2



3


4
5
6
7
8
9
10
{include file="_head.tpl" title="Mes informations de connexion et sécurité" current="me"}




{include file="./_nav.tpl" current="security"}



{if $ok}
<p class="block confirm">
	Changements enregistrés.
</p>
{/if}

111
112
113
114
115
116
117
118
119
120
121
122
123
			<dd class="help">Permet de chiffrer les messages qui vous sont envoyés par e-mail, notamment les messages de récupération de mot de passe, pour empêcher un attaquant de prendre contrôle de votre compte si votre adresse e-mail est piratée.</dd>
		{/if}
		<dt>Déconnecter toutes mes sessions</dt>
		<dd>{{Vous n'avez actuellement qu'une seule session ouverte (celle-ci).}{Vous avez actuellement %n sessions ouvertes (y compris celle-ci).} n=$sessions_count}</dd>
		<dd>{linkbutton href="!logout.php?all" label="Me déconnecter de toutes les sessions" shape="logout"}</dd>
		<dt>Journal de connexion</dt>
		<dd>Permet de voir les tentatives de connexion, les modifications de mot de passe, etc.</dd>
		<dd>{linkbutton href="!users/log.php?id=%d"|args:$logged_user.id label="Voir mon journal de connexion" shape="menu"}</dd>
	</dl>
{/if}


{include file="_foot.tpl"}







|





106
107
108
109
110
111
112
113
114
115
116
117
118
			<dd class="help">Permet de chiffrer les messages qui vous sont envoyés par e-mail, notamment les messages de récupération de mot de passe, pour empêcher un attaquant de prendre contrôle de votre compte si votre adresse e-mail est piratée.</dd>
		{/if}
		<dt>Déconnecter toutes mes sessions</dt>
		<dd>{{Vous n'avez actuellement qu'une seule session ouverte (celle-ci).}{Vous avez actuellement %n sessions ouvertes (y compris celle-ci).} n=$sessions_count}</dd>
		<dd>{linkbutton href="!logout.php?all" label="Me déconnecter de toutes les sessions" shape="logout"}</dd>
		<dt>Journal de connexion</dt>
		<dd>Permet de voir les tentatives de connexion, les modifications de mot de passe, etc.</dd>
		<dd>{linkbutton href="!users/log.php" label="Voir mon journal de connexion" shape="menu"}</dd>
	</dl>
{/if}


{include file="_foot.tpl"}

Modified src/templates/users/details.tpl from [d966205540] to [2007f910f2].

50
51
52
53
54
55
56







57
58
59
60
61
62
63
			<dd>{link href="?id=%d"|args:$child.id label=$child.name}</dd>
		{/foreach}
	{/if}
</dl>

<aside class="describe">
	<dl class="describe">







		<dt>Catégorie</dt>
		<dd>{$category.name}</dd>
		<dt>Droits</dt>
		<dd><span class="permissions">{display_permissions permissions=$category}</span></dd>
		<dt>Dernière connexion</dt>
		<dd>{if empty($user.date_login)}Jamais{else}{$user.date_login|date_short:true}{/if}</dd>
		<dd>







>
>
>
>
>
>
>







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
			<dd>{link href="?id=%d"|args:$child.id label=$child.name}</dd>
		{/foreach}
	{/if}
</dl>

<aside class="describe">
	<dl class="describe">
		{if $user.date_updated}
			<dt>Fiche modifiée le</dt>
			<dd>{$user.date_updated|date_short:true}</dd>
			<dd>
				{linkbutton shape="menu" label="Historique" href="!users/log.php?history=%d"|args:$user.id}
			</dd>
		{/if}
		<dt>Catégorie</dt>
		<dd>{$category.name}</dd>
		<dt>Droits</dt>
		<dd><span class="permissions">{display_permissions permissions=$category}</span></dd>
		<dt>Dernière connexion</dt>
		<dd>{if empty($user.date_login)}Jamais{else}{$user.date_login|date_short:true}{/if}</dd>
		<dd>

Modified src/templates/users/log.tpl from [b8f7ed5cb2] to [a4a181efc2].




1

2
3
4




5
6

7
8
9

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



{include file="_head.tpl" title="Journal de connexion et d'actions"}


{if $id && $current == 'users'}
	{include file="users/_nav_user.tpl" id=$id}




{/if}


<p class="help">
	Cette page liste les tentatives de connexion, les modifications de mot de passe ou d'identifiant, et toutes les actions de création, suppression ou modification de contenu de ce membre.
</p>


{if $list->count()}
	{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>
			{if !$id}
			<th>{if !$row.identity}*{else}{$row.identity}{/if}</th>
			{/if}
			<td>{$row.created|date_short:true}</td>
			<td class="help">
				{if $row.type == Log::LOGIN_FAIL || $row.type == Log::LOGIN_PASSWORD_CHANGE}
					<span class="alert">{icon shape="alert"}</span>
				{/if}
>
>
>
|
>

|
|
>
>
>
>


>



>






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{if $params.history}
	{include file="_head.tpl" title="Historique des modifications"}
{else}
	{include file="_head.tpl" title="Journal de connexion et d'actions"}
{/if}

{if $params.id_user}
	{include file="users/_nav_user.tpl" id=$params.id_user}
{elseif $params.history}
	{include file="users/_nav_user.tpl" id=$params.history}
{else}
	{include file="me/_nav.tpl" current="security"}
{/if}

{if !$params.history}
<p class="help">
	Cette page liste les tentatives de connexion, les modifications de mot de passe ou d'identifiant, et toutes les actions de création, suppression ou modification de contenu de ce membre.
</p>
{/if}

{if $list->count()}
	{include file="common/dynamic_list_head.tpl"}

	{foreach from=$list->iterate() item="row"}
		<tr>
			{if !$params.id_self}
			<th>{if !$row.identity}*{else}{$row.identity}{/if}</th>
			{/if}
			<td>{$row.created|date_short:true}</td>
			<td class="help">
				{if $row.type == Log::LOGIN_FAIL || $row.type == Log::LOGIN_PASSWORD_CHANGE}
					<span class="alert">{icon shape="alert"}</span>
				{/if}
47
48
49
50
51
52
53


54
55
56
57
58
59
60
61
62
		</tr>
	{/foreach}

	</tbody>
	</table>

	{$list->getHTMLPagination()|raw}


{else}
	<p class="block alert">
		Aucune activité trouvée.
	</p>
{/if}

</form>

{include file="_foot.tpl"}







>
>









57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
		</tr>
	{/foreach}

	</tbody>
	</table>

	{$list->getHTMLPagination()|raw}

	<p class="help">Note : les heures correspondent au fuseau horaire du serveur (<?=ini_get('date.timezone')?>).</p>
{else}
	<p class="block alert">
		Aucune activité trouvée.
	</p>
{/if}

</form>

{include file="_foot.tpl"}

Modified src/templates/web/config.tpl from [99e5132839] to [d1ae25c3c3].

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
						{button type="submit" name="disable_site" label="Désactiver le site public" shape="right" class="main"}
						{csrf_field key="config_site"}
					</p>
				</form>
			</dt>
			<dd class="help">
				En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br />
				Cette option est utile si vous avez déjà un site web et ne souhaitez pas utiliser la fonctionnalité site web de Garradin.
			</dd>
		</dl>
	</fieldset>

	{/if}

	<form method="post" action="{$self_url}">







|







69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
						{button type="submit" name="disable_site" label="Désactiver le site public" shape="right" class="main"}
						{csrf_field key="config_site"}
					</p>
				</form>
			</dt>
			<dd class="help">
				En désactivant le site public, les visiteurs seront automatiquement redirigés vers la page de connexion.<br />
				Cette option est utile si vous avez déjà un site web et ne souhaitez pas utiliser la fonctionnalité site web de Paheko.
			</dd>
		</dl>
	</fieldset>

	{/if}

	<form method="post" action="{$self_url}">

Modified src/www/admin/_inc.php from [0cfa0f9f9d] to [2059b7f42e].

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
		{
			Utils::redirect(ADMIN_URL . 'login.php');
		}
	}

	$tpl->assign('current', '');

	if ($session->get('plugins_menu') === null) {
		// Construction de la liste de plugins pour le menu
		// et stockage en session pour ne pas la recalculer à chaque page
		$session->set('plugins_menu', Plugin::listMenu($session));
	}

	$tpl->assign('plugins_menu', $session->get('plugins_menu'));
}

// Make sure we allow frames to work
header('X-Frame-Options: SAMEORIGIN', true);







<
<
<
<
<
<
|




52
53
54
55
56
57
58






59
60
61
62
63
		{
			Utils::redirect(ADMIN_URL . 'login.php');
		}
	}

	$tpl->assign('current', '');







	$tpl->assign('plugins_menu', Plugins::listModulesAndPluginsMenu($session));
}

// Make sure we allow frames to work
header('X-Frame-Options: SAMEORIGIN', true);

Modified src/www/admin/acc/accounts/deposit.php from [b43f97e6b8] to [f448751140].

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

$form->runIf('save', function () use ($checked, $transaction, $journal) {
	if (!count($checked)) {
		throw new UserException('Aucune ligne n\'a été cochée, impossible de créer un dépôt. Peut-être vouliez-vous saisir un virement ?');
	}

	$transaction->importFromDepositForm();
	Transactions::saveDeposit($transaction, $journal, $checked);

	Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id());
}, 'acc_deposit_' . $account->id());

// Uncheck everything if there was an error
if ($form->hasErrors()) {
	$journal = $account->getDepositJournal(CURRENT_YEAR_ID);
}

$date = new \DateTime;

if ($date > $current_year->end_date) {
	$date = $current_year->end_date;
}

$target = $account::TYPE_BANK;

$journal_count = $account->countDepositJournal(CURRENT_YEAR_ID);

$missing_balance = $account->getDepositMissingBalance(CURRENT_YEAR_ID);

$tpl->assign(compact(
	'account',
	'journal',
	'date',
	'target',
	'checked',
	'journal_count',
	'missing_balance',
	'transaction'
));

$tpl->display('acc/accounts/deposit.tpl');







|

















|

|







<





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

$form->runIf('save', function () use ($checked, $transaction, $journal) {
	if (!count($checked)) {
		throw new UserException('Aucune ligne n\'a été cochée, impossible de créer un dépôt. Peut-être vouliez-vous saisir un virement ?');
	}

	$transaction->importFromDepositForm();
	Transactions::saveDeposit($transaction, $journal->iterate(), $checked);

	Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id());
}, 'acc_deposit_' . $account->id());

// Uncheck everything if there was an error
if ($form->hasErrors()) {
	$journal = $account->getDepositJournal(CURRENT_YEAR_ID);
}

$date = new \DateTime;

if ($date > $current_year->end_date) {
	$date = $current_year->end_date;
}

$target = $account::TYPE_BANK;

$missing_balance = $account->getDepositMissingBalance(CURRENT_YEAR_ID);

$journal->loadFromQueryString();

$tpl->assign(compact(
	'account',
	'journal',
	'date',
	'target',
	'checked',

	'missing_balance',
	'transaction'
));

$tpl->display('acc/accounts/deposit.tpl');

Modified src/www/admin/acc/accounts/index.php from [f19ff030ea] to [ca40714bbd].

1
2
3
4

5
6
7
8
9
10




11
12
13
14
15
<?php
namespace Garradin;

use Garradin\Accounting\Reports;


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

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





$tpl->assign('chart_id', $current_year->id_chart);
$tpl->assign('grouped_accounts', Reports::getClosingSumsFavoriteAccounts(['year' => CURRENT_YEAR_ID]));

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




>






>
>
>
>





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

use Garradin\Accounting\Reports;
use Garradin\Accounting\Transactions;

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

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

$pending_count = Transactions::listPendingCreditAndDebtForClosedYears()->count();

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

$tpl->assign('chart_id', $current_year->id_chart);
$tpl->assign('grouped_accounts', Reports::getClosingSumsFavoriteAccounts(['year' => CURRENT_YEAR_ID]));

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

Modified src/www/admin/acc/accounts/simple.php from [83f17410b3] to [d130795fe7].

30
31
32
33
34
35
36






37
38
39

$list = Transactions::listByType(CURRENT_YEAR_ID, $type == -1 ? null : $type);
$list->setTitle(sprintf('Suivi - %s', $types[$type]));
$list->loadFromQueryString();

$can_edit = $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year->closed;







$tpl->assign(compact('type', 'list', 'types', 'can_edit', 'year'));

$tpl->display('acc/accounts/simple.tpl');







>
>
>
>
>
>
|


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

$list = Transactions::listByType(CURRENT_YEAR_ID, $type == -1 ? null : $type);
$list->setTitle(sprintf('Suivi - %s', $types[$type]));
$list->loadFromQueryString();

$can_edit = $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN) && !$year->closed;

$pending_count = null;

if ($type == Transaction::TYPE_CREDIT || $type == Transaction::TYPE_DEBT) {
	$pending_count = Transactions::listPendingCreditAndDebtForClosedYears()->count();
}

$tpl->assign(compact('type', 'list', 'types', 'can_edit', 'year', 'pending_count'));

$tpl->display('acc/accounts/simple.tpl');

Modified src/www/admin/acc/reports/_inc.php from [4ba354a98e] to [689b309913].

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

	if (qg('after') && ($a = Entity::filterUserDateValue(qg('after')))) {
		$criterias['after'] = $a;
	}

	$criterias['year'] = $year->id();
	$tpl->assign('year', $year);
	$tpl->assign('close_date', $year->closed && empty($criterias['before']) ? $year->end_date : time());
	$tpl->assign('before_default', $criterias['before'] ?? $year->end_date);
	$tpl->assign('after_default', $criterias['after'] ?? $year->start_date);
}

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







<







42
43
44
45
46
47
48

49
50
51
52
53
54
55

	if (qg('after') && ($a = Entity::filterUserDateValue(qg('after')))) {
		$criterias['after'] = $a;
	}

	$criterias['year'] = $year->id();
	$tpl->assign('year', $year);

	$tpl->assign('before_default', $criterias['before'] ?? $year->end_date);
	$tpl->assign('after_default', $criterias['after'] ?? $year->start_date);
}

if (qg('projects_only')) {
	$criterias['projects_only'] = true;
}
73
74
75
76
77
78
79


		$c = $c->format('Y-m-d');
	}
}

$tpl->assign('criterias_query', http_build_query($criterias_query));
unset($criterias_query['compare_year']);
$tpl->assign('criterias_query_no_compare', http_build_query($criterias_query));









>
>
72
73
74
75
76
77
78
79
80
		$c = $c->format('Y-m-d');
	}
}

$tpl->assign('criterias_query', http_build_query($criterias_query));
unset($criterias_query['compare_year']);
$tpl->assign('criterias_query_no_compare', http_build_query($criterias_query));

$tpl->assign('now', new \DateTime);

Modified src/www/admin/acc/transactions/new.php from [611e89f981] to [53588cb2a1].

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

$lines = [[], []];
$form->runIf(f('lines') !== null, function () use (&$lines) {
	$lines = Transaction::getFormLines();
});

// Quick-fill transaction from query parameters

if (qg('a')) {
	$amount = Utils::moneyToInteger(qg('a'));
}


if (qg('a0')) {
	$amount = (int)qg('a0');
}


if (qg('l')) {
	$transaction->label = qg('l');
}


if (qg('d')) {
	$transaction->date = new Date(qg('d'));
}


if (qg('t')) {
	$transaction->type = (int) qg('t');
}








// Quick-set bank account






if ($id = (int)qg('bk')) {



	$transaction->setDefaultAccount($transaction::TYPE_REVENUE, 'debit', $id);
	$transaction->setDefaultAccount($transaction::TYPE_EXPENSE, 'credit', $id);





	$transaction->setDefaultAccount($transaction::TYPE_TRANSFER, 'debit', $id);
}









$types_details = $transaction->getTypesDetails();

// Duplicate transaction
if (qg('copy')) {
	$old = Transactions::get((int)qg('copy'));








>
|
|


>
|
|


>




>
|



>




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

>
>
>
>
>
>
>
>







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

$lines = [[], []];
$form->runIf(f('lines') !== null, function () use (&$lines) {
	$lines = Transaction::getFormLines();
});

// Quick-fill transaction from query parameters
// 0 = amount, in single currency units
if (qg('0')) {
	$amount = Utils::moneyToInteger(qg('0'));
}

// 00 = Amount, in cents
if (qg('00')) {
	$amount = (int)qg('00');
}

// l = label
if (qg('l')) {
	$transaction->label = qg('l');
}

// dt = date
if (qg('dt')) {
	$transaction->date = new Date(qg('d'));
}

// t = type
if (qg('t')) {
	$transaction->type = (int) qg('t');
}

// ab = Bank/cash account
if (qg('ab') && ($a = $accounts->getWithCode(qg('ab')))
	&& in_array($a->type, [$a::TYPE_BANK, $a::TYPE_CASH, $a::TYPE_OUTSTANDING])) {
	$transaction->setDefaultAccount($transaction::TYPE_REVENUE, 'debit', $a->id);
	$transaction->setDefaultAccount($transaction::TYPE_EXPENSE, 'credit', $a->id);
	$transaction->setDefaultAccount($transaction::TYPE_TRANSFER, 'debit', $a->id);
}

// ar = Revenue account
if (qg('ar') && ($a = $accounts->getWithCode(qg('ar')))
	&& $a->type == $a::TYPE_REVENUE) {
	$transaction->setDefaultAccount($transaction::TYPE_REVENUE, 'credit', $a->id);
	$transaction->setDefaultAccount($transaction::TYPE_CREDIT, 'credit', $a->id);
}

// ae = Expense account
if (qg('ae') && ($a = $accounts->getWithCode(qg('ae')))
	&& $a->type == $a::TYPE_REVENUE) {
	$transaction->setDefaultAccount($transaction::TYPE_EXPENSE, 'debit', $a->id);
	$transaction->setDefaultAccount($transaction::TYPE_DEBT, 'debit', $a->id);
}

// at = Transfer account
if (qg('at') && ($a = $accounts->getWithCode(qg('at')))
	&& $a->type == $a::TYPE_BANK) {
	$transaction->setDefaultAccount($transaction::TYPE_TRANSFER, 'credit', $a->id);
}

// a3 = Third-party account
if (qg('a3') && ($a = $accounts->getWithCode(qg('a3')))
	&& $a->type == $a::TYPE_BANK) {
	$transaction->setDefaultAccount($transaction::TYPE_CREDIT, 'debit', $a->id);
	$transaction->setDefaultAccount($transaction::TYPE_DEBT, 'credit', $a->id);
}


$types_details = $transaction->getTypesDetails();

// Duplicate transaction
if (qg('copy')) {
	$old = Transactions::get((int)qg('copy'));

Added src/www/admin/acc/transactions/pending.php version [a795ae2cc4].





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace Garradin;

use Garradin\Accounting\Transactions;
use Garradin\Entities\Accounting\Transaction;

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

$list = Transactions::listPendingCreditAndDebtForClosedYears();
$list->loadFromQueryString();

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

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

Modified src/www/admin/acc/years/balance.php from [448fd1291b] to [da15f83422].

25
26
27
28
29
30
31


32
33
34
35
36
37
38
$csrf_key = 'acc_years_balance_' . $year->id();
$accounts = $year->accounts();

$form->runIf('save', function () use ($year, $session) {
	$db = DB::getInstance();
	// Fail everything if appropriation failed
	$db->begin();



	$transaction = new Transaction;
	$transaction->id_creator = Session::getUserId();
	$transaction->importFromBalanceForm($year);
	$transaction->save();

	if (f('appropriation')) {







>
>







25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$csrf_key = 'acc_years_balance_' . $year->id();
$accounts = $year->accounts();

$form->runIf('save', function () use ($year, $session) {
	$db = DB::getInstance();
	// Fail everything if appropriation failed
	$db->begin();

	$year->deleteOpeningBalance();

	$transaction = new Transaction;
	$transaction->id_creator = Session::getUserId();
	$transaction->importFromBalanceForm($year);
	$transaction->save();

	if (f('appropriation')) {
137
138
139
140
141
142
143

144
145
146
147

if (!empty($_POST['lines']) && is_array($_POST['lines'])) {
	$lines = Transaction::getFormLines();
}

$appropriation_account = $accounts->getSingleAccountForType(Account::TYPE_APPROPRIATION_RESULT);
$can_appropriate = $accounts->getIdForType(Account::TYPE_NEGATIVE_RESULT) && $accounts->getIdForType(Account::TYPE_POSITIVE_RESULT);


$tpl->assign(compact('lines', 'years', 'chart_change', 'previous_year', 'year_selected', 'year', 'csrf_key', 'can_appropriate', 'appropriation_account'));

$tpl->display('acc/years/balance.tpl');







>

|


139
140
141
142
143
144
145
146
147
148
149
150

if (!empty($_POST['lines']) && is_array($_POST['lines'])) {
	$lines = Transaction::getFormLines();
}

$appropriation_account = $accounts->getSingleAccountForType(Account::TYPE_APPROPRIATION_RESULT);
$can_appropriate = $accounts->getIdForType(Account::TYPE_NEGATIVE_RESULT) && $accounts->getIdForType(Account::TYPE_POSITIVE_RESULT);
$has_balance = $year->hasOpeningBalance();

$tpl->assign(compact('lines', 'years', 'chart_change', 'previous_year', 'year_selected', 'year', 'csrf_key', 'can_appropriate', 'appropriation_account', 'has_balance'));

$tpl->display('acc/years/balance.tpl');

Modified src/www/admin/acc/years/export.php from [f39aed5a9b] to [fe19ac4b6a].

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

use Garradin\Accounting\Export;
use Garradin\Accounting\Years;

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

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

$year_id = (int) qg('year') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);








<
<







1
2
3
4
5
6
7
8


9
10
11
12
13
14
15
<?php
namespace Garradin;

use Garradin\Accounting\Export;
use Garradin\Accounting\Years;

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



$year_id = (int) qg('year') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);

Added src/www/admin/config/ext/delete.php version [9d0842a19f].



























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
namespace Garradin;

use Garradin\UserTemplate\Modules;
use Garradin\Plugins;

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

$csrf_key = 'ext_delete';
$plugin = $module = null;

if (qg('plugin')) {
	$plugin = Plugins::get(qg('plugin'));

	$form->runIf(f('delete') && f('confirm_delete'), function () use ($plugin) {
		$plugin->delete();
	}, $csrf_key, '!config/ext/');
}
else {
	$module = Modules::get(qg('module'));

	$form->runIf(f('delete') && f('confirm_delete'), function () use ($module) {
		$module->delete();
	}, $csrf_key, '!config/ext/');
}

$tpl->assign(compact('plugin', 'module', 'csrf_key'));

$tpl->display('config/ext/delete.tpl');

Added src/www/admin/config/ext/index.php version [e286764cbd].









































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php
namespace Garradin;

use Garradin\UserTemplate\Modules;
use Garradin\Plugins;
use Garradin\Users\Session;

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

$csrf_key = 'ext';
$session = Session::getInstance();

$form->runIf('install', function () {
	Plugins::install(f('install'));
}, $csrf_key, '!config/ext/?focus=' . f('install'));

$form->runIf('enable', function () {
	$m = Modules::get(f('enable'));

	if (!$m) {
		throw new UserException('Ce module n\'existe pas');
	}

	$m->enabled = true;
	$m->save();
}, $csrf_key, '!config/ext/?focus=' . f('enable'));

$form->runIf('disable_module', function () {
	$m = Modules::get(f('disable_module'));

	if (!$m) {
		throw new UserException('Ce module n\'existe pas');
	}

	$m->enabled = false;
	$m->save();
}, $csrf_key, '!config/ext/');

$form->runIf('disable_plugin', function () {
	$p = Plugins::get(f('disable_plugin'));

	if (!$p) {
		throw new UserException('Cette extension n\'existe pas');
	}

	$p->set('enabled', false);
	$p->save();
}, $csrf_key, '!config/ext/');

Modules::refresh();

if (qg('install')) {
	$list = Plugins::listModulesAndPlugins(true);
	$tpl->assign('url_plugins', ENABLE_TECH_DETAILS ? WEBSITE . 'wiki?name=Extensions' : null);
	$tpl->assign('installable', true);
}
else {
	$list = Plugins::listModulesAndPlugins(false);
	$tpl->assign('installable', false);
}

$url_help_modules = sprintf(HELP_PATTERN_URL, 'modules');
$tpl->assign(compact('list', 'csrf_key', 'url_help_modules'));

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

flush();
Plugins::upgradeAllIfRequired();

Deleted src/www/admin/config/modules/index.php version [0741f74399].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
namespace Garradin;

use Garradin\UserTemplate\Modules;

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

$form->runIf(qg('enable') !== null, function () {
	$m = Modules::get(qg('enable'));

	if (!$m) {
		throw new UserException('Ce module n\'existe pas');
	}

	$m->enabled = true;
	$m->updateFromJSON(true);
	$m->save();
}, null, '!config/modules/');

$form->runIf(qg('disable') !== null, function () {
	$m = Modules::get(qg('disable'));

	if (!$m) {
		throw new UserException('Ce module n\'existe pas');
	}

	$m->enabled = false;
	$m->save();
}, null, '!config/modules/');

Modules::refresh();

$list = Modules::list();

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

$tpl->display('config/modules/index.tpl');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































Deleted src/www/admin/config/plugins.php version [30e38b80b8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

namespace Garradin;

use Garradin\Users\Session;

require_once __DIR__ . '/_inc.php';

$session = Session::getInstance();
$csrf_key = 'plugins';

$form->runIf('install', function ()  use ($session) {
	Plugin::install(f('plugin'), false);
	$session->set('plugins_menu', null);
}, $csrf_key, '!config/plugins.php');

$form->runIf('delete', function () use ($session) {
	$plugin = new Plugin(qg('delete'));
	$plugin->uninstall();
	$session->set('plugins_menu', null);
}, $csrf_key, '!config/plugins.php');

if (qg('delete')) {
	$plugin = new Plugin(qg('delete'));
	$tpl->assign('plugin', $plugin->getInfos());
	$tpl->assign('delete', true);
}
else {
	$tpl->assign('list_available', Plugin::listDownloaded());
	$tpl->assign('list_installed', Plugin::listInstalled());
}

$tpl->assign('garradin_website', WEBSITE);
$tpl->assign(compact('csrf_key'));

$tpl->display('config/plugins.tpl');

if (Plugin::upgradeAllIfRequired()) {
	$session->set('plugins_menu', null);
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































Modified src/www/admin/index.php from [3fd93a9cbf] to [461052c44c].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
<?php

namespace Garradin;

use Garradin\Web\Web;
use Garradin\Files\Files;
use Garradin\Users\Session;
use Garradin\Entities\Files\File;
use Garradin\UserTemplate\Modules;

require_once __DIR__ . '/_inc.php';

$banner = '';
$session = Session::getInstance();
Plugin::fireSignal('home.banner', ['user' => $session->getUser(), 'session' => $session], $banner);

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

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

$buttons = [];
Plugin::fireSignal('home.button', ['user' => $session->getUser(), 'session' => $session], $buttons);

foreach (Modules::snippets(Modules::SNIPPET_HOME_BUTTON) as $snippet) {
	if (trim($snippet) === '') {
		continue;
	}

	$buttons[] = $snippet;
}

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

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

$tpl->display('index.tpl');
flush();








|





|










|
<
<
<
<
<
<
<
<
<







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

require_once __DIR__ . '/_inc.php';

$banner = '';
$session = Session::getInstance();
Plugins::fireSignal('home.banner', ['user' => $session->getUser(), 'session' => $session], $banner);

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

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

$buttons = Plugins::listModulesAndPluginsHomeButtons($session);










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

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

$tpl->display('index.tpl');
flush();

Modified src/www/admin/install.php from [9dea1c90c7] to [b9289a2728].

1
2
3
4
5


6
7
8
9
10
11
12
13






14
15
16
17
18




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





















38
<?php
namespace Garradin;

use Garradin\Users\Session;
use Garradin\Entities\Accounting\Chart;



const INSTALL_PROCESS = true;

require_once __DIR__ . '/../../include/test_required.php';
require_once __DIR__ . '/../../include/init.php';

if (file_exists(DB_FILE))
{






    throw new UserException('Garradin est déjà installé');
}

Install::checkAndCreateDirectories();
Install::checkReset();





function f($key)
{
    return \KD2\Form::get($key);
}

$tpl = Template::getInstance();
$tpl->assign('admin_url', ADMIN_URL);

$form = new Form;
$tpl->assign_by_ref('form', $form);

$form->runIf('save', function () {
    Install::installFromForm();
    Session::getInstance()->forceLogin(1);
}, 'install', ADMIN_URL);

$tpl->assign('countries', Chart::COUNTRY_LIST);






















$tpl->display('install.tpl');





>
>






|
|
>
>
>
>
>
>
|




>
>
>
>



|









|
|




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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php
namespace Garradin;

use Garradin\Users\Session;
use Garradin\Entities\Accounting\Chart;
use Garradin\UserTemplate\Modules;
use Garradin\Plugins;

const INSTALL_PROCESS = true;

require_once __DIR__ . '/../../include/test_required.php';
require_once __DIR__ . '/../../include/init.php';

$exists = file_exists(DB_FILE);

if ($exists && !filesize(DB_FILE)) {
	@unlink(DB_FILE);
	$exists = false;
}

if ($exists) {
	throw new UserException('Garradin est déjà installé');
}

Install::checkAndCreateDirectories();
Install::checkReset();

if (DISABLE_INSTALL_FORM) {
	throw new \RuntimeException('Install form has been disabled');
}

function f($key)
{
	return \KD2\Form::get($key);
}

$tpl = Template::getInstance();
$tpl->assign('admin_url', ADMIN_URL);

$form = new Form;
$tpl->assign_by_ref('form', $form);

$form->runIf('save', function () {
	Install::installFromForm();
	Session::getInstance()->forceLogin(1);
}, 'install', ADMIN_URL);

$tpl->assign('countries', Chart::COUNTRY_LIST);

$modules = Modules::listLocal();
$plugins = Plugins::listInstallable(false);

$installable = [];

foreach (Install::DEFAULT_PLUGINS as $plugin) {
	if (array_key_exists($plugin, $plugins)) {
		$installable[$plugin] = ['plugin' => $plugins[$plugin]];
	}
}

foreach (Install::DEFAULT_MODULES as $module) {
	if (array_key_exists($module, $modules)) {
		$installable[$module] = ['module' => $modules[$module]];
	}
}

ksort($installable);

$tpl->assign('installable', $installable);

$tpl->display('install.tpl');

Modified src/www/admin/services/user/_form.php from [148f0505cd] to [b8dd314a46].

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

if (!defined('\Garradin\ROOT')) {
	die();
}

assert(isset($tpl, $form_url, $create));

$current_only = f('past_services') ? 0 : 1;

// If there is only one user selected we can calculate the amount
$single_user_id = isset($users) && count($users) == 1 ? key($users) : null;
$copy_service ??= null;
$copy_service_only_paid ??= null;
$users ??= null;

$grouped_services = Services::listGroupedWithFees($single_user_id, $current_only);

if (!count($grouped_services)) {
	$current_only = false;
	$grouped_services = Services::listGroupedWithFees($single_user_id, $current_only);
}

if (!isset($count_all)) {
	$count_all = Services::count();
}

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







|







|



|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

if (!defined('\Garradin\ROOT')) {
	die();
}

assert(isset($tpl, $form_url, $create));

$current_only = !f('past_services');

// If there is only one user selected we can calculate the amount
$single_user_id = isset($users) && count($users) == 1 ? key($users) : null;
$copy_service ??= null;
$copy_service_only_paid ??= null;
$users ??= null;

$grouped_services = Services::listGroupedWithFees($single_user_id, (int)$current_only);

if (!count($grouped_services)) {
	$current_only = false;
	$grouped_services = Services::listGroupedWithFees($single_user_id, (int)$current_only);
}

if (!isset($count_all)) {
	$count_all = Services::count();
}

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

Modified src/www/admin/services/user/edit.php from [7234f72cb7] to [059c1bf209].

19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
$form_url = sprintf('edit.php?id=%d&', $su->id());
$create = false;

require __DIR__ . '/_form.php';

$form->runIf('save', function () use ($su) {
	$su->importForm();

	$su->save();
}, $csrf_key, ADMIN_URL . 'services/user/?id=' . $su->id_user);

$service_user = $su;

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

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







>








19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$form_url = sprintf('edit.php?id=%d&', $su->id());
$create = false;

require __DIR__ . '/_form.php';

$form->runIf('save', function () use ($su) {
	$su->importForm();
	$su->updateExpectedAmount();
	$su->save();
}, $csrf_key, ADMIN_URL . 'services/user/?id=' . $su->id_user);

$service_user = $su;

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

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

Modified src/www/admin/static/print.css from [4ab93bb3b2] to [fd9783217b].

85
86
87
88
89
90
91




92
93
94
95
96
97
98
#rapport .parent {
    background: #ccc;
}

.noprint {
    display: none;
}





td.actions *, nav.tabs, .icn-btn, .pagination, a.icn {
    display: none !important;
}

td.num a {
    border: none;







>
>
>
>







85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#rapport .parent {
    background: #ccc;
}

.noprint {
    display: none;
}

.print-only {
    display: unset;
}

td.actions *, nav.tabs, .icn-btn, .pagination, a.icn {
    display: none !important;
}

td.num a {
    border: none;

Modified src/www/admin/static/scripts/accounting.js from [93a05e165e] to [7039cc2124].

1

























2
3
4
5
6
7
8
function initTransactionForm(is_new) {

























	// Advanced transaction: line management
	var lines = $('.transaction-lines tbody tr');

	function initLine(row) {
		var removeBtn = row.querySelector('button[name="remove_line"]');
		removeBtn.onclick = () => {
			var count = $('.transaction-lines tbody tr').length;

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







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function initTransactionForm(is_new) {
	var form = $('form')[0];
	// Check if an account is listed twice and ask for confirmation
	form.addEventListener('submit', (e) => {
		var accounts = [];
		var lines = $('.transaction-lines tbody tr');

		for (var i = 0; i < lines.length; i++) {
			var a = lines[i].querySelector('.input-list input[type="hidden"]');

			if (!a) {
				continue;
			}

			if (accounts.includes(a.value)
				&& !window.confirm(`Attention, cette écriture affecte deux fois le même compte (${a.value}). Confirmer ?`)) {
				e.preventDefault();
				return false;
			}

			accounts.push(a.value);
		}

		return true;
	});

	// Advanced transaction: line management
	var lines = $('.transaction-lines tbody tr');

	function initLine(row) {
		var removeBtn = row.querySelector('button[name="remove_line"]');
		removeBtn.onclick = () => {
			var count = $('.transaction-lines tbody tr').length;

Modified src/www/admin/static/scripts/code_editor.css from [e8169a1071] to [5b73a86043].

1

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

14
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
.codeEditor {

	width: 100%;
	height: 600px;
	border: 1px solid var(--gBorderColor);
	background: var(--gLightBackgroundColor);
	position: relative;
	display: block;
}

.codeEditor .sk_help {
	background: var(--gLightBorderColor);
	border-top: 2px solid var(--gBorderColor);
	position: absolute;

	left: 0;
	right: 0;
	bottom: 0;
	height: 15px;
	padding: 5px 1em 0;
	font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace;
	font-size: 12px;
}

.codeEditor .sk_toolbar {
	background: var(--gLightBorderColor);
	border-bottom: 2px solid var(--gBorderColor);
	height: 32px;
}



.codeEditor .sk_toolbar select {
	float: right;
	border: none;
	border-radius: .2em;
	height: 24px;
	padding: 1px;
	padding-left: 24px;
	margin: 4px .5em;
	background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEUAAACZm4leYFyOkX1RU09MTkp9f2+ys6KwsZrn5+KanIz///+6u6vGxryDhXtVV1Otpz1oAAAAA3RSTlMA+7omiqY6AAAAgklEQVQI12NI+f9fgQEEfO7e+QBm/BGU+P8fJPrd2LgLCD4AGSCBLCDD8Pfu3bu9QCK/Z86e/esDw4/Hv+/euf0ZyEj7PXPnbCDDI+33kZDQkE8M/11D1969e9eEwRNIlpe332f421Ne0dHRcYnh75k7Z8/cudPEoOIS4uriAlQMAwDpN0taA/g97gAAAABJRU5ErkJggg==") no-repeat 5px center;
	cursor: pointer;
}

.codeEditor .sk_toolbar select:hover {
	background-color: var(--gHoverLinkColor);

}

.codeEditor .sk_toolbar p {
	display: inline;
	padding: .3em .5em;
	border-radius: .5em;
	font-size: .9em;
	margin-left: 2em;
}

.codeEditor .sk_toolbar input {
	margin: 4px .5em;
	padding: 0;
	width: 24px;
	height: 24px;
	border: none;
	border-radius: .2em;
	cursor: pointer;
	text-indent: -70em;
	overflow: hidden;
	background: transparent no-repeat center center;
}

.codeEditor .sk_toolbar input:hover { background-color: var(--gLightBackgroundColor); }

.codeEditor .sk_toolbar .save { margin-left: 2em; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEU6XIthfJfOXAD8rz6PpsDe3t7+/v6YrsZhe5NVV1Py8vL09PTMzc6+0upyn88gSod8oq0HAAAAAXRSTlOZyTXzhgAAAGxJREFUCNdj4P+kBAT6Hxj+XjYGAtv7DH+fbQOCfSBGKBDkgRi7gQAschQoAmbsXrVq1ToQIw0IQGregQGQ8bCjo6OvDsQ4czS04jmQ8eLMzJiDUMYZkMj3ilAIg/9h6PEjJ4AMhp8zgeD/BwBY4VdD5HZlvAAAAABJRU5ErkJggg=="); }
.codeEditor .sk_toolbar .reset { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAABCklEQVR42p2TsUrDUBSGv4RM2qlLXXwBHdoncG3u5tpZx059gojgZAtC3bqqa7cQKH2FWiFZHUSIg5lChi7Xpak312Ma/eHA5Zzz/5fzHw5UMQZ0wxgDOCb5fnQ8Glxe0AR31zdcPW0mpoDOkoDPJKRI41rywdEpRRrTHeZ4drFIY15nh5w/f4jkea/D2aPafeJKTVuyI4Ut7G0NkVDmnbpxXMAHyJJgl1xPW+XT32emC0SA3z75FugO85Ic8Qf0jR33a/p0lgR6PW1pQJtbiPbNK8GzDLPhNBVAKVUphGGIJD7vdWQBi1SBuSGA/P1FFvgNb8vbH7nFaoO5Ja2Uki6Ommt8ME36t4lfRLtlZDAJ4ScAAAAASUVORK5CYII="); }
.codeEditor .sk_toolbar .search { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAAK/INwWK6QAAADBQTFRF////epWwPGiTdYKPdHqB1tbWWW6Hp6enR2yQc6b/dqncME9tmcz/i5CX5e32w9PmM3gGSwAAAAd0Uk5TAAAAQoTswgXk1s4AAABySURBVAjXYxA0v+4syAAEIqXvQh1BDPN3J88Vgxj3/r07EwtmnDw58y2YcebMHLBI6p/358NADN/3Z75eFhQUZFC5/i4WqIaRwcjtdsrWa0AGEDByr2oAM5i4V82AMBh2nGmCMIBCEAbDrhVghpKS9jYA43soYFw0gPcAAAAASUVORK5CYII="); }
.codeEditor .sk_toolbar .search_replace { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEUAAABnVEEAAACEhoVsaWX4liHmz2/8/Pzf398uNDawrKfn5+fOXADj4+P+/v6IioXFYmG9AAAAAnRSTlMAWv0tddIAAACKSURBVAjXY2Dg/w8EAgwMDP/evSuvWwhi7N69Y8UrATDjxurbC4CMn/P3rv9vwMDw3+/J31s9BxkYOl/vm3vjaCiEsSc0hoGhxe+J7dHUgwxckz0nAwUCGDotO3qPph4zYJg8q+P20Zg+AwbJ9rt3VE60f2BgML+76VgHiPH93bt3z8uBjP9g8AEArPJMCYP5JmIAAAAASUVORK5CYII="); }
.codeEditor .sk_toolbar .gotoline { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEX///9BQUEVFRWysrREREQ4ODhVVVX7+/vu7u7q6uqcnJy+vr/m5ub19fXc3Nz///89lwBDAAAAB3RSTlMAAILOakNmMhUGwgAAAHtJREFUCNdjYIABQbE0EEhkEMx793r37tdmDILZu1fv3rVqEYNgfv3/u/X/vwEZu4/Xl5eXARnbd96trS1hcPNaWdd7924Jg+///7UdHR1XGIK01lSfnDmzhUFQY8W6N2fOABl+q/a8OXfmCYOg3jugwJlJDIJKEMCAAQCfIDck8YzyWAAAAABJRU5ErkJggg=="); }
.codeEditor .sk_toolbar .fullscreen { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABMTFFOTlBOTlCQ1uMHAAAAA3RSTlMAOcKBmOr4AAAAQUlEQVQI12P4/4D7P8N/B0Yo8XsC23+GZw+4pzM4TmBzYsAGgBKODNcecM9m+D+B7T3DPwfG/Qz/G5iABlzg+g8ANzMax/3kkQoAAAAASUVORK5CYII="); }
.codeEditor.fullscreen .sk_toolbar .fullscreen { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABNTVFOTlBOTlBLB/faAAAAA3RSTlMAOsPdsomtAAAAQElEQVQI12NIEWCRZNi5gTePIe8D/08G6Q/8Txj0P/B/YVj/gf8fAzawHyQhD1ICJvI/8O9k2FnAl8eQosAiCQCgixb13aKGIwAAAABJRU5ErkJggg=="); }

.codeEditor .lineCount, .codeEditor textarea {
	font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace;
	font-size: 11pt;
	line-height: 11pt;
}
















.codeEditor .lineCount {
	position: absolute;
	top: 34px;
	left: 0;
	bottom: 22px;
	width: 46px;
	text-align: right;
	border-right: 2px solid var(--gLightBorderColor);
	overflow: hidden;

}

.codeEditor .lineCount i {
	display: block;
	padding-right: 2px;
	font-weight: normal;
}

.codeEditor .lineCount b {
	display: block;
	padding-right: 2px;
	font-weight: normal;
}

.codeEditor .lineCount b.current {
	background: var(--gLightBorderColor);

}

.codeEditor .container {
	position: absolute;
	right: 4px;
	top: 34px;
	bottom: 22px;
	left: 50px;
	margin: 0;
	padding: 0;
}

.codeEditor textarea {
	height: 100%;
	width: 100%;
	padding: 0 0 0 2px;
	margin: 0;
	background: transparent;


	border: none;
	overflow: auto;
	resize: none;

}

.codeEditor.fullscreen {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	z-index: 100000;
}

>

<








|

>



|
|

<



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


|
<
>










|

<
<
<
<
<
<
<
<
<

<
<
<
<
<
<
<
<
<
<






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



|

|


|

>















|
>





|
|











>
>



>












1
2
3

4
5
6
7
8
9
10
11
12
13
14
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
.codeEditor {
	min-height: 600px;
	width: 100%;

	border: 1px solid var(--gBorderColor);
	background: var(--gLightBackgroundColor);
	position: relative;
	display: block;
}

.codeEditor .sk_help {
	background: var(--gLightBorderColor);
	border-top: .2rem solid var(--gBorderColor);
	position: absolute;
	font-size: .9em;
	left: 0;
	right: 0;
	bottom: 0;
	height: 1.2rem;
	padding: .3rem 1rem 0;
	font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace;

}

.codeEditor .sk_toolbar {

	border-bottom: .2em solid var(--gBorderColor);
	display: flex;

	flex-direction: row;
	justify-content: space-between;
	position: absolute;
	top: 0;
	left: 0;
	right: 0;

	height: 2.5em;
	padding: 0;




}

.codeEditor .sk_toolbar button:first-child {

	font-weight: bold;
}

.codeEditor .sk_toolbar p {
	display: inline;
	padding: .3em .5em;
	border-radius: .5em;
	font-size: .9em;
	margin-left: 2em;
}

.codeEditor .sk_toolbar button {
	margin: 4px .5em;









}











.codeEditor .lineCount, .codeEditor textarea {
	font-family: "Deja Vu Sans Mono", "Droid Sans Mono", "Courier New", Courier, monospace;
	font-size: 11pt;
	line-height: 11pt;
}

.codeEditor .editor {
	position: absolute;
	top: calc(2.7em);
	height: calc(100% - 4.2em);
	bottom: calc(1.6em + .4em);
	left: 0;
	right: 0;
	background: #333;
}

.codeEditor .container {
	position: relative;
	display: block;
}

.codeEditor .lineCount {
	position: absolute;
	top: 0;
	left: 0;
	bottom: 0;
	width: 46px;
	text-align: right;
	border-right: 2px solid #666;
	overflow: hidden;
	color: #999;
}

.codeEditor .lineCount i {
	display: block;
	padding-right: 2px;
	font-weight: normal;
}

.codeEditor .lineCount b {
	display: block;
	padding-right: 2px;
	font-weight: normal;
}

.codeEditor .lineCount b.current {
	background: #666;
	color: #fff;
}

.codeEditor .container {
	position: absolute;
	right: 4px;
	top: 0;
	bottom: 0;
	left: 50px;
	margin: 0;
	padding: 0;
}

.codeEditor textarea {
	height: 100%;
	width: 100%;
	padding: 0 0 0 2px;
	margin: 0;
	background: transparent;
	color: #fff;
	border-radius: none;
	border: none;
	overflow: auto;
	resize: none;
	box-shadow: none;
}

.codeEditor.fullscreen {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	width: 100%;
	height: 100%;
	z-index: 100000;
}

Modified src/www/admin/static/scripts/code_editor.js from [8da523e07f] to [863d8d32d6].

1
2
3
4
5

6
7
8
9
10
11
12
13
14
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
(function (){
	g.style('scripts/code_editor.css');
	g.script('scripts/lib/text_editor.min.js', () => {
	g.script('scripts/lib/code_editor.min.js', function ()
	{

		var save_btn = document.querySelector('[name=save]');
		var code = new codeEditor('f_content');

		code.params.lang = {
			search: "Texte à chercher ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace: "Texte pour le remplacement ?\n(utiliser $1, $2... pour les captures d'expression régulière)",
			search_selection: "Texte à chercher dans la sélection ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace_result: "%d occurences trouvées et remplacées.",
			goto: "Aller à la ligne :",
			no_search_result: "Aucun résultat trouvé."
		};

		code.origValue = code.textarea.value;
		code.saved = true;















































		code.saveFile = function ()
		{

			save_btn.click();







































		};

		code.resetFile = function (e)
		{
			if (this.textarea.value == this.origValue) return;
			if (!window.confirm("Le fichier a été modifié, abandonner les modifications ?")) return;
			this.textarea.form.reset();
		};

		var help = document.createElement('div');
		help.className = 'sk_help';

		code.parent.appendChild(help);

		var toolbar = document.createElement('nav');
		toolbar.className = 'sk_toolbar';

		var appendButton = function (name, title, action)
		{
			var btn = document.createElement('input');
			btn.type = 'button';

			btn.value = btn.title = title;

			btn.className = name;

			btn.onclick = function () { action.call(code); return false; };

			toolbar.appendChild(btn);
		};

		appendButton('save', 'Enregistrer les modifications', code.saveFile);
		appendButton('reset', 'Recharger le fichier (effacer les modifications)', code.resetFile);

		appendButton('search', 'Chercher', code.search);
		appendButton('search_replace', 'Chercher et remplacer', code.searchAndReplace);
		appendButton('gotoline', 'Aller à la ligne', code.goToLine);

		code.parent.insertBefore(toolbar, code.parent.firstChild);

		code.shortcuts.push({ctrl: true, key: 's', callback: code.saveFile});

		// Cancel Escape to close
		if (window.parent && window.parent.g.dialog) {





>








|






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


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

















|

|

>
|
>
|
>
|




|
|

|
|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
(function (){
	g.style('scripts/code_editor.css');
	g.script('scripts/lib/text_editor.min.js', () => {
	g.script('scripts/lib/code_editor.min.js', function ()
	{
		const doc_url = 'https://fossil.kd2.org/paheko/wiki?name=Documentation/';
		var save_btn = document.querySelector('[name=save]');
		var code = new codeEditor('f_content');

		code.params.lang = {
			search: "Texte à chercher ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace: "Texte pour le remplacement ?\n(utiliser $1, $2... pour les captures d'expression régulière)",
			search_selection: "Texte à chercher dans la sélection ?\n(expression régulière autorisée, pour cela commencer par un slash '/')",
			replace_result: "%d occurences trouvées et remplacées.",
			goto: "Aller à la ligne numéro :",
			no_search_result: "Aucun résultat trouvé."
		};

		code.origValue = code.textarea.value;
		code.saved = true;

		code.onlinechange = function () {
			if (!this.textarea.value.match(/\{\{/)) {
				return;
			}

			if ((p = this.parent.querySelector('nav p')) && this.origValue != code.textarea.value)
			{
				toolbar.removeChild(p);
			}

			var line = this.getLine(this.current_line);
			var doc = [{link: 'Brindille', title: 'Brindille'}];

			if (match = line.match(/\{\{:(\w+)/)) {
				doc.push({link: 'Brindille/Fonctions', title: 'Fonction'});
				doc.push({link: 'Brindille/Fonctions#'+match[1], title: match[1]});
			}
			else if (match = line.match(/\{\{#(\w+)/)) {
				doc.push({link: 'Brindille/Sections', title: 'Section'});
				doc.push({link: 'Brindille/Sections#'+match[1], title: match[1]});
			}
			else if (match = line.match(/\{\{(select)/)) {
				doc.push({link: 'Brindille/Sections', title: 'Section'});
				doc.push({link: 'Brindille/Sections#'+match[1], title: match[1]});
			}
			else if (match = line.match(/\|(\w+)/)) {
				doc.push({link: 'Brindille/Filtres', title: 'Filtre'});
				doc.push({link: 'Brindille/Filtres#'+match[1], title: match[1]});
			}

			help.innerHTML = 'Documentation';

			for (var i = 0; i < doc.length; i++)
			{
				help.innerHTML += ' &gt; ';

				if (doc[i].link)
					help.innerHTML += '<a href="' + doc_url + doc[i].link + '" onclick="return !window.open(this.href);">' + doc[i].title + '</a>';
				else if (doc[i].tag)
					help.innerHTML += '<' + tag + '>' + doc[i].title + '</' + tag + '>';
				else
					help.innerHTML += doc[i].title;
			}		return false;

		};

		code.saveFile = function ()
		{
			const data = new URLSearchParams();

			for (const pair of new FormData(this.textarea.form)) {
				data.append(pair[0], pair[1]);
			}

			data.append('save', 1);
			this.textarea.form.classList.add('progressing');

			fetch(this.textarea.form.action + '&js', {
				method: 'post',
				body: data,
			}).then((response) => response.json())
			.then(data => {
				this.textarea.defaultValue = this.textarea.value;

				// Show saved
				let c = document.createElement('p');
				c.className = 'block confirm';
				c.id = 'confirm_saved';
				c.innerText = 'Enregistré';
				c.style.left = '-100%';
				c.style.opacity = '1';
				c.onclick = () => c.remove();

				document.querySelector('.codeEditor').appendChild(c);

				window.setTimeout(() => {
					c.style.left = '';
					this.textarea.form.classList.remove('progressing');
				}, 200);

				window.setTimeout(() => {
					c.style.opacity = 0;
				}, 3000);

				window.setTimeout(() => {
					c.remove();
				}, 5000);
			}).catch(e => { console.log(e); this.textarea.form.querySelector('[type=submit]').click(); } );
			return true;
		};

		code.resetFile = function (e)
		{
			if (this.textarea.value == this.origValue) return;
			if (!window.confirm("Le fichier a été modifié, abandonner les modifications ?")) return;
			this.textarea.form.reset();
		};

		var help = document.createElement('div');
		help.className = 'sk_help';

		code.parent.appendChild(help);

		var toolbar = document.createElement('nav');
		toolbar.className = 'sk_toolbar';

		var appendButton = function (icon, label, title, action)
		{
			var btn = document.createElement('button');
			btn.type = 'button';
			btn.innerText = label;
			btn.title = title;
			if (icon) {
				btn.setAttribute('data-icon', icon);
			}
			btn.onclick = () => { action.call(code); return false; };

			toolbar.appendChild(btn);
		};

		appendButton('→', 'Enregistrer', 'Enregistrer les modifications', code.saveFile);
		appendButton('🗘', 'Recharger', 'Recharger le fichier (effacer les modifications)', code.resetFile);

		appendButton('🔍', 'Chercher', 'Chercher', code.search);
		appendButton(null, 'Remplacer', 'Chercher et remplacer', code.searchAndReplace);
		appendButton(null, 'Aller à la ligne', 'Aller à la ligne', code.goToLine);

		code.parent.insertBefore(toolbar, code.parent.firstChild);

		code.shortcuts.push({ctrl: true, key: 's', callback: code.saveFile});

		// Cancel Escape to close
		if (window.parent && window.parent.g.dialog) {
84
85
86
87
88
89
90
91
92
93
94
95
96
					code.saveFile();
				}

				return false;
			};
		}
		else {
			appendButton('fullscreen', 'Plein écran', code.toggleFullscreen);
		}

		g.setParentDialogHeight('90%');
	})});
}());







|





174
175
176
177
178
179
180
181
182
183
184
185
186
					code.saveFile();
				}

				return false;
			};
		}
		else {
			appendButton(null, 'Plein écran', 'Plein écran', code.toggleFullscreen);
		}

		g.setParentDialogHeight('90%');
	})});
}());

Modified src/www/admin/static/scripts/lib/code_editor.min.js from [206f11e50f] to [b93b17e625].

1
!function(){function t(){}var e;String.prototype.repeat=function(t){return new Array(t+1).join(this)},window.codeEditor=function(t){if(!textEditor.call(this,t))return!1;this.onlinechange=null,this.onlinenumberchange=null,this.fullscreen=!1,this.nb_lines=0,this.current_line=0,this.search_str=null,this.search_pos=0,this.params={indent_size:4,lang:{search:"Text to search?\n(regexps allowed, begin them with '/')",replace:"Text for replacement?\n(use $1, $2... for regexp replacement)",search_selection:"Text to replace in selection?\n(regexps allowed, begin them with '/')",replace_result:"%d occurence found and replaced.",goto:"Line to go to:",no_search_result:"No search result found."}},(that=this).init(),this.textarea.spellcheck=!1,this.shortcuts.push({shift:!0,key:"tab",callback:this.indent}),this.shortcuts.push({key:"tab",callback:this.indent}),this.shortcuts.push({ctrl:!0,key:"f",callback:this.search}),this.shortcuts.push({ctrl:!0,key:"h",callback:this.searchAndReplace}),this.shortcuts.push({ctrl:!0,key:"g",callback:this.goToLine}),this.shortcuts.push({key:"F3",callback:this.searchNext}),this.shortcuts.push({key:"backspace",callback:this.backspace}),this.shortcuts.push({key:"enter",callback:this.enter}),this.shortcuts.push({key:'"',callback:this.insertBrackets}),this.shortcuts.push({key:"'",callback:this.insertBrackets}),this.shortcuts.push({key:"[",callback:this.insertBrackets}),this.shortcuts.push({key:"{",callback:this.insertBrackets}),this.shortcuts.push({key:"(",callback:this.insertBrackets}),this.shortcuts.push({key:"F11",callback:this.toggleFullscreen}),this.textarea.addEventListener("keypress",this.keyEvent.bind(this),!0),this.textarea.addEventListener("keydown",this.keyEvent.bind(this),!0)},codeEditor.prototype=(e=textEditor.prototype,t.prototype=e,new t),codeEditor.prototype.init=function(){var t=this;for(this.nb_lines=this.countLines(),this.parent=document.createElement("div"),this.parent.className="codeEditor",this.lineCounter=document.createElement("span"),this.lineCounter.className="lineCount",i=1;i<=this.nb_lines;i++)this.lineCounter.innerHTML+="<b>"+i+"</b>";this.lineCounter.innerHTML+="<i>---</i>",this.parent.appendChild(this.lineCounter);var e=document.createElement("div");e.className="container",e.appendChild(this.textarea.cloneNode(!0)),this.parent.appendChild(e);var s=this.textarea.parentNode;s.appendChild(this.parent),s.removeChild(this.textarea),this.textarea=this.parent.getElementsByTagName("textarea")[0],this.textarea.wrap="off",this.textarea.style="tab-size: "+this.params.indent_size;e=(this.textarea.value.match(/^\t/gm)||[]).length,s=new RegExp("^[ ]{"+this.params.indent_size+"}","mg"),s=(this.textarea.value.match(s)||[]).length;this.indent_pattern=e<s?" ".repeat(this.params.indent_size):"\t",this.textarea.addEventListener("focus",function(){t.update()},!1),this.textarea.addEventListener("keyup",function(){t.update()},!1),this.textarea.addEventListener("click",function(){t.update()},!1),this.textarea.addEventListener("scroll",function(){t.lineCounter.scrollTop=t.textarea.scrollTop},!1)},codeEditor.prototype.update=function(){var t=this.getSelection(),e=this.getLineNumberFromPosition(t),s=this.countLines();if(this.search_pos=t.end,s!=this.nb_lines){for(var i=this.lineCounter.getElementsByTagName("b"),r=this.nb_lines;s<r;r--)this.lineCounter.removeChild(i[r-1]);for(var n=this.lineCounter.lastChild,r=i.length;r<s;r++){var a=document.createElement("b");a.innerHTML=r+1,this.lineCounter.insertBefore(a,n)}this.nb_lines=s,"function"==typeof this.onlinenumberchange&&this.onlinenumberchange.call(this)}if(e!=this.current_line){for(i=this.lineCounter.getElementsByTagName("b"),r=0;r<this.nb_lines;r++)i[r].className="";i[e].className="current",this.current_line=e,"function"==typeof this.onlinechange&&this.onlinechange.call(this)}},codeEditor.prototype.countLines=function(){var t=this.textarea.value.match(/(\r?\n)/g);return t?t.length+1:1},codeEditor.prototype.getLineNumberFromPosition=function(t){if(0==(t=t||this.getSelection()).start)return 0;t=this.textarea.value.substr(0,t.start).match(/(\r?\n)/g);return t?t.length:0},codeEditor.prototype.getLines=function(){return this.textarea.value.split("\n")},codeEditor.prototype.getLine=function(t){return this.textarea.value.split("\n",t+1)[t]},codeEditor.prototype.getLinePosition=function(t,e){var s=0;for(i=0;i<t.length;i++){if(i==e)return{start:s+i,end:s+t[i].length,length:t[i].length,text:t[i]};s+=t[i].length}return!1},codeEditor.prototype.selectLines=function(t){for(var e=t.start;0<e;e--)if("\n"==this.textarea.value.substr(e,1)){t.start=e+1;break}for(e=t.end-1;e<this.textarea.length;e++)if("\n"==this.textarea.value.substr(e,1)){t.end=e-1;break}return this.setSelection(t.start,t.end),t},codeEditor.prototype.goToLine=function(t){var e=window.prompt(that.params.lang.goto);if(e){e=this.textarea.value.split("\n",parseInt(e,10)).join("\n").length;return this.scrollToSelection(this.setSelection(e,e)),!0}},codeEditor.prototype.indent=function(t,e){var s=this.getSelection(),i=t.shiftKey,r=this.getLines(),n=this.getLineNumberFromPosition(s),a=this.getLinePosition(r,n),t=s.end>a.end;if((0==s.length||!t)&&s.start!=a.start)return this.insertAtPosition(s.start,this.indent_pattern),!0;t=new RegExp("^([ ]{"+this.params.indent_size+"}|\t)*");if(0==s.length&&s.start==a.start){var h=n-1 in r&&r[n-1].match(t);return h=h&&0==a.length?this.indent_pattern.repeat(h.length):this.indent_pattern,this.insertAtPosition(s.start,h),!0}s=this.selectLines(s);h=this.textarea.value.substr(s.start,s.end-s.start),r=h.split("\n");if(i)for(var o=new RegExp("^([ ]{"+this.params.indent_size+"}|\t)"),c=0;c<r.length;c++)r[c]=r[c].replace(o,"");else for(c=0;c<r.length;c++)r[c]=""==r[c].replace(/\s+/,"")?"":this.indent_pattern+r[c];return h=r.join("\n"),this.replaceSelection(s,h),!0},codeEditor.prototype.search=function(){if(this.search_str=window.prompt(this.params.lang.search,this.search_str||""))return this.search_pos=0,this.searchNext()},codeEditor.prototype.searchNext=function(){if(!this.search_str)return!0;var t=this.getSelection(),e=t.end>=this.search_pos?this.search_pos:t.start,s=this.textarea.value.substr(e),i=this.getSearchRegexp(this.search_str),r=s.search(i);if(-1==r)return window.alert(this.params.lang.no_search_result);i=s.match(i);return t.start=e+r,t.end=t.start+i[0].length,t.length=i[0].length,t.text=i[0],this.setSelection(t.start,t.end),this.search_pos=t.end,this.scrollToSelection(t),!0},codeEditor.prototype.getSearchRegexp=function(t,e){var s,i;return t="/"==t.substr(0,1)?(s=t.lastIndexOf("/"),i=t.substr(1,s-1),t.substr(s+1).replace(/g/,"")):(i=t.replace(/([\/$^.?()[\]{}\\])/,"\\$1"),"i"),e&&(t+="g"),new RegExp(i,t)},codeEditor.prototype.searchAndReplace=function(t){var e=this.getSelection(),i=0!=e.length?this.params.lang.search_selection:this.params.lang.search;if(!(s=window.prompt(i,this.search_str||""))||!(r=window.prompt(that.params.lang.replace)))return!0;var n,i=this.getSearchRegexp(s,!0);return 0==e.length?(n=this.textarea.value.match(i).length,this.textarea.value=this.textarea.value.replace(i,r)):(n=e.text.match(i).length,this.replaceSelection(e,e.text.replace(i,r))),window.alert(this.params.lang.replace_result.replace(/%d/g,n)),!0},codeEditor.prototype.enter=function(t){var e=this.getSelection();e.start!=e.end&&(this.replaceSelection(e,""),e=this.getSelection());var s=this.getLineNumberFromPosition(e),i="",r=!1,s=this.getLine(s);return"{"==this.textarea.value.substr(e.start-1,1)&&(i+=this.indent_pattern,r="}"==this.textarea.value.substr(e.start,1)),(match=s.match(/^(\s+)/))&&(i+=match[1]),!!i&&(this.insertAtPosition(e.start,"\n"+i),r&&(r=this.getSelection(),this.insertAtPosition(r.start,"\n"+i.substr(0,i.length-this.indent_pattern.length)),this.setSelection(r.start,r.end)),!0)},codeEditor.prototype.backspace=function(t){var e=this.getSelection();if(0<e.length)return!1;if('""'==(s=this.textarea.value.substr(e.start-1,2))||"''"==s||"{}"==s||"()"==s||"[]"==s)return--e.start,e.end+=1,this.replaceSelection(e,""),!0;var s=this.textarea.value.substr(e.start-20,20);return-1!=(pos=s.search(/([ \t]+)$/))&&(e.start-=20-pos,this.replaceSelection(e,""),!0)},codeEditor.prototype.insertBrackets=function(t,e){var s=this.getSelection(),e=e,i=e;switch(e){case"(":i=")";break;case"[":i="]";break;case"{":i="}"}return 0==s.length?this.insertAtPosition(s.start,e+i,s.start+1):this.wrapSelection(s,e,i),!0},codeEditor.prototype.toggleFullscreen=function(t){for(var e=this.parent.className.split(" "),s=0;s<e.length;s++)if("fullscreen"==e[s])return e.splice(s,1),this.parent.className=e.join(" "),!(this.fullscreen=!1);return e.push("fullscreen"),this.parent.className=e.join(" "),this.fullscreen=!0}}();
|
1
!function(){function t(){}var e;String.prototype.repeat=function(t){return new Array(t+1).join(this)},window.codeEditor=function(t){if(!textEditor.call(this,t))return!1;this.onlinechange=null,this.onlinenumberchange=null,this.fullscreen=!1,this.nb_lines=0,this.current_line=0,this.search_str=null,this.search_pos=0,this.params={indent_size:4,lang:{search:"Text to search?\n(regexps allowed, begin them with '/')",replace:"Text for replacement?\n(use $1, $2... for regexp replacement)",search_selection:"Text to replace in selection?\n(regexps allowed, begin them with '/')",replace_result:"%d occurence found and replaced.",goto:"Line to go to:",no_search_result:"No search result found."}},(that=this).init(),this.textarea.spellcheck=!1,this.shortcuts.push({shift:!0,key:"tab",callback:this.indent}),this.shortcuts.push({key:"tab",callback:this.indent}),this.shortcuts.push({ctrl:!0,key:"f",callback:this.search}),this.shortcuts.push({ctrl:!0,key:"h",callback:this.searchAndReplace}),this.shortcuts.push({ctrl:!0,key:"g",callback:this.goToLine}),this.shortcuts.push({key:"F3",callback:this.searchNext}),this.shortcuts.push({key:"backspace",callback:this.backspace}),this.shortcuts.push({key:"enter",callback:this.enter}),this.shortcuts.push({key:'"',callback:this.insertBrackets}),this.shortcuts.push({key:"'",callback:this.insertBrackets}),this.shortcuts.push({key:"[",callback:this.insertBrackets}),this.shortcuts.push({key:"{",callback:this.insertBrackets}),this.shortcuts.push({key:"(",callback:this.insertBrackets}),this.shortcuts.push({key:"F11",callback:this.toggleFullscreen}),this.textarea.addEventListener("keypress",this.keyEvent.bind(this),!0),this.textarea.addEventListener("keydown",this.keyEvent.bind(this),!0)},codeEditor.prototype=(e=textEditor.prototype,t.prototype=e,new t),codeEditor.prototype.init=function(){var t=this;for(this.nb_lines=this.countLines(),this.parent=document.createElement("div"),this.parent.className="codeEditor",this.lineCounter=document.createElement("span"),this.lineCounter.className="lineCount",i=1;i<=this.nb_lines;i++)this.lineCounter.innerHTML+="<b>"+i+"</b>";this.lineCounter.innerHTML+="<i>---</i>";var e=document.createElement("div");e.className="editor",e.appendChild(this.lineCounter);var s=document.createElement("div");s.className="container",s.appendChild(this.textarea.cloneNode(!0)),e.appendChild(s),this.parent.appendChild(e);s=this.textarea.parentNode;s.appendChild(this.parent),s.removeChild(this.textarea),this.textarea=this.parent.getElementsByTagName("textarea")[0],this.textarea.wrap="off",this.textarea.style="tab-size: "+this.params.indent_size;e=(this.textarea.value.match(/^\t/gm)||[]).length,s=new RegExp("^[ ]{"+this.params.indent_size+"}","mg"),s=(this.textarea.value.match(s)||[]).length;this.indent_pattern=e<s?" ".repeat(this.params.indent_size):"\t",this.textarea.addEventListener("focus",function(){t.update()},!1),this.textarea.addEventListener("keyup",function(){t.update()},!1),this.textarea.addEventListener("click",function(){t.update()},!1),this.textarea.addEventListener("scroll",function(){t.lineCounter.scrollTop=t.textarea.scrollTop},!1)},codeEditor.prototype.update=function(){var t=this.getSelection(),e=this.getLineNumberFromPosition(t),s=this.countLines();if(this.search_pos=t.end,s!=this.nb_lines){for(var i=this.lineCounter.getElementsByTagName("b"),r=this.nb_lines;s<r;r--)this.lineCounter.removeChild(i[r-1]);for(var n=this.lineCounter.lastChild,r=i.length;r<s;r++){var a=document.createElement("b");a.innerHTML=r+1,this.lineCounter.insertBefore(a,n)}this.nb_lines=s,"function"==typeof this.onlinenumberchange&&this.onlinenumberchange.call(this)}if(e!=this.current_line){for(i=this.lineCounter.getElementsByTagName("b"),r=0;r<this.nb_lines;r++)i[r].className="";i[e].className="current",this.current_line=e,"function"==typeof this.onlinechange&&this.onlinechange.call(this)}},codeEditor.prototype.countLines=function(){var t=this.textarea.value.match(/(\r?\n)/g);return t?t.length+1:1},codeEditor.prototype.getLineNumberFromPosition=function(t){if(0==(t=t||this.getSelection()).start)return 0;t=this.textarea.value.substr(0,t.start).match(/(\r?\n)/g);return t?t.length:0},codeEditor.prototype.getLines=function(){return this.textarea.value.split("\n")},codeEditor.prototype.getLine=function(t){return this.textarea.value.split("\n",t+1)[t]},codeEditor.prototype.getLinePosition=function(t,e){var s=0;for(i=0;i<t.length;i++){if(i==e)return{start:s+i,end:s+t[i].length,length:t[i].length,text:t[i]};s+=t[i].length}return!1},codeEditor.prototype.selectLines=function(t){for(var e=t.start;0<e;e--)if("\n"==this.textarea.value.substr(e,1)){t.start=e+1;break}for(e=t.end-1;e<this.textarea.length;e++)if("\n"==this.textarea.value.substr(e,1)){t.end=e-1;break}return this.setSelection(t.start,t.end),t},codeEditor.prototype.goToLine=function(t){var e=window.prompt(that.params.lang.goto);if(e){e=this.textarea.value.split("\n",parseInt(e,10)).join("\n").length;return this.scrollToSelection(this.setSelection(e,e)),!0}},codeEditor.prototype.indent=function(t,e){var s=this.getSelection(),i=t.shiftKey,r=this.getLines(),n=this.getLineNumberFromPosition(s),a=this.getLinePosition(r,n),t=s.end>a.end;if((0==s.length||!t)&&s.start!=a.start)return this.insertAtPosition(s.start,this.indent_pattern),!0;t=new RegExp("^([ ]{"+this.params.indent_size+"}|\t)*");if(0==s.length&&s.start==a.start){var h=n-1 in r&&r[n-1].match(t);return h=h&&0==a.length?this.indent_pattern.repeat(h.length):this.indent_pattern,this.insertAtPosition(s.start,h),!0}s=this.selectLines(s);h=this.textarea.value.substr(s.start,s.end-s.start),r=h.split("\n");if(i)for(var o=new RegExp("^([ ]{"+this.params.indent_size+"}|\t)"),c=0;c<r.length;c++)r[c]=r[c].replace(o,"");else for(c=0;c<r.length;c++)r[c]=""==r[c].replace(/\s+/,"")?"":this.indent_pattern+r[c];return h=r.join("\n"),this.replaceSelection(s,h),!0},codeEditor.prototype.search=function(){if(this.search_str=window.prompt(this.params.lang.search,this.search_str||""))return this.search_pos=0,this.searchNext()},codeEditor.prototype.searchNext=function(){if(!this.search_str)return!0;var t=this.getSelection(),e=t.end>=this.search_pos?this.search_pos:t.start,s=this.textarea.value.substr(e),i=this.getSearchRegexp(this.search_str),r=s.search(i);if(-1==r)return window.alert(this.params.lang.no_search_result);i=s.match(i);return t.start=e+r,t.end=t.start+i[0].length,t.length=i[0].length,t.text=i[0],this.setSelection(t.start,t.end),this.search_pos=t.end,this.scrollToSelection(t),!0},codeEditor.prototype.getSearchRegexp=function(t,e){var s,i;return t="/"==t.substr(0,1)?(s=t.lastIndexOf("/"),i=t.substr(1,s-1),t.substr(s+1).replace(/g/,"")):(i=t.replace(/([\/$^.?()[\]{}\\])/,"\\$1"),"i"),e&&(t+="g"),new RegExp(i,t)},codeEditor.prototype.searchAndReplace=function(t){var e=this.getSelection(),i=0!=e.length?this.params.lang.search_selection:this.params.lang.search;if(!(s=window.prompt(i,this.search_str||""))||!(r=window.prompt(that.params.lang.replace)))return!0;var n,i=this.getSearchRegexp(s,!0);return 0==e.length?(n=this.textarea.value.match(i).length,this.textarea.value=this.textarea.value.replace(i,r)):(n=e.text.match(i).length,this.replaceSelection(e,e.text.replace(i,r))),window.alert(this.params.lang.replace_result.replace(/%d/g,n)),!0},codeEditor.prototype.enter=function(t){var e=this.getSelection();e.start!=e.end&&(this.replaceSelection(e,""),e=this.getSelection());var s=this.getLineNumberFromPosition(e),i="",r=!1,s=this.getLine(s);return"{"==this.textarea.value.substr(e.start-1,1)&&(i+=this.indent_pattern,r="}"==this.textarea.value.substr(e.start,1)),(match=s.match(/^(\s+)/))&&(i+=match[1]),!!i&&(this.insertAtPosition(e.start,"\n"+i),r&&(r=this.getSelection(),this.insertAtPosition(r.start,"\n"+i.substr(0,i.length-this.indent_pattern.length)),this.setSelection(r.start,r.end)),!0)},codeEditor.prototype.backspace=function(t){var e=this.getSelection();if(0<e.length)return!1;if('""'==(s=this.textarea.value.substr(e.start-1,2))||"''"==s||"{}"==s||"()"==s||"[]"==s)return--e.start,e.end+=1,this.replaceSelection(e,""),!0;var s=this.textarea.value.substr(e.start-20,20);return-1!=(pos=s.search(/([ \t]+)$/))&&(e.start-=20-pos,this.replaceSelection(e,""),!0)},codeEditor.prototype.insertBrackets=function(t,e){var s=this.getSelection(),e=e,i=e;switch(e){case"(":i=")";break;case"[":i="]";break;case"{":i="}"}return 0==s.length?this.insertAtPosition(s.start,e+i,s.start+1):this.wrapSelection(s,e,i),!0},codeEditor.prototype.toggleFullscreen=function(t){for(var e=this.parent.className.split(" "),s=0;s<e.length;s++)if("fullscreen"==e[s])return e.splice(s,1),this.parent.className=e.join(" "),!(this.fullscreen=!1);return e.push("fullscreen"),this.parent.className=e.join(" "),this.fullscreen=!0}}();

Deleted src/www/admin/static/scripts/loader.js version [ed5d9f8f69].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
(function () {
	var points = new Array;
	points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAQBAMAAAA/jegKAAAAMFBMVEWcThVVIgBVIgBUIQBWHwBVIgBVIgBVIgBVIgBVIQBVIgBPJABVIgBWIwAAAABVIgCCdzN5AAAAD3RSTlMAlcl4FOT2ptfdvRhENwF6tE0BAAAAY0lEQVQI12NgYDFtdGAAAo///+eC6PX///8ECvD8B4ICBoanIPoPA8N9EP2JgUEeRP8/wKAPor4JMMSD+QsY8sF0AEM/mJ4AVbeA4RqYNmBgBFG/HjBwgySkgfbwVCbJMjAAAAYDSpeMp7/QAAAAAElFTkSuQmCC');
	points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAALBAMAAAC9q6FRAAAAMFBMVEWcThWAMwCAMwCAMwCAMwCBMwCAMwCAMwCAMwCDLACAMwCAMwCANACAMgD/AACAMwAX2X54AAAAD3RSTlMAV83hfTmx++sLk+egJgFrG5vVAAAASklEQVQI12NgmJjckTKBgYFX////74UMDNf/g4ADw3owXcrQD6b/MJwH078Z5MH0ZwYuMP2JgW85iP7IwMAKNOD/AQYgw8k4kQEAgOI2ASlUEn0AAAAASUVORK5CYII=');
	points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKBAMAAAB/HNKOAAAAMFBMVEWcThXIcTfIcTfIcTfIcTfIcTfIcTfHcTfIcjbIcTfHcTeqVVXIcTfIczf/AADIcTeCBB0DAAAAD3RSTlMAsOfT2/XGYxKMcwNYKgH3X2xUAAAAPUlEQVQI12PgEPK8wMCw8f//GAaG9f///z/AIA8kvRnsgeR3Bn8g+ZlBFEh+ZbgKZvPo////i4GhPc14HQDfoiJRD3ymmQAAAABJRU5ErkJggg==');
	points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAALBAMAAAC9q6FRAAAAMFBMVEWcThX/mVX/mFX/mFX/mVX/mVT/mFX/m1X/mVX/mVX/mFX/mlT/pFr/mVX/mVX/mVVPZKOjAAAAD3RSTlMA96lr8Ie9JNfcQBgH4vnEmqxYAAAATUlEQVQI12NgYGDX8NRmYGDgCXz3/6MBA8PK/0AgzsAlD6I/Mex9D6J/MsSDqP9fGPLB9DeG+WD6MYM9mFZg2AaiRA8wMCxr9EwuYAAAYbQ00qywMe4AAAAASUVORK5CYII=');
	points.push('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALBAMAAABbgmoVAAAAMFBMVEWcThXIcTfIcTfHcTfJcjXIcTfIcTfIbzbHcTfIcTfJcDbHcTfIcTfIcTf/AADIcTd/hisqAAAAD3RSTlMAXvKHDNbtK9LgF5vHyQH5nLrmAAAAS0lEQVQI12NgcA5tfMDAwJj///9xBoZl/////+nAsB9I/Tdg6AdRBxj0QZQCw30QFcBgD6I2MHgCyU8LGNjn//+fyMDAsPhQ9AIGALkqLN+K3VggAAAAAElFTkSuQmC');
	
	function getRandomInt (min, max) {
	    return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	var anim = null;

	window.animatedLoader = function(elm, estimated_time) {
		var max = 500;
		var nb = 0;
		var prev = null;
		var i = (estimated_time * 1000) / max;

		anim = window.setInterval(function () {
			if (nb++ >= max)
			{
				window.clearInterval(anim);
			}
			
			if (prev)
			{
				prev.style.opacity = getRandomInt(25, 100) / 100;
			}

			var max_w = Math.min(elm.offsetWidth, elm.offsetWidth * ((nb / max)+0.1));
			var min_w = Math.max(0, max_w - (elm.offsetWidth / 10));

			var img = document.createElement('img');
			img.src = points[getRandomInt(0, points.length-1)];
			img.alt = '';
			img.style.left = getRandomInt(Math.abs(Math.floor(min_w)), Math.abs(Math.floor(max_w))) + 'px';
			img.style.top = getRandomInt(0, elm.offsetHeight) + 'px';
			elm.appendChild(img);
			prev = img;
		}, i);
	};

	window.stopAnimatedLoader = function() {
		window.clearInterval(anim);
	};
})();
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































Modified src/www/admin/static/scripts/wiki_editor.css from [57a2c80c94] to [7c91ec428a].

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
}

form#insertImage .cancel input {
    font-size: 0.8em;
    margin: .5em;
    opacity: 0.8;
}

#confirm_saved {
    position: absolute;
    top: .5em;
    right: 10em;
    text-align: center;
    transition: all .5s, opacity 2s;
}







<
<
<
<
<
<
<
<
202
203
204
205
206
207
208








}

form#insertImage .cancel input {
    font-size: 0.8em;
    margin: .5em;
    opacity: 0.8;
}








Modified src/www/admin/static/styles/01-layout.css from [ffa6515ae2] to [03092ed7ba].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    --gTextColor: 0, 0, 0;
    --gBorderColor: #666;
    --gLightBorderColor: #ccc;
    --gLightBackgroundColor: #eee;
    --gLinkColor: blue;
    --gHoverLinkColor: 127, 0, 0;

    --gMainColor: 156, 79, 21;
    --gSecondColor: 217, 134, 40;
    --gBgImage: url("../bg.png");
}

/* Dark colors */
html.dark {
    --gBgColor: 30, 30, 30;
    --gTextColor: 225, 225, 225;







|
|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    --gTextColor: 0, 0, 0;
    --gBorderColor: #666;
    --gLightBorderColor: #ccc;
    --gLightBackgroundColor: #eee;
    --gLinkColor: blue;
    --gHoverLinkColor: 127, 0, 0;

    --gMainColor: 32, 120, 122;
    --gSecondColor: 133, 185, 186;
    --gBgImage: url("../bg.png");
}

/* Dark colors */
html.dark {
    --gBgColor: 30, 30, 30;
    --gTextColor: 225, 225, 225;

Modified src/www/admin/static/styles/02-common.css from [deb85d27da] to [db410226b1].

632
633
634
635
636
637
638





































    max-width: 90%;
    overflow: auto;
    margin: 1em auto;
    background: #eee;
    padding: .5em;
    white-space: pre-wrap;
}












































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
    max-width: 90%;
    overflow: auto;
    margin: 1em auto;
    background: #eee;
    padding: .5em;
    white-space: pre-wrap;
}

svg.icon, .icon svg {
    fill: rgb(var(--gTextColor));
    stroke: rgb(var(--gTextColor));
}

.print-only {
    display: none;
}

.tag {
    font-size: .8rem;
    font-weight: normal;
    background: rgba(var(--gSecondColor), 0.3);
    border-radius: .5em;
    padding: .2em .4em;
    display: inline-block;
    margin: 0 .2em;
}

strong.tag {
    background: rgba(var(--gMainColor), 0.7);
    color: rgb(var(--gBgColor));
}

#confirm_saved {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    padding: .5em 0;
    margin: 0;
    text-align: center;
    border: 0;
    transition: all .5s, opacity 2s;
}

Modified src/www/admin/static/styles/03-forms.css from [b9691040d8] to [62bd0fac5d].

219
220
221
222
223
224
225















226
227
228
229
230
231
232
    content: attr(data-icon);
    font-weight: normal;
}

[data-icon]:empty:before {
    padding: 0;
}
















button.main, .icn-btn.main {
    color: rgb(var(--gTextColor));
    font-size: 1.2em;
    border-radius: 1em;
    padding: .5em 1em;
}







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







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
    content: attr(data-icon);
    font-weight: normal;
}

[data-icon]:empty:before {
    padding: 0;
}

/* Custom SVG icon */
.icn-btn > .icon {
    display: inline-block;
    padding-right: .3em;
    height: 1em;
    width: 1em;
    vertical-align: middle;
    transition: fill .3s, stroke .3s;
}

.icn-btn:hover > .icon {
    fill: rgb(var(--gHoverLinkColor)) !important;
    stroke: rgb(var(--gHoverLinkColor)) !important;
}

button.main, .icn-btn.main {
    color: rgb(var(--gTextColor));
    font-size: 1.2em;
    border-radius: 1em;
    padding: .5em 1em;
}
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
    font-size: .9em;
}

.actions-center {
    text-align: center;
}

form p.actions {
    float: right;
    margin: .5em;
}

/** Datepicker widget */
.datepicker-parent {
    position: relative;







|







448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
    font-size: .9em;
}

.actions-center {
    text-align: center;
}

p.actions {
    float: right;
    margin: .5em;
}

/** Datepicker widget */
.datepicker-parent {
    position: relative;

Modified src/www/admin/static/styles/05-navigation.css from [4983057d4d] to [5019cfe2e7].

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
    padding: .5em;
    text-align: center;
    position: relative;
    border: none;
    text-decoration: none;
}

nav.home ul li a::before {
    display: flex;
    font-size: 48px;
    width: 48px;
    height: 48px;
    align-items: center;
    justify-content: center;
    border-radius: .1em;
    margin: 0 auto 0 auto;
    padding: .1em;
    background: rgba(var(--gSecondColor), .5);
    margin-bottom: 5px;
    text-shadow: none;
}

nav.home ul li a[data-custom-icon]::before {
    content: "";
    width: 1em;
    height: 1em;
    color: #fff;
    background: rgba(var(--gSecondColor), .5) var(--custom-icon) no-repeat center center;
    background-size: calc(100% - .2em);
}







|














<
<
<
<
<
<
<
<
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151








    padding: .5em;
    text-align: center;
    position: relative;
    border: none;
    text-decoration: none;
}

nav.home ul li a::before, nav.home ul li a .icon {
    display: flex;
    font-size: 48px;
    width: 48px;
    height: 48px;
    align-items: center;
    justify-content: center;
    border-radius: .1em;
    margin: 0 auto 0 auto;
    padding: .1em;
    background: rgba(var(--gSecondColor), .5);
    margin-bottom: 5px;
    text-shadow: none;
}









Modified src/www/admin/static/styles/06-tables.css from [7e93f98ea7] to [f36be1f178].

55
56
57
58
59
60
61




62
63
64
65
66
67
68
    background: inherit !important;
}

table.list tbody tr.checked {
    color: #633 !important;
    background: #ffc !important;
}





table.list .error {
    color: red;
    font-weight: bold;
}

table.list .alert {







>
>
>
>







55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    background: inherit !important;
}

table.list tbody tr.checked {
    color: #633 !important;
    background: #ffc !important;
}

table.list tbody tr.highlight {
    box-shadow: 0px 0px 5px 5px rgba(var(--gSecondColor), 1);
}

table.list .error {
    color: red;
    font-weight: bold;
}

table.list .alert {
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
    width: 1.5em;
    color: rgba(var(--gMainColor), 0.5);
    text-shadow: none;
    transition: color .2s;
}

table.list td.icon svg {
    fill: rgb(var(--gTextColor));
    stroke: rgb(var(--gTextColor));
    max-width: 48px;
    max-height: 48px;
}

table.list .folder .icon span::before {
    color: rgba(var(--gMainColor), 0.9);
}







<
<







195
196
197
198
199
200
201


202
203
204
205
206
207
208
    width: 1.5em;
    color: rgba(var(--gMainColor), 0.5);
    text-shadow: none;
    transition: color .2s;
}

table.list td.icon svg {


    max-width: 48px;
    max-height: 48px;
}

table.list .folder .icon span::before {
    color: rgba(var(--gMainColor), 0.9);
}

Modified src/www/admin/static/styles/10-accounting.css from [23e876dfef] to [b27372cb3b].

86
87
88
89
90
91
92





93
94
95
96
97
98
99
    justify-content: center;
    align-items: center;
}

.year-header .forms fieldset {
    margin: 1em;
}






.year-infos .graphs {
    text-align: center;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}







>
>
>
>
>







86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
    justify-content: center;
    align-items: center;
}

.year-header .forms fieldset {
    margin: 1em;
}

.year-header figure.logo img {
    float: left;
    max-height: 100px;
}

.year-infos .graphs {
    text-align: center;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

Modified src/www/admin/users/log.php from [508c0b5404] to [2d1fa3f851].

1
2
3
4
5
6
7
8





9



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace Garradin;

use Garradin\Log;
use Garradin\Users\Session;

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






if (!$session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)) {



	$id = Session::getUserId();

	if (!$id) {
		throw new UserException('');
	}
}
else {
	$id = (int)qg('id') ?: null;
}

$tpl->assign('current', $id == Session::getUserId() ? 'me' : 'users');

$list = Log::list($id);
$list->loadFromQueryString();

$tpl->assign(compact('list', 'id'));

$tpl->display('users/log.tpl');








>
>
>
>
>
|
>
>
>
|

|
|


<
<
|
<
|

|


|


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


24

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

use Garradin\Log;
use Garradin\Users\Session;

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

$params = [];

if ($id = (int)qg('history')) {
	$params['history'] = $id;
}
elseif (($id = (int)qg('id')) && $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)) {
	$params['id_user'] = $id;
}
else {
	$params['id_self'] = Session::getUserId();

	if (!$params['id_self']) {
		throw new UserException('Access forbidden');
	}
}




$tpl->assign('current', isset($params['id_self']) ? 'me' : 'users');

$list = Log::list($params);
$list->loadFromQueryString();

$tpl->assign(compact('list', 'params'));

$tpl->display('users/log.tpl');

Modified tools/make_installer.php from [e16af43e44] to [67ca648488].

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


namespace KD2 {
	##KD2
}

namespace {
	const WEBSITE = 'https://fossil.kd2.org/garradin/';
	const INSTALL_DIR = __DIR__ . '/.install';

	echo '
	<!DOCTYPE html>
	<html>
	<head>
	<meta charset="utf-8" />







|







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


namespace KD2 {
	##KD2
}

namespace {
	const WEBSITE = 'https://fossil.kd2.org/paheko/';
	const INSTALL_DIR = __DIR__ . '/.install';

	echo '
	<!DOCTYPE html>
	<html>
	<head>
	<meta charset="utf-8" />
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
	$v = \SQLite3::version();

	if (!version_compare($v['versionString'], '3.16', '>=')) {
		throw new \Exception('SQLite3 version 3.16 ou supérieur requise. Version installée : ' . $v['versionString']);
	}

	$step = $_GET['step'] ?? null;


	@mkdir(INSTALL_DIR);
	$i = new KD2\FossilInstaller(WEBSITE, __DIR__, INSTALL_DIR, '!^garradin-(.*)\.tar\.gz$!');

	if ($step == 'download') {






		$i->download($i->latest());
		$next = 'install';
	}
	elseif ($step == 'install') {






		$i->install($i->latest());
		$i->clean($i->latest());
		$next = null;
	}
	else {
		$next = 'download';
	}

	echo $next ? '<meta http-equiv="refresh" content="0;url=?step='.$next.'" />' : '';







>


|


>
>
>
>
>
>
|



>
>
>
>
>
>
|
|







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
	$v = \SQLite3::version();

	if (!version_compare($v['versionString'], '3.16', '>=')) {
		throw new \Exception('SQLite3 version 3.16 ou supérieur requise. Version installée : ' . $v['versionString']);
	}

	$step = $_GET['step'] ?? null;
	$error = null;

	@mkdir(INSTALL_DIR);
	$i = new KD2\FossilInstaller(WEBSITE, __DIR__, INSTALL_DIR, '!^paheko-(.*)\.tar\.gz$!');

	if ($step == 'download') {
		$latest = $i->latest();

		if (!$latest) {
			die('</head><h1>Aucune version à télécharger n\'a été trouvée.</h1>');
		}

		$i->download($latest);
		$next = 'install';
	}
	elseif ($step == 'install') {
		$latest = $i->latest();

		if (!$latest) {
			die('</head><h1>Aucune version à télécharger n\'a été trouvée.</h1>');
		}

		$i->install($latest);
		$i->clean($latest);
		$next = null;
	}
	else {
		$next = 'download';
	}

	echo $next ? '<meta http-equiv="refresh" content="0;url=?step='.$next.'" />' : '';

Deleted tools/make_plugin.php version [a9aa0fc1e3].

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

if (empty($argv[1]) || empty($argv[2]))
{
    die("Usage : " . basename(__FILE__) . " /path/to/plugin/source /path/to/plugin/archive\n");
}

$plugin_file = $argv[2];
$plugin_file = preg_replace('/\.(?:tar(?:\.gz)?|phar)?$/', '', $plugin_file);

$target = realpath($argv[1]);

if (!file_exists($target . '/garradin_plugin.ini'))
{
	die("ERREUR : Le fichier $target/garradin_plugin.ini est introuvable.\n");
}

$infos = parse_ini_file($target . '/garradin_plugin.ini');

if (!empty($infos['config']))
{
	if (!file_exists($target . '/config.json'))
	{
		die("ERREUR : Le fichier config.json est obligatoire si config=1 dans garradin_plugin.ini.\n");
	}

	if (!file_exists($target . '/www/admin/config.php'))
	{
		die("ERREUR : Le fichier www/admin/config.php est obligatoire si config=1 dans garradin_plugin.ini.\n");
	}
}

$required = ['nom', 'description', 'auteur', 'url', 'version', 'menu', 'config'];

foreach ($required as $key)
{
	if (!array_key_exists($key, $infos))
	{
		die('ERREUR : Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".' . "\n");
	}
}

if (!empty($infos['menu']) && !file_exists($target . '/www/admin/index.php'))
{
	die("ERREUR : Le fichier www/admin/index.php est obligatoire quand menu=1\n");
}

@unlink('/tmp/plugin.tar');
@unlink('/tmp/plugin.tar.gz');

$p = new PharData('/tmp/plugin.tar');

$p->buildFromDirectory($target);

$p->compress(Phar::GZ);

@unlink('/tmp/plugin.tar');
rename('/tmp/plugin.tar.gz', $plugin_file . '.tar.gz');
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<