Comment: | Merge with trunk |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | templates |
Files: | files | file ages | folders |
SHA3-256: |
6355b60f754b38fb45391d890c781b1f |
User & Date: | bohwaz on 2022-07-09 19:01:17 |
Other Links: | branch diff | manifest | tags |
2022-07-09
| ||
22:57 | Implement "block" parameter for restrict section check-in: 888ed8d2a0 user: bohwaz tags: templates | |
19:01 | Merge with trunk check-in: 6355b60f75 user: bohwaz tags: templates | |
2022-07-05
| ||
21:00 | Actually, config and skeletons should be public as well check-in: 9c93de8169 user: bohwaz tags: trunk, stable | |
2022-01-06
| ||
12:53 | Fix two bugs in custom templates check-in: acb07cd04b user: bohwaz tags: templates | |
Added SECURITY.md version [58d2d41ec6].
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # Security Policy We take the security of Garradin very seriously. Nous prenons la sécurité de Garradin au sérieux. ## Supported Versions Only the latest stable branch is supported. ## Reporting a Vulnerability If you find a security issue, please contact us: security@garradin.eu Vous pouvez nous contacter à l'adresse e-mail ci-dessus si vous trouvez un problème de sécurité. |
Modified debian/config.debian.php from [94c16ecca6] to [3e3e32e81f].
︙ | ︙ | |||
71 72 73 74 75 76 77 | file_put_contents($last_file, $last_sqlite); define('Garradin\DB_FILE', $last_sqlite); } if (!defined('Garradin\LOCAL_LOGIN')) { | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | file_put_contents($last_file, $last_sqlite); define('Garradin\DB_FILE', $last_sqlite); } if (!defined('Garradin\LOCAL_LOGIN')) { define('Garradin\LOCAL_LOGIN', -1); } } elseif (isset($_SERVER['SERVER_NAME'])) { if (file_exists('/etc/garradin/config.php')) { require_once '/etc/garradin/config.php'; } |
︙ | ︙ |
Modified src/Makefile from [c8742c3d50] to [490ed32e35].
︙ | ︙ | |||
38 39 40 41 42 43 44 | cd /tmp/garradin-build/garradin/src; \ rm -f Makefile include/lib/KD2/data/countries.en.json cd /tmp/garradin-build/garradin/src/data; mkdir plugins && cd plugins; \ wget https://fossil.kd2.org/garradin-plugins/uv/welcome.tar.gz mv /tmp/garradin-build/garradin/src /tmp/garradin-build/garradin-${VERSION} @#cd /tmp/garradin-build/; zip -r -9 garradin-${VERSION}.zip garradin-${VERSION}; @#mv -f /tmp/garradin-build/garradin-${VERSION}.zip ./ | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | cd /tmp/garradin-build/garradin/src; \ rm -f Makefile include/lib/KD2/data/countries.en.json cd /tmp/garradin-build/garradin/src/data; mkdir plugins && cd plugins; \ wget https://fossil.kd2.org/garradin-plugins/uv/welcome.tar.gz mv /tmp/garradin-build/garradin/src /tmp/garradin-build/garradin-${VERSION} @#cd /tmp/garradin-build/; zip -r -9 garradin-${VERSION}.zip garradin-${VERSION}; @#mv -f /tmp/garradin-build/garradin-${VERSION}.zip ./ tar czvfh garradin-${VERSION}.tar.gz --hard-dereference -C /tmp/garradin-build garradin-${VERSION} deb: cd ../debian; ./makedeb.sh publish: release deb $(eval VERSION=$(shell cat VERSION)) gpg --armor --detach-sign garradin-${VERSION}.tar.gz |
︙ | ︙ |
Modified src/config.dist.php from [0e72595c79] to [5e023a0228].
︙ | ︙ | |||
121 122 123 124 125 126 127 | * * Défaut : DATA_ROOT . '/plugins' */ //const PLUGINS_ROOT = DATA_ROOT . '/plugins'; /** | | < > | | | > | | < | 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 | * * Défaut : DATA_ROOT . '/plugins' */ //const PLUGINS_ROOT = DATA_ROOT . '/plugins'; /** * Signaux système * * Permet de déclencher des signaux sans passer par un plugin. * Le fonctionnement des signaux système est strictment identique aux signaux des plugins. * Les signaux système sont exécutés en premier, avant les signaux des plugins. * * Format : pour chaque signal, un tableau comprenant une seule clé et une seule valeur. * 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 */ |
︙ | ︙ | |||
185 186 187 188 189 190 191 | * * Défaut : false */ //const MAIL_ERRORS = false; /** | | > > > > > > > > | 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 | * * 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 */ //const ERRORS_REPORT_URL = null; /** |
︙ | ︙ | |||
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 | * * Défaut : true * (Afin d'aider au rapport de bugs des instances auto-hébergées) */ //const ENABLE_TECH_DETAILS = true; /** * Activer la possibilité de faire une mise à jour semi-automatisée * depuis fossil.kd2.org. * * Si mis à TRUE, alors un bouton sera accessible depuis le menu "Configuration" * 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 ! * * Défaut : true * * @var bool */ //const ENABLE_UPGRADES = true; /** * Utilisation de cron pour les tâches automatiques * * Si "true" on s'attend à ce qu'une tâche automatisée appelle | > > > > > > > > > > > > > > > > > > > > > > > > > > | < > > > > | > | 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 | * * Défaut : true * (Afin d'aider au rapport de bugs des instances auto-hébergées) */ //const ENABLE_TECH_DETAILS = true; /** * Activation du log SQL (option de développement) * * Si cette constante est renseignée par un chemin de fichier SQLite valide, * alors *TOUTES* les requêtes SQL et leur contenu sera logué dans la base de données indiquée. * * Cette option permet ensuite de parcourir les requêtes via l'interface dans * Configuration -> Fonctions avancées -> Journal SQL pour permettre d'identifier * les requêtes qui mettent trop de temps, et comment elles pourraient * être améliorées. Visualiser les requêtes SQL nécessite d'avoir également activé * ENABLE_TECH_DETAILS. * * ATTENTION : cela signifie que des informations personnelles (mot de passe etc.) * peuvent se retrouver dans le log. Ne pas utiliser à moins de tester en développement. * Cette option peut significativement ralentir le chargement des pages. * * Défaut : null (= désactivé) * @var string|null */ // const SQL_DEBUG = __DIR__ . '/debug_sql.sqlite'; /** * Activer la possibilité de faire une mise à jour semi-automatisée * depuis fossil.kd2.org. * * Si mis à TRUE, alors un bouton sera accessible depuis le menu "Configuration" * 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 */ //const ENABLE_UPGRADES = true; /** * Utilisation de cron pour les tâches automatiques * * Si "true" on s'attend à ce qu'une tâche automatisée appelle * les scripts suivants: * - scripts/cron.php toutes les 24 heures (envoi des rappels de cotisation, * création des sauvegardes) * - scripts/emails.php toutes les 5 minutes environ (envoi des emails en attente) * * Si "false", les actions de scripts/cron.php seront effectuées quand une personne * se connecte. Et les emails seront envoyés instantanément (ce qui peut ralentir ou * planter si un message a beaucoup de destinataires). * * Défaut : false */ //const USE_CRON = false; /** |
︙ | ︙ | |||
346 347 348 349 350 351 352 | * * Défaut : STARTTLS */ //const SMTP_SECURITY = 'STARTTLS'; /** | | > > > > > > > > > > | > | > > > < > > | | > > > > > > > | > > < | | 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 | * * Défaut : STARTTLS */ //const SMTP_SECURITY = 'STARTTLS'; /** * Adresse e-mail destinée à recevoir les erreurs de mail * (adresses invalides etc.) * * Si laissé NULL, alors l'adresse email de l'association sera utilisée. * En cas d'hébergement de plusieurs associations, il est conseillé * d'utiliser une adresse par association. * * Voir la documentation de configuration sur des exemples de scripts * permettant de traiter les mails reçus à cette adresse. * * Défaut : null */ //const MAIL_RETURN_PATH = 'returns@monserveur.com'; /** * Mot de passe pour l'accès à l'API permettant de gérer les mails d'erreur * (voir MAIL_RETURN_PATH) * * Cette adresse HTTP permet de gérer un bounce email reçu en POST. * C'est utile si votre serveur de mail est capable de faire une requête HTTP * à la réception d'un message. * * La requête bounce doit contenir un paramètre "message", contenant l'intégralité * de l'email avec les entêtes. * * Si on définit 'abcd' ici, il faudra faire une requête comme ceci : * curl -F 'message=@/tmp/message.eml' https://bounce:abcd@monasso.com/admin/handle_bounce.php * * En alternative le serveur de mail peut aussi appeler le script * 'scripts/handle_bounce.php' * * Défaut : null (l'API handlebounce est désactivée) * * @type string|null */ //const MAIL_BOUNCE_PASSWORD = null; /** * Couleur primaire de l'interface admin par défaut * (peut être personnalisée dans la configuration) * * Défaut : #9c4f15 */ |
︙ | ︙ | |||
444 445 446 447 448 449 450 | * Si cette constante est renseignée (en octets) alors il ne sera * pas possible de stocker plus que cette valeur. * Tout envoi de fichier sera refusé. * * Défaut : null (dans ce cas c'est le stockage qui détermine la taille disponible, donc généralement l'espace dispo sur le disque dur !) */ | | > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | * Si cette constante est renseignée (en octets) alors il ne sera * pas possible de stocker plus que cette valeur. * Tout envoi de fichier sera refusé. * * Défaut : null (dans ce cas c'est le stockage qui détermine la taille disponible, donc généralement l'espace dispo sur le disque dur !) */ //const FILE_STORAGE_QUOTA = 10*1024*1024; // Forcer le quota alloué à 10 Mo, quel que soit le backend de stockage /** * 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 * * 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. * * Les outils supportés sont : * - ssconvert (apt install gnumeric) (plus rapide) * - unoconv (apt install unoconv) (utilise LibreOffice) * - unoconvert (https://github.com/unoconv/unoserver/) en spécifiant l'interface * * Défault : null (= fonctionnalité désactivée) */ //const CALC_CONVERT_COMMAND = 'unoconv'; //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'; /** * Clé de licence * * Cette clé permet de débloquer certaines fonctionnalités dans des extensions officielles. * * Pour l'obtenir il faut se créer un compte sur Garradin.eu |
︙ | ︙ |
Modified src/include/data/1.0.0_migration.sql from [074699bf2f] to [5b1e5033eb].
︙ | ︙ | |||
180 181 182 183 184 185 186 | DROP TABLE compta_comptes_bancaires; DROP TABLE compta_moyens_paiement; INSERT INTO acc_charts (country, code, label) VALUES ('FR', 'PCA2018', 'Plan comptable associatif 2018'); CREATE TEMP TABLE tmp_accounts (code,label,description,position,type); | | | 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | DROP TABLE compta_comptes_bancaires; DROP TABLE compta_moyens_paiement; INSERT INTO acc_charts (country, code, label) VALUES ('FR', 'PCA2018', 'Plan comptable associatif 2018'); CREATE TEMP TABLE tmp_accounts (code,label,description,position,type); .import charts/fr_pca_2018.csv tmp_accounts INSERT INTO acc_accounts (id_chart, code, label, description, position, type) SELECT (SELECT id FROM acc_charts WHERE code = 'PCA2018'), code, label, description, CASE position WHEN 'Actif' THEN 1 WHEN 'Passif' THEN 2 |
︙ | ︙ |
Modified src/include/data/1.1.0_schema.sql from [2577b8bab7] to [5816ffb7e0].
︙ | ︙ | |||
60 61 62 63 64 65 66 | description TEXT NULL, amount INTEGER NULL, formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01) id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL if fee is not linked to accounting, this is reset using a trigger if the year is deleted | | > | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | description TEXT NULL, amount INTEGER NULL, formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01) id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL if fee is not linked to accounting, this is reset using a trigger if the year is deleted id_year INTEGER NULL REFERENCES acc_years (id) ON DELETE SET NULL, -- NULL if fee is not linked to accounting id_analytical INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS services_users -- Enregistrement des cotisations et activités ( id INTEGER NOT NULL PRIMARY KEY, id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, |
︙ | ︙ | |||
148 149 150 151 152 153 154 155 156 157 158 159 160 161 | type INTEGER NOT NULL DEFAULT 0, -- Type de compte spécial : banque, caisse, en attente d'encaissement, etc. user INTEGER NOT NULL DEFAULT 1 -- 0 = fait partie du plan comptable original, 1 = a été ajouté par l'utilisateur ); CREATE UNIQUE INDEX IF NOT EXISTS acc_accounts_codes ON acc_accounts (code, id_chart); CREATE INDEX IF NOT EXISTS acc_accounts_type ON acc_accounts (type); CREATE INDEX IF NOT EXISTS acc_accounts_position ON acc_accounts (position); CREATE TABLE IF NOT EXISTS acc_years -- Exercices ( id INTEGER NOT NULL PRIMARY KEY, label TEXT NOT NULL, | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | type INTEGER NOT NULL DEFAULT 0, -- Type de compte spécial : banque, caisse, en attente d'encaissement, etc. user INTEGER NOT NULL DEFAULT 1 -- 0 = fait partie du plan comptable original, 1 = a été ajouté par l'utilisateur ); CREATE UNIQUE INDEX IF NOT EXISTS acc_accounts_codes ON acc_accounts (code, id_chart); CREATE INDEX IF NOT EXISTS acc_accounts_type ON acc_accounts (type); CREATE INDEX IF NOT EXISTS acc_accounts_position ON acc_accounts (position); -- Balance des comptes par exercice CREATE VIEW IF NOT EXISTS acc_accounts_balances AS SELECT id_year, id, label, code, type, debit, credit, CASE -- 3 = dynamic asset or liability depending on balance WHEN position = 3 AND (debit - credit) > 0 THEN 1 -- 1 = Asset (actif) comptes fournisseurs, tiers créditeurs WHEN position = 3 THEN 2 -- 2 = Liability (passif), comptes clients, tiers débiteurs ELSE position END AS position, CASE WHEN position IN (1, 4) -- 1 = asset, 4 = expense OR (position = 3 AND (debit - credit) > 0) THEN debit - credit ELSE credit - debit END AS balance, CASE WHEN debit - credit > 0 THEN 1 ELSE 0 END AS is_debt FROM ( SELECT t.id_year, a.id, a.label, a.code, a.position, a.type, SUM(l.credit) AS credit, SUM(l.debit) AS debit FROM acc_accounts a INNER JOIN acc_transactions_lines l ON l.id_account = a.id INNER JOIN acc_transactions t ON t.id = l.id_transaction GROUP BY t.id_year, a.id ); CREATE TABLE IF NOT EXISTS acc_years -- Exercices ( id INTEGER NOT NULL PRIMARY KEY, label TEXT NOT NULL, |
︙ | ︙ | |||
263 264 265 266 267 268 269 270 271 272 273 274 275 276 | ( signal TEXT NOT NULL, plugin TEXT NOT NULL REFERENCES plugins (id), callback TEXT NOT NULL, PRIMARY KEY (signal, plugin) ); ---------- FILES ---------------- CREATE TABLE IF NOT EXISTS files -- Files metadata ( id INTEGER NOT NULL PRIMARY KEY, path TEXT NOT NULL, | > > > > > > > > > > > > > | 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 | ( 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, last_use TEXT NULL, access_level INT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS api_credentials_key ON api_credentials (key); ---------- FILES ---------------- CREATE TABLE IF NOT EXISTS files -- Files metadata ( id INTEGER NOT NULL PRIMARY KEY, path TEXT NOT NULL, |
︙ | ︙ | |||
353 354 355 356 357 358 359 360 | ); CREATE TABLE IF NOT EXISTS compromised_passwords_cache_ranges -- Cache des préfixes de mots de passe compromis ( prefix TEXT NOT NULL PRIMARY KEY, date INTEGER NOT NULL ); | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | ); CREATE TABLE IF NOT EXISTS compromised_passwords_cache_ranges -- Cache des préfixes de mots de passe compromis ( prefix TEXT NOT NULL PRIMARY KEY, date INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS emails ( -- List of emails addresses -- We are not storing actual email addresses here for privacy reasons -- So that we can keep the record (for opt-out reasons) even when the -- email address has been removed from the users table id INTEGER NOT NULL PRIMARY KEY, hash TEXT NOT NULL, verified INTEGER NOT NULL DEFAULT 0, optout INTEGER NOT NULL DEFAULT 0, invalid INTEGER NOT NULL DEFAULT 0, fail_count INTEGER NOT NULL DEFAULT 0, sent_count INTEGER NOT NULL DEFAULT 0, fail_log TEXT NULL, last_sent TEXT NULL, added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS emails_hash ON emails (hash); CREATE TABLE IF NOT EXISTS emails_queue ( -- List of emails waiting to be sent id INTEGER NOT NULL PRIMARY KEY, sender TEXT NULL, recipient TEXT NOT NULL, recipient_hash TEXT NOT NULL, subject TEXT NOT NULL, content TEXT NOT NULL, content_html TEXT NULL, sending INTEGER NOT NULL DEFAULT 0, -- Will be changed to 1 when the queue run will start sending_started TEXT NULL, -- Will be filled with the datetime when the email sending was started context INTEGER NOT NULL ); |
Added src/include/data/1.1.19_migration.sql version [a29354b289].
> > > | 1 2 3 | UPDATE acc_accounts SET type = 13 WHERE type = 0 AND code = '1068'; UPDATE acc_accounts SET type = 14 WHERE type = 0 AND code = '110'; UPDATE acc_accounts SET type = 15 WHERE type = 0 AND code = '119'; |
Added src/include/data/1.1.21_migration.sql version [6a26982b55].
> > > > | 1 2 3 4 | ALTER TABLE services_fees ADD COLUMN id_analytical INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL; UPDATE acc_charts SET code = 'PCA_2018' WHERE code = 'PCA2018'; UPDATE acc_charts SET code = 'PCA_1999' WHERE code = 'PCA1999'; |
Added src/include/data/1.1.25_migration.sql version [72b38ceb6c].
> > > | 1 2 3 | UPDATE plugins_signaux SET signal = 'home.banner' WHERE signal = 'accueil.banniere'; UPDATE plugins_signaux SET signal = 'reminder.send.after' WHERE signal = 'rappels.auto'; UPDATE plugins_signaux SET signal = 'email.send.before' WHERE signal = 'email.envoi'; |
Added src/include/data/charts/be_pcmn_2019.csv version [1a3ff37c3d].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | "code","label","description","position","type" 1,"FONDS SOCIAL, PROVISIONS POUR RISQUES ET CHARGE ET DETTES À PLUS D’UN AN",,"Passif", 10,"Fonds de l’association ou de la fondation",,"Passif", 12,"Plus-values de réévaluation",,"Passif", 120,"Plus-values de réévaluation sur immobilisations incorporelles ",,"Passif", 121,"Plus-values de réévaluation sur immobilisations corporelles ",,"Passif", 122,"Plus-values de réévaluation sur immobilisations financières ",,"Passif", 124,"Reprises de réductions de valeurs sur placements de trésorerie ",,"Passif", 13,"Fonds affectés et autres réserves",,"Passif", 130,"Fonds affectés pour investissements",,"Passif", 131,"Fonds affectés pour passif social",,"Passif", 132,"Réserves immunisés",,"Passif", 139,"Autres fonds affectés et autres réserves",,"Passif","Affectation du résultat" 14,"Résultats reportés (+)(-)","Bénéfice ou déficit","Actif ou passif", 15,"Subsides en capital",,"Passif", 16,"Provisions et impôts différés",,"Passif", 160,"Provisions pour pensions et obligations similaires",,"Passif", 161,"Provisions pour charge fiscales",,"Passif", 162,"Provisions pour grosses réparations et gros entretiens",,"Passif", 163,"Provisions pour obligations environnementales",,"Passif", 164,"Provisions pour autres risques et charge",,"Passif", 167,"Provisions pour remboursement de subsides, legs et dons avec droit de reprise",,"Passif", 168,"Impôts différés",,"Passif", 17,"Dettes à plus d'un an",,"Passif", 170,"Emprunts subordonnés",,"Passif", 171,"Emprunts obligataires non subordonnés",,"Passif", 172,"Dettes de location-financement et dettes assimilées",,"Passif", 173,"Établissements de crédit",,"Passif", 1730,"Dettes en comptes",,"Passif", 1731,"Promesses",,"Passif", 1732,"Crédits d'acceptation",,"Passif", 174,"Autres emprunts",,"Passif", 175,"Dettes commerciales",,"Passif", 1750,"Fournisseurs",,"Passif", 1751,"Effets à payer",,"Passif", 176,"Acomptes sur commandes",,"Passif", 178,"Cautionnements en numéraire",,"Passif", 179,"Autres dettes",,"Passif", 1790,"Productives d’intérêts",,"Passif", 1791,"Non productives d’intérêts ou assorties d’un intérêt anormalement faible",,"Passif", 2,"FRAIS D’ÉTABLISSEMENT, ACTIFS IMMOBILISÉS ET CRÉANCES À PLUS D’UN AN",,"Actif", 20,"Frais d'établissement ",,"Actif", 200,"Frais de constitution",,"Actif", 201,"Frais d'émission d'emprunt",,"Actif", 202,"Autres frais d'établissement",,"Actif", 204,"Frais de restructuration",,"Actif", 21,"Immobilisations incorporelles ","Actifs immobilisés","Actif", 210,"Frais de recherche et de développement",,"Actif", 211,"Concessions, brevets, licences, savoir-faire, marques et droits similaires",,"Actif", 212,"Goodwill",,"Actif", 213,"Acomptes versés",,"Actif", 22,"Terrains et constructions ",,"Actif", 220,"Terrains",,"Actif", 221,"Constructions"," ","Actif", 222,"Terrains bâtis ",,"Actif", 223,"Autres droits réels sur des immeubles",,"Actif", 23,"Installations, machines et outillage ",,"Actif", 24,"Mobilier et matériel roulant ",,"Actif", 25,"Immobilisations détenues en location-financement et droits similaires",,"Actif", 250,"Terrains et construction",,"Actif", 251,"Installations, machines et outillage",,"Actif", 252,"Mobilier et matériel roulant",,"Actif", 26,"Autres immobilisations corporelles ",,"Actif", 27,"Immobilisations corporelles en cours et acomptes versés ",,"Actif", 28,"Immobilisations financières",,"Actif", 280,"Participations dans des sociétés liées",,"Actif", 2800,"Valeur d'acquisition",,"Actif", 2801,"Montants non appelés",,"Actif", 2808,"Plus-values actées",,"Actif", 2809,"Réductions de valeur actées",,"Actif", 281,"Créances sur des entités liées",,"Actif", 2810,"Créances en compte",,"Actif", 2811,"Effets à recevoir",,"Actif", 2812,"Titres à revenu fixe",,"Actif", 2817,"Créances douteuses",,"Actif", 2819,"Réductions de valeurs actées",,"Actif", 282,"Participations dans des sociétés avec lesquelles il existe un lien de participation",,"Actif", 2820,"Valeur d'acquisition",,"Actif", 2821,"Montants non appelés",,"Actif", 2828,"Plus-values actées ",,"Actif", 2829,"Réductions de valeurs actées",,"Actif", 283,"Créances sur des sociétés avec lesquelles il existe un lien de participation",,"Actif", 2830,"Créances en compte"," ","Actif", 2831,"Effets à recevoir"," ","Actif", 2832,"Titres à revenu fixe"," ","Actif", 2837,"Créances douteuses"," ","Actif", 2839,"Réductions de valeurs actées "," ","Actif", 284,"Autres actions et parts",,"Actif", 2840,"Valeur d'acquisition"," ","Actif", 2841,"Montants non appelés"," ","Actif", 2848,"Plus-values actées"," ","Actif", 2849,"Réductions de valeurs actées"," ","Actif", 285,"Autres créances",,"Actif", 2850,"Créances en compte"," ","Actif", 2851,"Effets à recevoir"," ","Actif", 2852,"Titres à revenu fixe"," ","Actif", 2857,"Créances douteuses"," ","Actif", 2859,"Réductions de valeurs actées"," ","Actif", 288,"Cautionnements versés en numéraire",,"Actif", 29,"Créances à plus d'un an",,"Actif", 290,"Créances commerciales",,"Actif", 2900,"Clients"," ","Actif", 2901,"Effets à recevoir"," ","Actif", 2906,"Acomptes versés "," ","Actif", 2907,"Créances douteuses"," ","Actif", 2909,"Réductions de valeurs actées"," ","Actif", 291,"Autres créances",,"Actif", 2910,"Créances en compte"," ","Actif", 2911,"Effets à recevoir"," ","Actif", 2912,"Subsides à recevoir"," ","Actif", 2915,"Créances non productives d’intérêts ou assorties d’un intérêt anormalement faible"," ","Actif", 2916,"Créances douteuses"," ","Actif", 2919,"Réductions de valeurs actées"," ","Actif", 3,"STOCKS ET COMMANDES EN COURS D'EXÉCUTION",,"Actif", 30,"Matières premières",,"Actif", 300,"Valeur d'acquisition",,"Actif", 309,"Réductions de valeur actées",,"Actif", 31,"Fournitures",,"Actif", 310,"Valeur d'acquisition",,"Actif", 319,"Réductions de valeur actées",,"Actif", 32,"En-cours de fabrication",,"Actif", 320,"Valeur d'acquisition",,"Actif", 329,"Réductions de valeur actées",,"Actif", 33,"Produit finis",,"Actif", 330,"Valeur d'acquisition",,"Actif", 339,"Réductions de valeur actées ",,"Actif", 34,"Marchandises",,"Actif", 340,"Valeur d'acquisition",,"Actif", 349,"Réductions de valeur actées ",,"Actif", 35,"Immeubles destinés a la vente",,"Actif", 350,"Valeur d'acquisition",,"Actif", 359,"Réductions de valeur actées",,"Actif", 36,"Acomptes versés sur achats pour stocks",,"Actif", 360,"Acomptes versés",,"Actif", 369,"Réductions de valeur actées ",,"Actif", 37,"Commandes en cours d'exécution",,"Actif", 370,"Valeur d'acquisition",,"Actif", 371,"Bénéfice pris en compte",,"Actif", 379,"Réductions de valeur actées",,"Actif", 4,"CRÉANCES ET DETTES À UN AN AU PLUS",,"Passif", 40,"Créances commerciales",,"Passif", 400,"Clients",,"Passif", 401,"Effets à recevoir",,"Passif", 404,"Produit à recevoir",,"Passif", 406,"Acomptes versés",,"Passif", 407,"Créances douteuses",,"Passif", 409,"Réductions de valeurs actées ",,"Passif", 41,"Autres créances",,"Passif", 410," ",,"Passif", 411,"Tva à récupérer",,"Passif", 412,"Impôts et précomptes à récupérer ",,"Passif", 4120,"4120 à 4124",,"Passif", 4125,"4125 à 4127 Autres impôts et taxes belges",,"Passif", 4128,"Impôts et taxes étrangers"," ","Passif", 413,"Subsides à recevoir",,"Passif", 414,"Produit à recevoir",,"Passif", 415,"Créances non productives d’intérêts ou assorties d’un intérêt anormalement faible",,"Passif", 416,"Créances diverses",,"Passif", 417,"Créances douteuses",,"Passif", 418,"Cautionnements versés en numéraire",,"Passif", 419,"Réductions de valeurs actées ",,"Passif", 42,"Dettes à plus d'un an échéant dans l'année (même subdivision que le compte 17)",,"Passif", 43,"Dettes financières",,"Passif", 430,"Établissements de crédit – Emprunts en compte à terme fixe",,"Passif", 431,"Établissements de crédit – Promesses",,"Passif", 432,"Établissements de crédit – Crédits d'acceptation",,"Passif", 433,"Établissements de crédit – Dettes en compte courant",,"Passif", 439,"Autres emprunts",,"Passif", 44,"Dettes commerciales",,"Passif", 440,"Fournisseurs",,"Passif", 441,"Effets à payer",,"Passif", 444,"Factures à recevoir",,"Passif", 45,"Dettes fiscales, salariales et sociales",,"Passif", 450,"Dettes fiscales estimées",,"Passif", 4505," à 4507 Autres impôts et taxes belges"," ","Passif", 4508,"Impôts et taxes étrangers"," ","Passif", 451,"Tva à payer",,"Passif", 452,"Impôts et taxes à payer",,"Passif", 4525,"Autres impôts et taxes belges"," ","Passif", 4528,"Impôts et taxes étrangers"," ","Passif", 453,"Précomptes retenus",,"Passif", 454,"Office national de la Sécurité sociale",,"Passif", 455,"Rémunérations",,"Passif", 456,"Pécules de vacances",,"Passif", 459,"Autres dettes sociales",,"Passif", 46,"Acomptes sur commandes",,"Passif", 48,"Dettes diverses",,"Passif", 480,"Obligations et coupons échus",,"Passif", 483,"Subsides à rembourser",,"Passif", 488,"Cautionnements reçus en numéraire",,"Passif", 489,"Autres dettes diverses",,"Passif", 4890,"Productives d’intérêts"," ","Passif", 4891,"Non productives d’intérêts ou assorties d’un intérêt anormalement faible"," ","Passif", 49,"Comptes de régularisation et d'attente",,"Actif", 490,"Charge à reporter",,"Actif", 491,"Produit acquis",,"Actif", 492,"Charge à imputer",,"Passif", 493,"Produit à reporter",,"Passif", 4931,"Produit à reporter (créditeur)",,"Passif","Report à nouveau créditeur" 4932,"Produit à reporter (débiteur)",,"Actif","Report à nouveau débiteur" 499,"Comptes d'attente",,"Passif", 5,"PLACEMENTS DE TRÉSORERIE ET VALEURS DISPONIBLES",,"Actif", 50,"Placements de trésorerie autres que actions et parts, titres à revenu fixe et dépôts à terme",,"Actif", 500,"Valeur d'acquisition",,"Actif", 509,"Réductions de valeurs actées ",,"Actif", 51,"Actions et parts",,"Actif", 510,"Valeur d'acquisition",,"Actif", 511,"Montants non appelés ",,"Actif", 519,"Réductions de valeur actées",,"Actif", 52,"Titres à revenu fixe",,"Actif", 520,"Valeur d'acquisition",,"Actif", 529,"Réductions de valeurs actées ",,"Actif", 53,"Dépôts à terme",,"Actif", 530,"De plus d'un an",,"Actif", 531,"De plus d'un mois et à un an au plus",,"Actif", 532,"D'un mois au plus",,"Actif", 539,"Réductions de valeur actées",,"Actif", 54,"Valeurs échues à l'encaissement",,"Actif", 55,"Établissements de crédit ",,"Actif", 56,"Comptes bancaires",,"Actif","Banque" 57,"Caisses",,"Actif", 570,"Caisses-espèces",,"Actif","Caisse" 578,"Caisses-timbres",,"Actif", 58,"Virements internes",,"Actif", 6,"CHARGES",,"Charge", 60,"Approvisionnements et marchandises",,"Charge", 600,"Achats de matières premières",,"Charge","Dépenses" 601,"Achats de fournitures",,"Charge","Dépenses" 602,"Achats de services, travaux et études",,"Charge","Dépenses" 603,"Sous-traitances générales",,"Charge","Dépenses" 604,"Achats de marchandises",,"Charge","Dépenses" 605,"Achats d'immeubles destinés à la vente",,"Charge", 608,"Remises, ristournes et rabais obtenus",,"Charge", 609,"Variation des stocks",,"Charge", 6090,"de matières premières",,"Charge", 6091,"de fournitures",,"Charge", 6094,"de marchandises",,"Charge", 6095,"d'immeubles destinés à la vente",,"Charge", 61,"Services et biens divers",,"Charge","Dépenses" 617,"Personnel intérimaire et personnes mises à la disposition de l’association ou de la fondation",,"Charge", 618,"Rémunérations, primes pour assurances extralégales, pensions de retraite et de survie des administrateurs qui ne sont pas attribuées en vertu d'un contrat de travail",,"Charge", 62,"Rémunérations, charges sociales et pensions",,"Charge", 620,"Rémunérations et avantages sociaux directs",,"Charge","Dépenses" 6200,"Administrateurs ou gérants",,"Charge", 6201,"Personnel de direction",,"Charge", 6202,"Employés",,"Charge", 6203,"Ouvriers",,"Charge", 6204,"Autres membres du personnel",,"Charge", 621,"Cotisations patronales pour assurances sociales",,"Charge", 622,"Primes patronales pour assurances extra-légales",,"Charge", 623,"Autres frais du personnel",,"Charge", 624,"Pensions de retraite et de survie",,"Charge", 6240,"Administrateurs ou gérants (27)",,"Charge", 6241,"Personnel",,"Charge", 63,"Amortissements, réductions de valeur et provisions pour risques et charges",,"Charge", 630,"Dotations aux amortissements et aux réductions de valeur sur immobilisations",,"Charge", 6300,"Dotations aux amortissements sur frais d'établissement",,"Charge", 6301,"Dotation aux amortissements sur immobilisations incorporelles",,"Charge", 6302,"Dotation aux amortissements sur immobilisations corporelles",,"Charge", 6308,"Dotation aux réductions de valeurs sur immobilisations incorporelles",,"Charge", 6309,"Dotation aux réductions de valeurs sur immobilisations corporelles",,"Charge", 631,"Réductions de valeur sur stocks",,"Charge", 6310,"Dotations",,"Charge", 6311,"Reprises",,"Charge", 632,"Réductions de valeur sur commandes en cours d'exécution",,"Charge", 6320,"Dotations",,"Charge", 6321,"Reprises",,"Charge", 633,"Réductions de valeur sur créances commerciales à plus d'un an",,"Charge", 6330,"Dotations",,"Charge", 6331,"Reprises",,"Charge", 634,"Réductions de valeur sur créances à un an au plus",,"Charge", 6340,"Dotations",,"Charge", 6341,"Reprises",,"Charge", 635,"Provisions pour pensions et obligations similaires",,"Charge", 6350,"Dotations",,"Charge", 6351,"Utilisations et reprises",,"Charge", 636,"Provisions pour grosses réparations et gros entretiens",,"Charge", 6360,"Dotations",,"Charge", 6361,"Utilisations et reprises",,"Charge", 637,"Provisions pour obligations environnementales",,"Charge", 6370,"Dotations",,"Charge", 6371,"Utilisations et reprises",,"Charge", 638,"Provisions pour subsides et legs à rembourser et pour dons avec droit de reprise",,"Charge", 6380,"Dotations",,"Charge", 6381,"Utilisations et reprises",,"Charge", 639,"Provisions pour autres risques et charges",,"Charge", 6390,"Dotations",,"Charge", 6391,"Utilisations et reprises",,"Charge", 64,"Autres charges d'exploitation",,"Charge", 640,"Charges fiscales",,"Charge", 641,"Moins-values sur réalisations courantes d'immobilisations corporelles",,"Charge", 642,"Moins-values sur réalisations de créances commerciales",,"Charge", 643,"Dons",,"Charge", 644,"644-648 Charges d'exploitations diverses","À subdiviser","Charge", 649,"Charges d'exploitation portées à l'actif au titre de frais de restructuration (–)",,"Charge", 65,"Charges financières ",,"Charge", 650,"Charges des dettes",,"Charge", 6500,"Intérêts, commissions et frais afférents aux dettes",,"Charge", 6501,"Amortissements frais d'émission d'emprunts et des primes de remboursement",,"Charge", 6502,"Intérêts intercalaires portés à l'actif",,"Charge", 651,"Réductions de valeur sur actifs circulants",,"Charge", 6510,"Dotations",,"Charge", 6511,"Reprises",,"Charge", 652,"Moins-values sur réalisation d'actifs circulants",,"Charge", 653,"Charges d'escompte de créances",,"Charge", 654,"Différences de change",,"Charge", 655,"Écarts de conversion des devises",,"Charge", 656,"Provisions à caractère financier",,"Charge", 6560,"Dotations",,"Charge", 6561,"Utilisations et reprises",,"Charge", 657,"Charges financières diverses","À subdiviser","Charge", 659,"Charges financières portées à l'actif au titre de frais de restructuration",,"Charge", 66,"Charges d’exploitation ou financières non récurrentes",,"Charge", 660,"Amortissements et réductions de valeur non récurrents (dotations)",,"Charge", 6600,"sur frais d'établissement",,"Charge", 6601,"sur immobilisations incorporelles",,"Charge", 6602,"sur immobilisations corporelles",,"Charge", 661,"Réduction de valeur sur immobilisations financières (dotation)",,"Charge", 662,"Provisions pour risques et charges non récurrents",,"Charge", 6620,"Provisions pour risques et charges d’exploitation non récurrents",,"Charge", 66200,"Dotations",,"Charge", 66201,"Utilisations",,"Charge", 6621,"Provisions pour risques et charges financiers non récurrents",,"Charge", 66210,"Dotations",,"Charge", 66211,"Utilisation",,"Charge", 663,"Moins-values sur réalisation d'actifs immobilisés",,"Charge", 6630,"Moins-values sur réalisation d'immobilisations incorporelles et corporelles",,"Charge", 6631,"Moins-values sur réalisations d'actifs immobilisés",,"Charge", 664,"664 à 667 Autres charges d'exploitation non récurrentes","À subdiviser","Charge", 668,"Autres charges financières non récurrentes",,"Charge", 6690,"Charges d’exploitation portées à l’actif au titre de frais de restructuration",,"Charge", 6691,"Charges financières non récurrentes portées à l'actif au titre de frais de restructuration",,"Charge", 67,"Impôts",,"Charge", 670," Impôts belges sur le résultat de l’exercice",,"Charge", 6701,"Excédent de versements d'impôts et de précomptes porté à l’actif (–)",,"Charge", 6702," Charges fiscales estimées",,"Charge", 6710,"Suppléments d'impôts dus ou versés",,"Charge", 6711," Suppléments d'impôts estimés",,"Charge", 6712,"Provisions fiscales constituées"," Impôts étrangers sur le résultat de l’exercice, impôts étrangers sur le résultat d'exercices antérieurs","Charge", 68," Transferts aux impôts différés et aux réserves immunisées",,"Charge", 680," Transferts aux impôts différés",,"Charge", 689,"Transferts aux réserves immunisées",,"Charge", 69,"Affectations et prélèvements",,"Charge", 690,"Résultat négatif de l'exercice antérieur reporté"," ","Passif","Résultat déficitaire" 691,"Transfert aux fonds affectés et autres réserves"," ","Actif", 692,"Résultat positif à reporter"," ","Actif","Résultat excédentaire" 7,"PRODUITS",,"Produit", 70,"Chiffre d'affaires",,"Produit", 700,"Ventes et prestations de services",,"Produit","Recettes" 708,"Remises, ristournes et rabais accordés",,"Produit", 71,"Variation des stocks et des commandes en cours d’exécution",,"Produit", 712,"Des en-cours de fabrication",,"Produit","Recettes" 713,"Des produits finis",,"Produit","Recettes" 715,"Des immeubles construits destinés à la vente",,"Produit", 717,"Des commandes en cours d'exécution",,"Produit", 7170,"Valeur d'acquisition",,"Produit", 7171,"Bénéfice pris en compte",,"Produit", 72,"Production immobilisée",,"Produit", 73,"Cotisations, dons, legs et subsides",,"Produit","Recettes" 730,"Cotisations",,"Produit","Recettes" 731,"Dons",,"Produit","Recettes" 732,"Legs",,"Produit","Recettes" 733,"Subsides",,"Produit","Recettes" 74,"Autres produits d’exploitation",,"Produit","Recettes" 740," ",,"Produit", 741,"Plus-values sur réalisations courantes d'immobilisations corporelles",,"Produit", 742,"Plus-values sur réalisation de créances commerciales",,"Produit", 743,"743-749 Produits d'exploitation divers","À subdiviser","Produit", 75,"Produits financiers",,"Produit","Recettes" 750,"Produits des immobilisations financières",,"Produit", 751,"Produits des actifs circulants",,"Produit", 752,"Plus-values sur la réalisation d'actifs circulants",,"Produit", 753,"(libellé vide dans la version officielle)",,"Produit", 754,"Différences de change",,"Produit", 755,"Écarts de conversion des devises",,"Produit", 756,"756-759 Produits financiers divers","À subdiviser","Produit", 76,"Produits d'exploitation ou financiers non récurrents",,"Produit", 760,"Reprise d'amortissements et réductions de valeur",,"Produit", 7600,"Reprise sur immobilisations incorporelles",,"Produit", 7601,"Reprise sur immobilisations corporelles",,"Produit", 761,"Reprises de réductions de valeur sur immobilisations financières",,"Produit", 762,"Reprises de provisions pour risques et charges non récurrents",,"Produit", 7620,"Reprises de provisions pour risques et charges d’exploitation non récurrents",,"Produit", 7621,"Reprises de provisions pour risques et charges financiers non récurrents",,"Produit", 763,"Plus-values sur réalisation d'actifs immobilisés",,"Produit", 7630,"Plus-values sur réalisation d'immobilisations incorporelles et corporelles",,"Produit", 7631,"Plus-values sur réalisations d'actifs immobilisés",,"Produit", 764,"764-768 Autres produits d’exploitation non récurrents","À subdiviser","Produit", 769,"Autres produits financiers non récurrents",,"Produit", 77,"Régularisation d'impôts",,"Produit", 78,"Prélèvement sur les réserves immunisées et les impôts différés",,"Produit", 780,"Prélèvement sur les impôts différés",,"Produit", 789,"Prélèvement sur les réserves immunisées",,"Produit", 79,"Affectations et prélèvements",,"Produit", 790,"Résultat positif de l'exercice antérieur reporté","Résultat excédentaire","Produit", 791,"Autres réserves",,"Produit", 792,"Résultat négatif à reporter","Résultat déficitaire","Charge", "0","DROITS ET ENGAGEMENTS HORS BILAN","Sont portés dans les comptes de la classe 0 les droits et engagements autres que ceux qui doivent être portés dans les comptes des classes 1 à 5.",, "00","Garanties constituées par des tiers pour compte de l’association ou de la fondation",,, "000","Créanciers de l'association ou de la fondation, bénéficiaires de garanties de tiers",,, "001","Tiers constituants de garanties pour compte de l’association ou de la fondation",,, "01","Garanties personnelles pour compte de tiers",,, "010","Débiteurs pour engagements sur effets en circulation",,, "011","Créanciers d'engagements sur effets en circulation",,, "0110","Effets cédés par l’association ou la fondation sous son endos",,, "0111","Autres engagements sur effets en circulation",,, "012","Débiteurs pour autres garanties personnelles",,, "013","Créanciers d'autres garanties personnelles",,, "02","Garanties réelles constituées sur avoirs propres",,, "020","Créanciers de l'association ou de la fondation, bénéficiaires de garanties réelles",,, "021","Garanties réelles constituées pour compte propre",,, "022","Créanciers de tiers, bénéficiaires de garanties réelles",,, "023","Garanties réelles constituées pour compte de tiers",,, "03","Garanties reçues",,, "032","Garanties reçues",,, "033","Constituants de garanties",,, "04","Biens et valeurs détenus par des tiers en leur nom mais aux risques et profits de l’association ou de la fondation",,, "040","Tiers, détenteurs en leur nom mais aux risques et profits de l'association ou de la fondation de biens et de valeurs",,, "041","Biens et valeurs détenus par des tiers en leur nom mais aux risques et profits de l’association ou de la fondation",,, "05","Engagements d'acquisition et de cession d’immobilisations",,, "050","Engagements d'acquisition",,, "051","Créanciers d'engagements d'acquisition",,, "052","Débiteurs pour engagements de cession",,, "053","Engagements de cession",,, "06","Marchés à terme",,, "060","Marchandises achetées à terme - à recevoir",,, "061","Créanciers pour marchandises achetées à terme",,, "062","Débiteurs pour marchandises vendues à terme",,, "063","Marchandises vendues à terme - à livrer",,, "064","Devises achetées à terme - à recevoir",,, "065","Créanciers pour devises achetées à terme",,, "066","Débiteurs pour devises vendues à terme",,, "067","Devises vendues à terme - à livrer",,, "07","Biens et valeurs de tiers détenus par l'association ou la fondation",,, "070","Droits d'usage à long terme",,, "0700","Sur terrains et constructions",,, "0701","Sur installations, machines et outillage",,, "0702","Sur mobilier et matériel roulant",,, "071","Créanciers de loyers et redevances",,, "072","Biens et valeurs de tiers reçus en dépôt, en consignation ou à façon",,, "073","Commettants et déposants de biens et de valeurs",,, "074","Biens et valeurs détenus pour compte ou aux risques et profits de tiers",,, "075","Créanciers de biens et valeurs détenus pour compte de tiers ou à leurs risques et profits",,, "09","Droits et engagements divers",,, |
Added src/include/data/charts/fr_cse_2015.csv version [5baa551f12].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | "code","label","description","position","type" 1,"Classe 1 — Comptes de capitaux (Fonds propres, emprunts et dettes assimilés)",,"Passif", 10,"FONDS ASSOCIATIFS ET RÉSERVES",,"Passif", 102,"Fonds associatifs sans droit de reprise",,"Passif", 1021,"Première situation nette établie",,"Passif", 1022,"Fonds statutaires",,"Passif", 1023,"Dotations non consomptibles",,"Passif", 10231,"Dotations non consomptibles initiales",,"Passif", 10232,"Dotations non consomptibles complémentaires",,"Passif", 1024,"Autres fonds propres sans droit de reprise",,"Passif", 103,"Fonds associatif avec droit de reprise",,"Passif", 1032,"Fonds statutaires",,"Passif", 1034,"Autres fonds propres avec droit de reprise",,"Passif", 105,"Écarts de réévaluation",,"Passif", 1051,"Écarts réévaluation sur biens sans droit reprise",,"Passif", 1052,"Écarts réévaluation sur biens avec droit reprise",,"Passif", 106,"Réserves",,"Passif", 1061,"Réserves « Attributions économiques et professionnelles »",,"Actif ou passif", 1062,"Réserves « Activités sociales et culturelles »",,"Passif", 1063,"Réserves Indisponibles",,, 1064,"Réserves statutaires",,"Passif", 1068,"Réserves réglementées",,"Passif", 1069,"Réserves pour projet de l’entité",,"Passif", 108,"Dotations consomptibles",,"Passif", 1081,"Dotations consomptibles",,"Passif", 1089,"Dotation consomptibles inscrites au compte de résultat",,"Passif", 11,"REPORT à NOUVEAU",,"Passif", 110,"Report à nouveau (Solde créditeur)",,"Passif", 1101,"Report à nouveau « Attributions économiques et professionnelles » (solde créditeur)",,"Passif", 1102,"Report à nouveau « Activités sociales et culturelles » (solde créditeur)",,"Passif", 119,"Report à nouveau (Solde débiteur)",,"Passif", 1191,"Report à nouveau « Attributions économiques et professionnelles » (solde débiteur)",,"Passif", 1192,"Report à nouveau « Activités sociales et culturelles » (solde débiteur)",,"Passif", 12,"RÉSULTAT NET DE L'EXERCICE",,"Passif", 120,"Résultat de l'exercice (excédent)",,"Passif","Résultat excédentaire" 1201,"Résultat de l’exercice « Attributions économiques et professionnelles » (excédent)",,"Passif","Résultat excédentaire" 1202,"Résultat de l’exercice « Activités sociales et culturelles » (excédent)",,"Passif","Résultat excédentaire" 129,"Résultat de l'exercice (déficit)",,"Passif","Résultat déficitaire" 1291,"Résultat de l’exercice « Attributions économiques et professionnelles » (déficit)",,"Passif","Résultat déficitaire" 1292,"Résultat de l’exercice « Activités sociales et culturelles » (déficit)",,"Passif","Résultat déficitaire" 13,"SUBVENTIONS D'INVESTISSEMENTS",,"Passif", 131,"Subventions d'équipement",,"Passif", 1311,"État",,"Passif", 1312,"Régions",,"Passif", 1313,"Départements",,"Passif", 1314,"Communes",,"Passif", 1315,"Collectivités publiques",,"Passif", 1316,"Entreprises publiques",,"Passif", 1317,"Entreprises et organismes privés",,"Passif", 139,"Subventions inscrites au compte de résultat",,"Passif", 14,"PROVISIONS RÉGLEMENTÉES",,"Passif", 148,"Autres provisions réglementées",,"Passif", 15,"PROVISIONS POUR RISQUES ET CHARGES",,"Passif", 151,"Provisions pour risques",,"Passif", 152,"Provisions pour charges sur legs ou donations",,"Passif", 153,"Provisions pour pensions et obligations simil",,"Passif", 155,"Provisions pour impôts",,"Passif", 157,"Provisions pour charges à répartir",,"Passif", 158,"Autres provisions pour charges",,"Passif", 16,"EMPRUNTS ET DETTES ASSIMILÉES",,"Passif", 163,"Autres emprunts obligataires",,"Passif", 1631,"Titres associatifs et assimilés",,"Passif", 164,"Emprunts auprès des établissements de crédit",,"Passif", 165,"Dépôts et cautionnements reçus",,"Passif", 1651,"Dépôts",,"Passif", 1655,"Cautionnements",,"Passif", 167,"Emprunts et dettes sous conditions particulières",,"Passif", 168,"Autres emprunts et dettes assimilées",,"Passif", 18,"COMPTES DE LIAISONS",,"Passif", 181,"Apports permanents siège-établissements",,"Passif", 185,"Biens & prestations de services échangés siège-établissements",,"Passif", 186,"Biens & prestations de services entre établissements - Charges",,"Passif", 187,"Biens & prestations de services entre établissements - Produits",,"Passif", 19,"FONDS DÉDIÉS OU REPORTÉS",,"Passif", 191,"Fonds reportés liés aux legs ou donations",,"Passif", 1911,"Legs ou donations",,"Passif", 1912,"Donations temporaires d’usufruit",,"Passif", 194,"Fonds dédiés sur subventions fonctionnement",,"Passif", 195,"Fonds dédiés sur contributions financières autres organismes",,"Passif", 196,"Fonds dédiés sur ressources liées à la générosité",,"Passif", 2,"Classe 2 — Comptes d'immobilisations",,"Actif", 20,"IMMOBILISATIONS INCORPORELLES",,"Actif", 201,"Frais d'établissement",,"Actif", 203,"Frais de recherche et de développement",,"Actif", 204,"Donations temporaires d’usufruit",,"Actif", 205,"Brevets, licences, marques...",,"Actif", 206,"Droit au bail",,"Actif", 208,"Autres immobilisations incorporelles",,"Actif", 21,"IMMOBILISATIONS CORPORELLES",,"Actif", 211,"Terrains",,"Actif", 212,"Agencements / aménagements de terrains",,"Actif", 2131,"Bâtiments",,"Actif", 2135,"Installations, agencements...de constructions",,"Actif", 214,"Constructions sur sol d'autrui",,"Actif", 215,"Installations techniques, matériel, outillage",,"Actif", 2154,"Matériel industriel",,"Actif", 2155,"Outillage industriel",,"Actif", 218,"Autres immobilisations corporelles",,"Actif", 2181,"Installations, agencements, aménagements divers",,"Actif", 2182,"Matériel de transport",,"Actif", 2183,"Matériel de bureau et matériel informatique","D’une valeur supérieure à 500€","Actif", 2184,"Mobilier","D’une valeur supérieure à 500€","Actif", 23,"IMMOBILISATIONS EN COURS",,"Actif", 231,"Immobilisations corporelles en cours",,"Actif", 238,"Avances, acomptes sur immobilisations corporelles",,"Actif", 24,"BIEN DESTINÉS À ÊTRE CÉDÉS",,"Actif", 240,"Biens reçus par legs ou donations à céder",,"Actif", 26,"PARTICIPATIONS ET CRÉANCES RATTACHÉES",,"Actif", 261,"Titres de participation",,"Actif", 266,"Autres formes de participation",,"Actif", 267,"Créances rattachées à des participations",,"Actif", 269,"Versements restants sur participations",,"Actif", 27,"IMMOBILISATIONS FINANCIÈRES",,"Actif", 271,"Titres immobilisés (droit de propriété)",,"Actif", 272,"Titres immobilisés (droit de créance)",,"Actif", 274,"Prêts",,"Actif", 2742,"Prêts aux partenaires",,"Actif", 275,"Dépôts et cautionnements versés",,"Actif", 276,"Autres créances immobilisées",,"Actif", 28,"AMORTISSEMENTS DES IMMOBILISATIONS",,"Actif", 280,"Amortissements des immobilisations incorporelles",,, 2801,"Amortissements frais d'établissement",,"Actif", 2804,"Amortissements donations temporaires d’usufruit",,"Actif", 2805,"Amortissements brevets, licences, marques...",,"Actif", 2806,"Amortissements droit au bail",,"Actif", 2808,"Amortissements autres immo.incorporelles",,"Actif", 2812,"Amortissements agencements, aménagements de terrains",,"Actif", 28131,"Amortissements bâtiments",,"Actif", 28135,"Amortissements installations, agencements...",,"Actif", 2814,"Amt.constructions sur sol d'autrui",,"Actif", 2815,"Amortissements installations techniques, matériel, outillage",,"Actif", 28181,"Amortissements installations, agencements, aménagements",,"Actif", 28182,"Amortissements matériel de transport",,"Actif", 28183,"Amortissement matériel de bureau, informatique","D’une valeur supérieure à 500€","Actif", 28184,"Amortissements du mobilier","D’une valeur supérieure à 500€","Actif", 29,"DÉPRÉCIATIONS DES IMMOBILISATIONS",,"Actif", 2904,"Donations temporaires d'usufruit",,"Actif", 2905,"Brevets, licences, marques...",,"Actif", 2906,"Droit au bail",,"Actif", 2908,"Autres immobilisations incorporelles",,"Actif", 2911,"Terrains",,"Actif", 2931,"Immobilisations corporelles en cours",,"Actif", 294,"Biens reçus par legs ou donations à céder",,"Actif", 2961,"Titres de participations",,"Actif", 2966,"Autres formes de participations",,"Actif", 2967,"Créances rattachées à des participations",,"Actif", 2971,"Titres immobilisés (droit de propriété)",,"Actif", 2972,"Titres immobilisés (droit de créance)",,"Actif", 2974,"Prêts",,"Actif", 2975,"Dépôts et cautionnements versés",,"Actif", 2976,"Autres créances immobilisées",,"Actif", 3,"Classe 3 — Comptes de stocks",,"Actif", 31,"MATIÈRES PREMIÈRES ET FOURNITURES",,"Actif", 318,"Matières premières et fournitures",,"Actif", 32,"AUTRES APPROVISIONNEMENTS",,"Actif", 321,"Matières consommables",,"Actif", 322,"Fournitures consommables",,"Actif", 326,"Emballages",,"Actif", 33,"EN-COURS DE PRODUCTION DE BIENS",,"Actif", 331,"Produits en cours",,"Actif", 335,"Travaux en cours",,"Actif", 34,"En-cours de production de services",,"Actif", 341,"Études en cours",,"Actif", 345,"Prestations de services en cours",,"Actif", 35,"STOCKS DE PRODUITS",,"Actif", 351,"Produits intermédiaires",,"Actif", 355,"Produits finis",,"Actif", 358,"Produits résiduels",,"Actif", 37,"STOCKS DE MARCHANDISES",,"Actif", 370,"Stocks de marchandises",,"Actif", 39,"PROVISIONS POUR DÉPRÉCIATIONS STOCKS & EN-COURS",,"Actif", 391,"Matières premières et fournitures",,"Actif", 392,"Autres approvisionnements",,"Actif", 393,"En-cours de production de biens",,"Actif", 394,"En-cours de production de services",,"Actif", 395,"Stocks de produits",,"Actif", 397,"Stocks de marchandises",,"Actif", 4,"Classe 4 — Comptes de tiers",,"Actif ou passif", 40,"FOURNISSEURS ET COMPTES RATTACHÉS",,"Actif ou passif", 401,"Fournisseurs",,"Actif ou passif", 4010,"Autres fournisseurs",,"Actif ou passif","Tiers" 403,"Fournisseurs - Effets à payer",,"Passif", 404,"Fournisseurs d'immobilisations",,"Actif ou passif", 405,"Fournisseurs d'immobilisations - Effets à payer",,"Passif", 408,"Fournisseurs - Factures non parvenues",,"Passif", 4091,"Fournisseurs - Avances & acomptes",,"Actif", 41,"BÉNÉFICIAIRES ET COMPTES RATTACHÉS",,"Actif ou passif", 410,"Bénéficiaires et comptes rattachés",,, 411,"Bénéficiaires",,"Actif ou passif", 4110,"Autres bénéficiaires","Pour les dettes ou créances des membres","Actif ou passif","Tiers" 413,"Bénéficiaires - Effets à recevoir",,"Actif", 416,"Bénéficiaires douteux ou litigieux",,"Actif", 418,"Bénéficiaires non encore facturés",,"Actif", 419,"Bénéficiaires créditeurs",,"Passif", 4191,"Bénéficiaires créditeurs : avances et acomptes",,"Passif", 4198,"Rabais, remises, ristournes à accorder et autres avoirs à établir ",,"Passif", 42,"PERSONNEL ET COMPTES RATTACHÉS",,"Actif ou passif", 421,"Personnel : Rémunérations dues",,"Passif", 4210,"Autres membres du personnel","Dettes dues aux salarié⋅e⋅s","Actif ou passif","Tiers" 422,"Comités d'entreprise, d'établissement",,"Actif ou passif", 425,"Personnel : Avances & acomptes",,"Actif", 427,"Personnel - Oppositions",,"Passif", 4286,"Personnel- Charges à payer",,"Passif", 4287,"Personnel- Produits à recevoir",,"Actif", 43,"SÉCURITÉ SOCIALE & AUTRES ORGANISMES SOCIAUX",,"Passif", 431,"Sécurité sociale",,"Passif", 4372,"Mutuelles",,"Passif", 4373,"Caisses de retraites et de prévoyance",,"Passif", 4378,"Autres organismes sociaux",,"Passif", 44,"État ET AUTRES COLLECTIVITÉS PUBLIQUES",,"Actif", 441,"État - Subventions à recevoir",,"Actif", 4421,"Prélèvements à la source- Impôt sur le revenu",,"Actif ou passif", 444,"État - Impôts sur les bénéfices",,"Actif ou passif", 4452,"TVA due intracommunautaire",,"Actif ou passif", 4455,"Taxes sur CA à décaisser",,"Actif", 44562,"TVA déductible sur immobilisations",,"Actif", 44566,"TVA déductible sur autres biens et services",,"Actif", 44571,"TVA normale collectée",,"Actif", 445711,"TVA réduite collectée",,"Actif", 445712,"TVA super-réduite collectée",,"Actif", 445713,"TVA intermédiaire collectée",,"Actif", 4458,"Taxe sur CA à régulariser ou en attente",,"Actif", 447,"Autres impôts, taxes et versements assimilés",,"Actif", 4486,"État - Charges à payer",,"Passif", 4487,"État - Produits à recevoir",,"Actif", 45,"CONFÉDÉRATION, FÉDÉRATION, UNIONS, etc. AFFILIÉES",,"Actif ou passif", 451,"Confédération, fédération et associations affiliées",,"Actif ou passif", 455,"Partenaires - comptes courants",,"Actif ou passif", 46,"DÉBITEURS DIVERS ET CREDIT.DVS",,"Actif ou passif", 461,"Créances reçues par legs ou donations",,"Actif", 466,"Dettes des legs ou donations",,"Passif", 4671,"Débiteurs divers",,"Actif ou passif", 4672,"Créditeurs divers",,"Actif ou passif", 4681,"Frais des bénévoles",,"Actif ou passif", 4686,"Divers - Charges à payer",,"Passif", 4687,"Divers - Produits à recevoir",,"Actif", 47,"COMPTES D'ATTENTE",,"Actif ou passif", 4715,"Compte de transit",,"Actif ou passif", 4718,"Compte d'attente",,"Actif ou passif", 48,"COMPTES DE RÉGULARISATION",,"Actif ou passif", 481,"Charges à répartir",,"Passif", 486,"Charges constatées d'avance",,"Actif", 487,"Produits constatés d'avance",,"Passif", 49,"DÉPRÉCIATIONS DES COMPTES DE TIERS",,"Actif ou passif", 491,"Provisions pour dépréciation des comptes d'usagers",,"Passif", 496,"Provisions pour dépréciations des comptes débiteurs divers",,"Passif", 5,"Classe 5 — Comptes financiers",,"Actif", 50,"VALEURS MOBILIÈRES DE PLACEMENT",,"Actif", 503,"Actions",,"Actif", 506,"Obligations",,"Actif", 508,"Autres valeurs mobilières de placement et créances assimilées",,"Actif", 51,"BANQUES, ÉTABLISSEMENTS FINANCIERS",,"Actif", 5112,"Chèques à encaisser",,"Actif ou passif","Attente d'encaissement" 5115,"Paiements par carte à encaisser",,"Actif ou passif", 512,"Banques",,"Actif ou passif", "512A","Compte courant Fonctionnement",,"Actif ou passif","Banque" "512B","Compte courant Œuvres Sociales",,"Actif ou passif","Banque" 5186,"Intérêts courus à payer",,"Passif", 5187,"Intérêts courus à recevoir",,"Actif", 53,"Caisses",,"Actif ou passif", 530,"Caisse",,"Actif ou passif","Caisse" 58,"VIREMENTS INTERNES",,"Actif ou passif", 580,"Virements internes",,"Actif ou passif", 59,"DÉPRÉCIATIONS DES COMPTES FINANCIERS",,"Actif ou passif", 5903,"Actions",,"Actif ou passif", 5906,"Obligations",,"Actif ou passif", 5908,"Autres valeurs mobilières de placement et créances assimilées",,"Actif ou passif", 6,"Classe 6 — Comptes de charges",,"Charge", 60,"ACHATS (SAUF 603)",,"Charge", 601,"Achats stockés - Matières premières et fournitures",,"Charge", 6010,"Achats stockés de matières et fournitures",,"Charge", 6011,"Achats stockés - Matières premières",,"Charge", 6017,"Achats stockés - Fournitures",,"Charge", 602,"Achats stockés - Autres approvisionnements",,"Charge", 6021,"Achats stockés - Matières consommables",,"Charge", 60221,"Achats stockés - Combustibles",,"Charge", 60222,"Achats stockés - Produits d'entretien",,"Charge", 60223,"Achats stockés - Fournitures d'atelier",,"Charge", 60224,"Achats stockés - Fournitures de magasin",,"Charge", 60225,"Fournitures de bureau",,"Charge", 6026,"Achats stockés - Emballages",,"Charge", 603,"Variations de stocks",,"Charge", 6031,"Variations de stocks matières & fournitures",,"Charge", 6032,"Variations stocks autres approvisionnements",,"Charge", 6037,"Variations de stocks de marchandises",,"Charge", 604,"Achats d'études et prestations de services",,"Charge", 605,"Achats matériel, équipements & travaux",,"Charge", 606,"Achats non stockés de matières et fournitures",,"Charge","Dépenses" 6061,"Fournitures non stockables (eau, énergie...)","Facture d'eau, d’électricité, etc.","Charge","Dépenses" 60611,"Eau",,"Charge","Dépenses" 60612,"Électricité",,"Charge","Dépenses" 60613,"Chauffage",,"Charge","Dépenses" 6063,"Fournitures d'entretien et petit équipement","Vis, et matériel de bricolage (sauf outils) par exemple","Charge","Dépenses" 6064,"Fournitures administratives","Cartouches d'encre, papier, matériel bureautique, etc.","Charge","Dépenses" 6065,"Petits logiciels","Par exemple contribution à un logiciel de gestion associative génial :-)","Charge","Dépenses" 6068,"Autres fournitures & matières",,"Charge","Dépenses" 607,"Achats de marchandises","Marchandises destinées à être revendues en l'état.","Charge","Dépenses" 608,"Frais accessoires d'achats",,"Charge", 60811,"Frais accessoires d'achats sur matières",,"Charge", 60817,"Frais accessoires d'achats sur fournitures",,"Charge", 609,"Rabais, remises et ristournes sur achats",,"Charge", 6091,"Rabais, remises, ristournes sur achats matières et fournitures",,"Charge", 6092,"Rabais, remises, ristournes sur achats et autres approvisionnements",,"Charge", 6097,"Rabais, remises, ristournes sur achats de marchandises",,"Charge", 61,"SERVICES EXTÉRIEURS",,"Charge", 611,"Sous-traitance générale",,"Charge", 6122,"Redevance crédit-bail mobilier",,"Charge", 6125,"Redevances crédit-bail immobilier",,"Charge", 6132,"Locations immobilières","Locations versées pour un local ou du matériel.","Charge","Dépenses" 6135,"Locations mobilières",,"Charge", 6136,"Malis sur emballages",,"Charge", 614,"Charges locatives et de copropriété",,"Charge", 6152,"Entretien sur biens immobiliers",,"Charge", 6155,"Entretien sur biens mobiliers",,"Charge", 6156,"Maintenance",,"Charge", 616,"Primes d'assurance","Frais d’assurance local, activité, etc.","Charge","Dépenses" 6161,"Primes d'assurances multirisques",,"Charge", 6164,"Primes d'assurances / risques d'exploitation",,"Charge", 6165,"Primes d'assurances / insolvabilité usagers",,"Charge", 6168,"Autres assurances",,"Charge", 617,"Etudes et recherches",,"Charge", 6181,"Documentation générale",,"Charge", 6183,"Documentation technique",,"Charge", 6185,"Frais de colloques, séminaires, conférences",,"Charge", 6187,"Prestations administratives",,"Charge", 62,"AUTRES SERVICES EXTÉRIEURS",,"Charge", 621,"Personnel extérieur à l'association",,"Charge", 6211,"Personnel intérimaire",,"Charge", 6214,"Personnel détaché ou prêté à l'association",,"Charge", 62141,"Mises à disposition de personnel salarié","Frais de mise à disposition via un groupement d’employeurs","Charge","Dépenses" 622,"Rémunérations d'intermédiaires et honoraires",,"Charge", 6221,"Commissions ... sur achats",,"Charge", 6222,"Commissions... sur ventes",,"Charge", 6226,"Honoraires",,"Charge", 62264,"Honoraires sur legs ou donations à céder",,"Charge", 6227,"Frais d'actes et de contentieux",,"Charge", 6228,"Rémunérations divers intermédiaires & honoraires",,"Charge", 623,"Publicité, publications, relations publiques","Bulletins, affiches, communication, etc.","Charge","Dépenses" 6231,"Annonces et insertions",,"Charge", 6232,"Fêtes et cérémonies",,"Charge", 6233,"Foires et expositions",,"Charge", 6234,"Cadeaux",,"Charge", 6236,"Catalogues et imprimés",,"Charge", 6237,"Publications",,"Charge", 6238,"Divers : pourboires, dons courants",,"Charge", 624,"Transports de biens...",,"Charge", 6241,"Transports sur achats",,"Charge", 6242,"Transports sur ventes",,"Charge", 6243,"Transports entre établissements",,"Charge", 6244,"Transports administratifs",,"Charge", 6247,"Transports collectifs du personnel",,"Charge", 6248,"Transports divers",,"Charge", 625,"Déplacements, missions et réceptions","Billet de train, remboursement de frais kilométrique, etc.","Charge","Dépenses" 6251,"Voyages et déplacements",,"Charge", 6255,"Frais de déménagement",,"Charge", 6256,"Frais de missions",,"Charge", 6257,"Frais de réceptions, représentations",,"Charge", 626,"Frais postaux et de télécommunications","Facture d'accès à Internet, timbres, etc.","Charge","Dépenses" 6261,"Liaisons spécialisées",,"Charge", 6263,"Affranchissements, frais postaux",,"Charge", 6265,"Téléphone",,"Charge", 627,"Services bancaires et assimilés","Frais bancaires","Charge","Dépenses" 628,"Divers",,"Charge","Dépenses" 6281,"Cotisations (liées à l'activité économique)",,"Charge", 6284,"Frais de recrutement du personnel",,"Charge", 63,"IMPÔTS, TAXES ET VERSEMENTS ASSIMILÉS",,"Charge", 631,"Sur rémunérations - administration des impôts",,"Charge", 6311,"Taxe sur les salaires",,"Charge", 633,"Sur rémunérations - autres organismes",,"Charge", 6331,"Versement de transport",,"Charge", 6332,"Allocation logement",,"Charge", 6333,"Formation professionnelle continue",,"Charge", 6334,"Participations employeurs à l'effort de construction",,"Charge", 635,"Autres - Administration des impôts",,"Charge", 63512,"Taxes foncières",,"Charge", 63513,"Autres impôts locaux",,"Charge", 6354,"Droits d'enregistrement et de timbre",,"Charge", 637,"Autres - Autres organismes",,"Charge", 64,"CHARGES DE PERSONNEL",,"Charge", 641,"Rémunérations du personnel",,"Charge", 6411,"Salaires, appointements",,"Charge", 6412,"Congés payés",,"Charge", 6413,"Primes et gratifications",,"Charge", 6414,"Indemnités et avantages divers",,"Charge", 6415,"Supplément familial",,"Charge", 645,"Charges de sécurité sociale et de prévoyance",,"Charge", 6451,"Cotisations à l'URSSAF",,"Charge", 6452,"Cotisations aux mutuelles",,"Charge", 6453,"Cotisations caisses de retraites et de prévoyance",,"Charge", 6458,"Cotisations aux autres organismes sociaux",,"Charge", 647,"Autres charges sociales",,"Charge", 6472,"Versements aux comités d'entreprise et d'établissement",,"Charge", 6473,"Versement aux comités d'hygiène et de sécurité",,"Charge", 6474,"Versements aux autres œuvres sociales",,"Charge", 6475,"Médecine du travail, pharmacie",,"Charge", 648,"Autres charges de personnel",,"Charge", 6481,"Indemnités du personnel de culte",,"Charge", 6485,"Charges sociales sur indemnités de culte",,"Charge", 6488,"Autres charges de personnel",,"Charge", 65,"AUTRES CHARGES DE GESTION COURANTE",,"Charge", 6511,"Redevances pour concessions, brevets, licences",,"Charge", 6516,"Droits d'auteur et de reproduction",,"Charge", 6518,"Autres droits et valeurs similaires",,"Charge", 652,"Licences fédérales","Licences payées pour les adhérents (par exemple fédération sportive etc.)","Charge","Dépenses" 653,"Charges de la générosité du public",,"Charge", 6531,"Autres charges sur legs ou donations",,"Charge", 654,"Pertes sur créances irrécouvrables",,"Charge", 655,"Quotes-parts sur opérations faites en commun",,"Charge", 657,"Aides financières",,"Charge", 6571,"Aides financières octroyées",,"Charge", 6572,"Quotes-parts de générosité reversée",,"Charge", 658,"Charges diverses de gestion courante",,"Charge","Dépenses" 6586,"Cotisations (vie statutaire)",,"Charge", 6588,"Charges diverses de gestion courante",,"Charge", 66,"CHARGES FINANCIÈRES",,"Charge", 661,"Charges d'intérêts",,"Charge", 665,"Escomptes accordés",,"Charge", 666,"Pertes de changes",,"Charge", 667,"Charges nettes sur cessions de valeurs mobilières de placement",,"Charge", 668,"Autres charges financières",,"Charge", 67,"Charges exceptionnelles",,"Charge", 670,"Charges exceptionnelles","Autres dépenses exceptionnelles","Charge","Dépenses" 6712,"Pénalités, amendes fiscales et pénales",,"Charge", 6713,"Dons, libéralités",,"Charge", 6714,"Créances devenues irrécouvrables",,"Charge", 6718,"Autres charges exceptionnelles de gestion",,"Charge", 673,"Apports ou affectations en numéraire",,"Charge", 675,"Valeurs comptables des éléments d'actifs cédés",,"Charge", 6750,"Valeurs comptables des actifs cédés",,"Charge", 6754,"Immobilisations reçues par legs ou donations",,"Charge", 678,"Autres charges exceptionnelles sur opération en capital",,"Charge", 68,"Dotation AUX AMORTISSEMENTS, DÉPRÉCIATIONS ET ENGAGEMENTS",,"Charge", 6811,"Dotation aux amortissements des immobilisations",,"Charge", 6812,"Dotation aux amortissements charges à répartir",,"Charge", 6815,"Dotation aux provisions d'exploitation",,"Charge", 6816,"Dotation provisions pour dépréciations des immobillisations",,"Charge", 68164,"Dotation pr dépréc. d’actifs reçus par legs ou donations",,"Charge", 6817,"Dotation aux dépréciations des actifs circulants",,"Charge", 68173,"Dotations dépréciations stocks et en-cours",,"Charge", 68174,"Dotations dépréciations créances",,"Charge", 686,"Dotation aux amortissements & Provisions - Charges financières",,"Charge", 68662,"Dotation aux amortissements & provisions immobilisations financières",,"Charge", 68665,"Dotation aux amortissements & provisions valeurs mobilières de placement",,"Charge", 687,"Dotation aux amortissements & Provisions - Charges exceptionnelles",,"Charge", 689,"Reports en fonds dédiés",,"Charge", 6891,"Reports en fonds reportés",,"Charge", 6894,"Reports en fonds dédiés / subventions d’exploitation",,"Charge", 6895,"Reports en fonds dédiés / contributions financières d'autres organismes",,"Charge", 6896,"Reports en fonds dédiés / ressources générosité",,"Charge", 69,"IMPÔTS SUR LES BÉNÉFICES",,"Charge", 695,"Impôts sur les bénéfices",,"Charge", 7,"Classe 7 — Comptes de produits",,"Produit", 70,"VENTES PROD.FINIS, MARCHANDISES, PRESTATIONS",,"Produit", 701,"Ventes de produits finis","Vente de produits fabriqués par l'association.","Produit","Recettes" 702,"Ventes de produits intermédiaires",,"Produit", 703,"Ventes de produits résiduels",,"Produit", 704,"Travaux",,"Produit", 705,"Études",,"Produit","Recettes" 706,"Prestations de services",,"Produit","Recettes" 7063,"Parrainages",,"Produit", 707,"Ventes de marchandises","Ventes de produits achetés et revendus en l’état","Produit","Recettes" 7073,"Ventes de dons en nature",,"Produit", 708,"Produits des activités annexes",,"Produit", 7081,"Produits des services exploités dans l’intérêt du personnel",,"Produit", 7083,"Locations diverses",,"Produit", 7085,"Ports et frais accessoires facturés",,"Produit", 7088,"Autres produits d'activités annexes",,"Produit", 709,"Rabais, remises, ristournes accordés",,"Produit", 7091,"Rabais, remises, ristournes sur ventes de produits finis",,"Produit", 7092,"Rabais, remises, ristournes sur ventes de produits intermédiaires",,"Produit", 7094,"Rabais, remises, ristournes sur travaux",,"Produit", 7095,"Rabais, remises, ristournes sur études",,"Produit", 7096,"Rabais, remises, ristournes sur prest.de services",,"Produit", 7097,"Rabais, remises, ristournes sur ventes marchandises",,"Produit", 71,"PRODUCTION STOCKÉE",,"Produit", 713,"Variation de stocks (en-cours, productions)",,"Produit", 7133,"Variation des en-cours de production de biens",,"Produit", 7134,"Variation des en-cours de production services",,"Produit", 7135,"Variations de stocks de produits",,"Produit", 72,"PRODUCTION IMMOBILISÉE",,"Produit", 721,"Production immobilisée incorporelle",,"Produit", 722,"Production immobilisée corporelle",,"Produit", 73,"CONCOURS PUBLICS",,"Produit", 730,"Concours publics",,"Produit", 74,"SUBVENTION D'EXPLOITATION",,"Produit", 740,"Subventions reçues",,"Produit","Recettes" 7403,"Autres subventions",,"Produit","Recettes" 748,"Subventions d'exploitation diverses",,"Produit", 75,"AUTRES PRODUITS DE GESTION COURANTE",,"Produit", 751,"Redevances pour concessions, licences...",,"Produit", 753,"Versements des fondateurs ou consommation dot",,"Produit", 7531,"Versements des fondateurs",,"Produit", 7532,"Quotes-parts de dotation consomptible virée a",,"Produit", 754,"Ressources liées à la générosité du public","Dons reçus","Produit","Recettes" 7541,"Dons manuels",,"Produit", 75411,"Dons manuels",,"Produit", 75412,"Abandons de frais par les bénévoles",,"Produit", 7542,"Mécénats",,"Produit", 7543,"Legs, donations et assurances-vie",,"Produit", 75431,"Assurances-vie",,"Produit", 75432,"Legs ou donations",,"Produit", 75433,"Autres produits sur legs ou donations",,"Produit", 755,"Contributions financières",,"Produit", 7551,"Contributions financières d’autres organismes",,"Produit", 7552,"Quotes-parts de générosité reçues",,"Produit", 756,"Cotisations","Cotisations des adhérent⋅e⋅s","Produit","Recettes" 7561,"Cotisations sans contrepartie",,"Produit", 7562,"Cotisations avec contrepartie",,"Produit", 756201,"Subvention de fonctionnement reçue l'employeur","Produits affectés à la section « Attributions économiques et professionnelles »","Produit","Recettes" 756202,"Contribution reçue de l'employeur","Produits affectés à la section « Activités sociales et culturelles »","Produit","Recettes" 757,"Gains de change / créances et dettes d’exploitation",,"Produit", 758,"Produits divers de gestion courante",,"Produit", 7588,"Autres produits divers de gestion courante",,"Produit", 76,"PRODUITS FINANCIERS",,"Produit", 761,"Produits des participations",,"Produit", 762,"Produits des autres immobilisations financières",,"Produit", 763,"Revenus des autres créances",,"Produit", 764,"Revenus des valeurs mobilières de placement",,"Produit", 765,"Escomptes obtenus",,"Produit", 766,"Gains de change",,"Produit", 767,"Produits nets sur cession valeurs mobilières de placement",,"Produit", 768,"Autres produits financiers",,"Produit", 77,"PRODUITS EXCEPTIONNELS",,"Produit", 771,"Produits exceptionnels sur opération de gestion",,"Produit", 7713,"Libéralités perçues",,"Produit", 7718,"Autres produits exceptionnels sur opération de gestion",,"Produit", 775,"Produits des cessions d'actif",,"Produit", 7754,"Immobilisations reçues en legs ou donations à céder",,"Produit", 777,"Quote-part subvention d'investissement virée au résultat",,"Produit", 778,"Autres produits exceptionnels",,"Produit", 7780,"Manifestations diverses","Revenus provenant de manifestations au profit de l'association : droit d'entrée, location d'emplacement en vide grenier, ventes, etc.","Produit","Recettes" 78,"REPRISES SUR AMORTISSEMENTS, DÉPRÉCIATIONS, ENGAGEMENTS",,"Produit", 781,"Reprises / Amortissements & Provisions d'exploitation",,"Produit", 7811,"Amortissements immobilisations corporelles & incorporelles",,"Produit", 7815,"Reprises sur provisions d'exploitation",,"Produit", 7816,"Dépréciations immobilisations corporelles & incorporelles",,"Produit", 78164,"Reprises dépréciations d’actifs reçus par legs ou donations destinés à être cédés",,"Produit", 7817,"Dépréciations actifs circulant",,"Produit", 786,"Reprises sur provisions pour risques et dépréciations ","À inscrire dans les produits exceptionnels","Produit", 7865,"Risques & charges financiers",,"Produit", 7866,"Déprec.des éléments financiers",,"Produit", 787," Reprises sur provisions pour risques et dépréciations","À inscrire dans les produits exceptionnels","Produit", 7872,"Provisions réglementées - Immobilisations",,"Produit", 7873,"Provisions réglementées - stocks",,"Produit", 7874,"Autres provisions réglementées",,"Produit", 7875,"Risques et charges",,"Produit", 7876,"Dépréciations exceptionnelles",,"Produit", 789,"Utilisations fonds reportés et de fonds dédiés",,"Produit", 7891,"Utilisations de fonds reportés",,"Produit", 7894,"Utilisations des fonds dédiés / subventions",,"Produit", 7895,"Utilisations des fonds dédiés / contributions",,"Produit", 7896,"Utilisations des fonds dédiés / générosité",,"Produit", 79,"TRANSFERT DE CHARGES",,"Produit", 791,"Transferts de charges d'exploitation",,"Produit", 796,"Transferts de charges financières",,"Produit", 797,"Transferts de charges exceptionnelles",,"Produit", 8,"Classe 8 — Comptes spéciaux",,, 80,"ENGAGEMENTS",,, 801,"Engagements donnés par l’entité",,, 8011,"Avals, cautions, garanties",,, 8014,"Effets circulant sous l’endos de l’entité",,, 8016,"Redevances crédit-bail restant à courir",,, 80161,"Crédit-bail mobilier",,, 80165,"Crédit-bail immobilier",,, 8018,"Autres engagements donnés",,, 802,"Engagements reçus par l’entité",,, 8021,"Avals, cautions, garanties",,, 8024,"Créances escomptées non échues",,, 8026,"Engagements reçus pour utilisation en crédit-bail",,, 80261,"Crédit-bail mobilier",,, 80265,"Crédit-bail immobilier",,, 8028,"Autres engagements reçus",,, 809,"Contrepartie des engagements",,, 8091,"Contrepartie 801",,, 8092,"Contrepartie 802",,, 86,"EMPLOI DES CONTRIBUTIONS VOLONTAIRES EN NATURE",,, 860,"Secours en nature (alimentaires, vestimentaires…)",,"Charge","Bénévolat" 861,"Mise à disposition gratuite de biens (locaux, matériels…)",,"Charge","Bénévolat" 862,"Prestations",,"Charge","Bénévolat" 864,"Personnel bénévole",,"Charge","Bénévolat" 87,"CONTRIBUTIONS VOLONTAIRES EN NATURE",,, 870,"Bénévolat",,"Produit","Bénévolat" 871,"Prestations en nature",,"Produit","Bénévolat" 875,"Dons en nature",,"Produit","Bénévolat" 89,"COMPTES DE BILAN",,, 890,"Bilan d'ouverture",,"Actif ou passif","Ouverture" 891,"Bilan de clôture",,"Actif ou passif","Clôture" 9,"Classe 9 — Comptes analytiques",,, 90,"COMPTES RÉFLÉCHIS",,, 906,"Charges réfléchies",,, 907,"Produits réfléchis",,, 99,"Projets",,,"Analytique" |
Modified src/include/data/charts/fr_pca_1999.csv from [e42a3ea113] to [a9e5b40a08].
1 2 3 4 5 6 7 8 9 10 11 12 13 | code,label,description,position,type 1,"Classe 1 — Comptes de capitaux (Fonds propres, emprunts et dettes assimilés)",,Passif, 10,FONDS ASSOCIATIFS ET RÉSERVES,,Passif, 102,Fonds associatif sans droit de reprise,,Passif, 1021,Valeur du patrimoine intégré,,Passif, 1022,Fonds statutaire,,Passif, 1024,Apports sans droit de reprise,,Passif, 103,Fonds associatif avec droit de reprise,,Passif, 1034,Apports avec droit de reprise,,Passif, 105,Écarts de réévaluation,,Passif, 106,Réserves,,Passif, 1063,Réserves statutaires ou contractuelles,,Passif, 1064,Réserves réglementées,,Passif, | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | code,label,description,position,type 1,"Classe 1 — Comptes de capitaux (Fonds propres, emprunts et dettes assimilés)",,Passif, 10,FONDS ASSOCIATIFS ET RÉSERVES,,Passif, 102,Fonds associatif sans droit de reprise,,Passif, 1021,Valeur du patrimoine intégré,,Passif, 1022,Fonds statutaire,,Passif, 1024,Apports sans droit de reprise,,Passif, 103,Fonds associatif avec droit de reprise,,Passif, 1034,Apports avec droit de reprise,,Passif, 105,Écarts de réévaluation,,Passif, 106,Réserves,,Passif, 1063,Réserves statutaires ou contractuelles,,Passif, 1064,Réserves réglementées,,Passif, 1068,Autres réserves (dont réserves pour projet associatif),,Passif,Affectation du résultat 11,REPORT À NOUVEAU,,Passif, 110,Report à nouveau (Solde créditeur),,Passif,Report à nouveau créditeur 119,Report à nouveau (Solde débiteur),,Passif,Report à nouveau débiteur 12,RÉSULTAT NET DE L'EXERCICE,,Passif, 120,Résultat de l'exercice (excédent),,Passif,Résultat excédentaire 129,Résultat de l'exercice (déficit),,Passif,Résultat déficitaire 13,SUBVENTIONS D'INVESTISSEMENT AFFECTÉES A DES BIENS NON RENOUVELABLES,,Passif, 131,Subventions d'investissement (renouvelables),,Passif, 139,Subventions d'investissement inscrites au compte de résultat,,Passif, 14,PROVISIONS REGLEMENTÉES,,Passif, |
︙ | ︙ |
Modified src/include/data/charts/fr_pca_2018.csv from [eee3351425] to [fef45dc8f4].
︙ | ︙ | |||
14 15 16 17 18 19 20 | 105,Ecarts de réévaluation,,Passif, 1051,Ecarts réévaluation sur biens sans dt reprise,,Passif, 1052,Ecarts réévaluation sur biens avec dt reprise,,Passif, 106,Réserves,,Passif, 1062,Réserves indisponibles,,Passif, 1063,Réserves statutaires,,Passif, 1064,Réserves réglementées,,Passif, | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 105,Ecarts de réévaluation,,Passif, 1051,Ecarts réévaluation sur biens sans dt reprise,,Passif, 1052,Ecarts réévaluation sur biens avec dt reprise,,Passif, 106,Réserves,,Passif, 1062,Réserves indisponibles,,Passif, 1063,Réserves statutaires,,Passif, 1064,Réserves réglementées,,Passif, 1068,Réserves pour projet de l’entité,,Passif,Affectation du résultat 108,Dotations consomptibles,,Passif, 1081,Dotations consomptibles,,Passif, 1089,Dot. consomptibles inscrites au cpte de résul,,Passif, 11,REPORT à NOUVEAU,,Passif, 110,Report à nouveau (Solde créditeur),,Passif,Report à nouveau créditeur 119,Report à nouveau (Solde débiteur),,Passif,Report à nouveau débiteur 12,RÉSULTAT NET DE L'EXERCICE,,Passif, 120,Résultat de l'exercice (excédent),,Passif,Résultat excédentaire 129,Résultat de l'exercice (déficit),,Passif,Résultat déficitaire 13,SUBVENTIONS D'INVESTISSEMENTS,,Passif, 131,Subventions d'équipement,,Passif, 139,Subventions inscrites au compte de résultat,,Passif, 14,PROVISIONS RÉGLEMENTÉES,,Passif, |
︙ | ︙ |
Name change from src/include/data/charts/fr_copro_2020.csv to src/include/data/charts/fr_pcc_2020.csv.
︙ | ︙ |
Added src/include/data/charts/fr_pcg_2014.csv version [587a5d3dee].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | "code","label","description","position","type" 1,"COMPTES DE CAPITAUX",,"Passif", 10,"CAPITAL ET RÉSERVES",,"Passif", 101,"Capital",,"Passif", 1011,"Capital souscrit -non appelé",,"Passif", 1012,"Capital souscrit -appelé, non versé",,"Passif", 1013,"Capital souscrit - appelé, versé",,"Passif", 102,"Fonds fiduciaires",,"Passif", 104,"Primes liées au capital social",,"Passif", 105,"Écarts de réévaluation",,"Passif", 106,"Réserves",,"Passif", 107,"Écart d’équivalence",,"Passif", 108,"Compte de l’exploitant",,"Passif", 109,"Actionnaires : capital souscrit – non appelé",,"Passif", 11,"REPORT À NOUVEAU (SOLDE CRÉDITEUR OU DÉBITEUR)",,"Passif", 110,"Report à nouveau (solde créditeur)",,"Passif", 119,"Report à nouveau (solde débiteur)",,"Passif", 12,"RÉSULTAT DE L’EXERCICE (BÉNÉFICE OU PERTE)",,"Passif", 120,"Résultat de l’exercice (bénéfice)",,"Passif","Résultat excédentaire" 129,"Résultat de l’exercice (perte)",,"Passif","Résultat déficitaire" 13,"SUBVENTIONS D’INVESTISSEMENT",,"Passif", 131,"Subventions d’équipement",,"Passif", 1311,"État",,"Passif", 1312,"Régions",,"Passif", 1313,"Départements",,"Passif", 1314,"Communes",,"Passif", 1315,"Collectivités publiques",,"Passif", 1316,"Entreprises publiques",,"Passif", 1317,"Entreprises et organismes privés",,"Passif", 1318,"Autres",,"Passif", 138,"Autres subventions d’investissement","Même ventilation que celle du compte 131, à rajouter si nécessaire","Passif", 139,"Subventions d’investissement inscrites au compte de résultat",,"Passif", 1391," Subventions d'équipement ","Même ventilation que celle du compte 131, à rajouter si nécessaire","Passif", 1398,"Autres subventions d’investissement ","Même ventilation que celle du compte 131, à rajouter si nécessaire","Passif", 14,"PROVISIONS RÉGLEMENTÉES",,"Passif", 142,"Provisions réglementées relatives aux immobilisations",,"Passif", 143,"Provisions réglementées relatives aux stocks",,"Passif", 144,"Provisions réglementées relatives aux autres éléments de l’actif",,"Passif", 145,"Amortissements dérogatoires",,"Passif", 146,"Provision spéciale de réévaluation",,"Passif", 147,"Plus-values réinvesties",,"Passif", 148,"Autres provisions réglementées",,"Passif", 15,"PROVISIONS POUR RISQUES ET CHARGES",,"Passif", 151,"Provisions pour risques",,"Passif", 153,"Provisions pour pensions et obligations similaires",,"Passif", 154,"Provisions pour restructurations",,"Passif", 155,"Provisions pour impôts",,"Passif", 156,"Provisions pour renouvellement des immobilisations (entreprises concessionnaires)",,"Passif", 157,"Provisions pour charges à répartir sur plusieurs exercices",,"Passif", 158,"Autres provisions pour charges",,"Passif", 16,"EMPRUNTS ET DETTES ASSIMILÉES",,"Passif", 161,"Emprunts obligataires convertibles",,"Passif", 163,"Autres emprunts obligataires",,"Passif", 164,"Emprunts auprès des établissements de crédit",,"Passif", 165,"Dépôts et cautionnements reçus",,"Passif", 166,"Participation des salariés aux résultats",,"Passif", 167,"Emprunts et dettes assortis de conditions particulières",,"Passif", 168,"Autres emprunts et dettes assimilées",,"Passif", 169,"Primes de remboursement des obligations",,"Passif", 17,"DETTES RATTACHÉES À DES PARTICIPATIONS",,"Passif", 171,"Dettes rattachées à des participations (groupe)",,"Passif", 174,"Dettes rattachées à des participations (hors groupe)",,"Passif", 178,"Dettes rattachées à des sociétés en participation",,"Passif", 18,"COMPTES DE LIAISON DES ÉTABLISSEMENTS ET SOCIÉTÉS EN PARTICIPATION",,"Passif", 181,"Comptes de liaison des établissements",,"Passif", 186,"Biens et prestations de services échangés entre établissements (charges)",,"Passif", 187,"Biens et prestations de services échangés entre établissements (produits)",,"Passif", 188,"Comptes de liaison des sociétés en participation",,"Passif", 2,"COMPTES D’IMMOBILISATIONS",,"Actif", 20,"IMMOBILISATIONS INCORPORELLES",,"Actif", 201,"Frais d’établissement",,"Actif", 203,"Frais de recherche et de développement",,"Actif", 205,"Concessions et droits similaires, brevets, licences, marques, procédés, logiciels, droits et valeurs similaires",,"Actif", 206,"Droit au bail",,"Actif", 207,"Fonds commercial",,"Actif", 208,"Autres immobilisations incorporelles",,"Actif", 21,"IMMOBILISATIONS CORPORELLES",,"Actif", 211,"Terrains","Au besoin, créer des sous-comptes 2111 et suivants : Terrains nus, Terrains aménagés, Sous - sols et sursols, Terrains de gisement et Terrains bâtis.","Actif", 212,"Agencements et aménagements de terrains (même ventilation que celle du compte 211","(même ventilation que celle du compte 211) ","Actif", 213,"Constructions",,"Actif", 214,"Constructions sur sol d’autrui (même ventilation que celle du compte 213",,"Actif", 215,"Installations techniques, matériels et outillage industriels",,"Actif", 218,"Autres immobilisations corporelles",,"Actif", 22,"IMMOBILISATIONS MISES EN CONCESSION",,"Actif", 23,"IMMOBILISATIONS EN COURS",,"Actif", 231,"Immobilisations corporelles en cours",,"Actif", 232,"Immobilisations incorporelles en cours",,"Actif", 237,"Avances et acomptes versés sur immobilisations incorporelles",,"Actif", 238,"Avances et acomptes versés sur commandes d’immobilisations corporelles",,"Actif", 25,"PARTS DANS DES ENTREPRISES LIÉES ET CRÉANCES SUR DES ENTREPRISES LIÉES",,"Actif", 26,"PARTICIPATIONS ET CRÉANCES RATTACHÉES À DES PARTICIPATIONS",,"Actif", 261,"Titres de participation",,"Actif", 266,"Autres formes de participation",,"Actif", 267,"Créances rattachées à des participations",,"Actif", 268,"Créances rattachées à des sociétés en participation",,"Actif", 269,"Versements restant à effectuer sur titres de participation non libérés",,"Actif", 27,"AUTRES IMMOBILISATIONS FINANCIÈRES",,"Actif", 271,"Titres immobilisés autres que les titres immobilisés de l’activité de portefeuille (droit de propriété)",,"Actif", 272,"Titres immobilisés (droit de créance)",,"Actif", 273,"Titres immobilisés de l’activité de portefeuille",,"Actif", 274,"Prêts",,"Actif", 275,"Dépôts et cautionnements versés",,"Actif", 276,"Autres créances immobilisées",,"Actif", 277,"(Actions propres ou parts propres)",,"Actif", 278,"Mali de fusion sur actifs financiers",,"Actif", 279,"Versements restant à effectuer sur titres immobilisés non libérés",,"Actif", 28,"AMORTISSEMENTS DES IMMOBILISATIONS",,"Actif", 280,"Amortissements des immobilisations incorporelles",,"Actif", 281,"Amortissements des immobilisations corporelles",,"Actif", 282,"Amortissements des immobilisations mises en concession",,"Actif", 29,"DÉPRÉCIATIONS DES IMMOBILISATIONS",,"Actif", 290,"Dépréciations des immobilisations incorporelles",,"Actif", 291,"Dépréciations des immobilisations corporelles ","Même ventilation que celle du compte 21","Actif", 292,"Dépréciations des immobilisations mises en concession",,"Actif", 293,"Dépréciations des immobilisations en cours",,"Actif", 296,"Provisions pour dépréciation des participations et créances rattachées à des participations",,"Actif", 297,"Provisions pour dépréciation des autres immobilisations financières",,"Actif", 3,"COMPTES DE STOCKS ET EN-COURS",,"Actif", 31,"MATIÈRES PREMIÈRES (ET FOURNITURES)",,"Actif", 311,"Matières (ou groupe) A",,"Actif", 312,"Matières (ou groupe) B",,"Actif", 317,"Fournitures A, B, C",,"Actif", 32,"AUTRES APPROVISIONNEMENTS",,"Actif", 321,"Matières consommables",,"Actif", 322,"Fournitures consommables",,"Actif", 326,"Emballages",,"Actif", 33,"EN-COURS DE PRODUCTION DE BIENS",,"Actif", 331,"Produits en cours",,"Actif", 335,"Travaux en cours",,"Actif", 34,"EN-COURS DE PRODUCTION DE SERVICES",,"Actif", 341,"Études en cours",,"Actif", 345,"Prestations de services en cours",,"Actif", 35,"STOCKS DE PRODUITS",,"Actif", 351,"Produits intermédiaires",,"Actif", 355,"Produits finis",,"Actif", 358,"Produits résiduels (ou matières de récupération)",,"Actif", 37,"STOCKS DE MARCHANDISES",,"Actif", 371,"Marchandises (ou groupe) A",,"Actif", 372,"Marchandises (ou groupe) B",,"Actif", 39,"PROVISIONS POUR DÉPRÉCIATION DES STOCKS ET EN-COURS",,"Actif", 391,"Provisions pour dépréciation des matières premières (et fournitures)",,"Actif", 392,"Provisions pour dépréciation des autres approvisionnements",,"Actif", 393,"Provisions pour dépréciation des en-cours de production de biens",,"Actif", 394,"Provisions pour dépréciation des en-cours de production de services",,"Actif", 395,"Provisions pour dépréciation des stocks de produits",,"Actif", 397,"Provisions pour dépréciation des stocks de marchandises",,"Actif", 4,"COMPTES DE TIERS",,"Actif ou passif", 40,"FOURNISSEURS ET COMPTES RATTACHÉS",,"Actif ou passif", 400,"Fournisseurs et Comptes rattachés",,"Actif ou passif", 401,"Fournisseurs",,"Actif ou passif", 403,"Fournisseurs – Effets à payer",,"Passif", 404,"Fournisseurs d’immobilisations",,"Actif ou passif", 405,"Fournisseurs d’immobilisations – Effets à payer",,"Passif", 408,"Fournisseurs – Factures non parvenues",,"Passif", 409,"Fournisseurs débiteurs",,"Actif", 41,"CLIENTS ET COMPTES RATTACHÉS",,"Actif ou passif", 410,"Clients et comptes rattachés",,"Actif ou passif", 411,"Clients",,"Actif ou passif", 413,"Clients – Effets à recevoir",,"Actif", 416,"Clients douteux ou litigieux",,"Actif", 418,"Clients – Produits non encore facturés",,"Actif", 419,"Clients créditeurs",,"Passif", 42,"PERSONNEL ET COMPTES RATTACHÉS",,"Actif ou passif", 421,"Personnel – Rémunérations dues",,"Passif", 422,"Comités d’entreprises, d’établissement,…",,"Actif ou passif", 424,"Participation des salariés aux résultats",,"Actif", 425,"Personnel – Avances et acomptes",,"Actif", 426,"Personnel – Dépôts",,"Passif", 427,"Personnel – Oppositions",,"Passif", 428,"Personnel – Charges à payer et produits à recevoir",,"Passif", 43,"SÉCURITÉ SOCIALE ET AUTRES ORGANISMES SOCIAUX",,"Passif", 431,"Sécurité sociale",,"Passif", 437,"Autres organismes sociaux",,"Passif", 438,"Organismes sociaux – Charges à payer et produits à recevoir",,"Passif", 44,"ÉTAT ET AUTRES COLLECTIVITÉS PUBLIQUES",,"Actif", 441,"État – Subventions à recevoir",,"Actif", 442," Contributions, impôts et taxes recouvrés pour le compte de l’État ",,"Passif", 443,"Opérations particulières avec l’État, les collectivités publiques, les organismes internationaux",,"Actif ou passif", 444,"État – Impôts sur les bénéfices",,"Actif ou passif", 445,"État – Taxes sur le chiffre d’affaires",,"Actif", 446,"Obligations cautionnées",,"Actif", 447,"Autres impôts, taxes et versements assimilés",,"Actif", 448,"État – Charges à payer et produits à recevoir",,"Actif ou passif", 449,"Quotas d’émission à acquérir",,"Passif", 45,"GROUPE ET ASSOCIÉS",,"Actif ou passif", 451,"Groupe",,"Actif ou passif", 455,"Associés – Comptes courants",,"Actif ou passif", 456,"Associés – Opérations sur le capital",,"Actif", 457,"Associés – Dividendes à payer",,"Passif", 458,"Associés – Opérations faites en commun et en G.I.E.",,"Actif ou passif", 46,"DÉBITEURS DIVERS ET CRÉDITEURS DIVERS",,"Actif ou passif", 462,"Créances sur cessions d’immobilisations",,"Actif", 464,"Dettes sur acquisitions de valeurs mobilières de placement",,"Passif", 465,"Créances sur cessions de valeurs mobilières de placement",,"Actif", 467,"Autres comptes débiteurs ou créditeurs",,"Actif ou passif", 468,"Divers – Charges à payer et produits à recevoir",,"Actif ou passif", 4686,"Charges à payer",,"Passif", 4687,"Produits à recevoir",,"Actif", 47,"COMPTES TRANSITOIRES OU D’ATTENTE",,"Actif ou passif", 471,"à 475 comptes d’attente",,"Actif ou passif", 476,"Différence de conversion – Actif",,"Actif", 477,"Différences de conversion – Passif",,"Passif", 478,"Autres comptes transitoires",,"Actif ou passif", 48,"COMPTES DE RÉGULARISATION",,"Actif ou passif", 481,"Charges à répartir sur plusieurs exercices",,"Passif", 486,"Charges constatées d’avance",,"Actif", 487,"Produits constatés d’avance",,"Passif", 488,"Comptes de répartition périodique des charges et des produits",,"Actif ou passif", 489,"Quotas d’émission alloués par l’État",,"Actif", 49,"PROVISIONS POUR DÉPRÉCIATION DES COMPTES DE TIERS",,"Actif ou passif", 491,"Provisions pour dépréciation des comptes de clients",,"Passif", 495,"Provisions pour dépréciation des comptes du groupe et des associés",,"Passif", 496,"Provisions pour dépréciation des comptes de débiteurs divers",,"Passif", 5,"COMPTES FINANCIERS",,"Actif", 50,"VALEURS MOBILIÈRES DE PLACEMENT",,"Actif", 501,"Parts dans des entreprises liées",,"Actif", 502,"Actions propres",,"Actif", 503,"Actions",,"Actif", 504,"Autres titres conférant un droit de propriété",,"Actif", 505,"Obligations et bons émis par la société et rachetés par elle",,"Actif", 506,"Obligations",,"Actif", 507,"Bons du Trésor et bons de caisse à court terme",,"Actif", 508,"Autres valeurs mobilières de placement et autres créances assimilées",,"Actif", 509,"Versements restant à effectuer sur valeurs mobilières de placement non libérées",,"Actif", 51,"BANQUES, ÉTABLISSEMENTS FINANCIERS ET ASSIMILÉS",,"Actif", 511,"Valeurs à l’encaissement",,"Actif","Attente d'encaissement" 512,"Banques",,"Actif ou passif", "512A","Compte courant",,"Actif ou passif","Banque" 514,"Chèques postaux",,"Actif ou passif","Banque" 515,"« Caisses » du Trésor et des établissements publics",,"Actif ou passif", 516,"Sociétés de bourse",,"Actif", 517,"Autres organismes financiers",,"Actif ou passif", 518,"Intérêts courus",,"Actif ou passif", 519,"Concours bancaires courants",,"Actif ou passif", 52,"INSTRUMENTS DE TRÉSORERIE",,"Actif ou passif", 53,"CAISSE",,"Actif ou passif", 531,"Caisse siège social",,"Actif ou passif","Caisse" 532,"Caisse succursale (ou usine) A",,"Actif ou passif", 533,"Caisse succursale (ou usine) B",,"Actif ou passif", 54,"RÉGIES D’AVANCE ET ACCRÉDITIFS",,"Actif ou passif", 58,"VIREMENTS INTERNES",,"Actif ou passif", 59,"DÉPRÉCIATION DES COMPTES FINANCIERS",,"Actif ou passif", 590,"Provisions pour dépréciation des valeurs mobilières de placement",,"Passif", 6,"COMPTES DE CHARGES",,"Charge", 60,"ACHATS (SAUF 603)",,"Charge", 601,"Achats stockés – Matières premières (et fournitures)",,"Charge","Dépenses" 602,"Achats stockés – Autres approvisionnements",,"Charge","Dépenses" 604,"Achats d’études et prestations de services",,"Charge","Dépenses" 605,"Achats de matériel, équipements et travaux",,"Charge","Dépenses" 606,"Achats non stockés de matière et fournitures","On peut rajouter des sous-comptes : eau, électricité, etc.","Charge","Dépenses" 607,"Achats de marchandises",,"Charge","Dépenses" 608,"(Compte réservé, le cas échéant, à la récapitulation des frais accessoires incorporés aux achats)",,"Charge", 609,"Rabais, remises et ristournes obtenus sur achats",,"Charge", 603,"Variations des stocks (approvisionnements et marchandises)",,"Charge", 61,"SERVICES EXTÉRIEURS",,"Charge", 611,"Sous-traitance générale",,"Charge", 612,"Redevances de crédit-bail",,"Charge", 613,"Locations",,"Charge","Dépenses" 614,"Charges locatives et de copropriété",,"Charge","Dépenses" 615,"Entretien et réparations",,"Charge", 616,"Primes d’assurances",,"Charge","Dépenses" 617,"Études et recherches",,"Charge", 618,"Divers",,"Charge", 619,"Rabais, remises et ristournes obtenus sur services extérieurs",,"Charge", 62,"AUTRES SERVICES EXTÉRIEURS",,"Charge", 621,"Personnel extérieur à l’entreprise",,"Charge", 622,"Rémunérations d’intermédiaires et honoraires",,"Charge", 623,"Publicité, publications, relations publiques",,"Charge", 624,"Transports de biens et transports collectifs du personnel","Ne comprend pas les transports de l’exploitant","Charge", 625,"Déplacements, missions et réceptions",,"Charge","Dépenses" 626,"Frais postaux et de télécommunications",,"Charge","Dépenses" 627,"Services bancaires et assimilés",,"Charge","Dépenses" 628,"Divers",,"Charge", 629,"Rabais, remises et ristournes obtenus sur autres services extérieurs",,"Charge", 63,"IMPÔTS, TAXES ET VERSEMENTS ASSIMILÉS",,"Charge", 631,"Impôts, taxes et versements assimilés sur rémunérations (administrations des impôts)",,"Charge", 633,"Impôts, taxes et versements assimilés sur rémunérations (autres organismes)",,"Charge", 635,"Autres impôts, taxes et versements assimilés (administrations des impôts)",,"Charge", 637,"Autres impôts, taxes et versements assimilés (autres organismes)",,"Charge", 64,"CHARGES DE PERSONNEL",,"Charge", 641,"Rémunérations du personnel",,"Charge", 644,"Rémunération du travail de l’exploitant",,"Charge","Dépenses" 645,"Charges de sécurité sociale et de prévoyance",,"Charge","Dépenses" 6451,"Cotisations à l’Urssaf",,"Charge","Dépenses" 646,"Cotisations sociales personnelles de l’exploitant",,"Charge","Dépenses" 647,"Autres charges sociales",,"Charge", 648,"Autres charges de personnel",,"Charge", 65,"AUTRES CHARGES DE GESTION COURANTE",,"Charge", 651,"Redevances pour concessions, brevets, licences, marques, procédés, logiciels, droits et valeurs similaires",,"Charge", 653,"Jetons de présence",,"Charge", 654,"Pertes sur créances irrécouvrables",,"Charge", 655,"Quote-part de résultat sur opérations faites en commun",,"Charge", 658,"Charges diverses de gestion courante",,"Charge", 66,"CHARGES FINANCIÈRES",,"Charge", 661,"Charges d’intérêts",,"Charge", 664,"Pertes sur créances liées à des participations",,"Charge", 665,"Escomptes accordés",,"Charge", 666,"Pertes de change",,"Charge", 667,"Charges nettes sur cessions de valeurs mobilières de placement",,"Charge", 668,"Autres charges financières",,"Charge", 67,"CHARGES EXCEPTIONNELLES",,"Charge", 671,"Charges exceptionnelles sur opérations de gestion",,"Charge", 672,"(Compte à la disposition des entités pour enregistrer, en cours d’exercice, les charges sur exercices antérieurs)",,"Charge", 675,"Valeurs comptables des éléments d’actif cédés",,"Charge", 678,"Autres charges exceptionnelles",,"Charge", 68,"DOTATIONS AUX AMORTISSEMENTS ET AUX PROVISIONS",,"Charge", 681,"Dotations aux amortissements et aux provisions – Charges d’exploitation",,"Charge", 686,"Dotations aux amortissements et aux provisions – Charges financières",,"Charge", 687,"Dotations aux amortissements et aux provisions – Charges exceptionnelles",,"Charge", 69,"PARTICIPATION DES SALARIÉS – IMPÔTS SUR LES BÉNÉFICES ET ASSIMILÉS",,"Charge", 691,"Participation des salariés aux résultats",,"Charge", 695,"Impôts sur les bénéfices",,"Charge", 696,"Suppléments d’impôt sur les sociétés liés aux distributions",,"Charge", 697,"Imposition forfaitaire annuelle des sociétés",,"Charge", 698,"Intégration fiscale",,"Charge", 699,"Produits – Reports en arrière des déficits",,"Charge", 7,"COMPTES DE PRODUITS",,"Produit", 70,"VENTES DE PRODUITS FABRIQUÉS, PRESTATIONS DE SERVICES, MARCHANDISES",,"Produit", 701,"Ventes de produits finis",,"Produit","Recettes" 702,"Ventes de produits intermédiaires",,"Produit","Recettes" 703,"Ventes de produits résiduels",,"Produit","Recettes" 704,"Travaux",,"Produit","Recettes" 705,"Études",,"Produit","Recettes" 706,"Prestations de services",,"Produit","Recettes" 707,"Ventes de marchandises",,"Produit","Recettes" 708,"Produits des activités annexes",,"Produit", 709,"Rabais, remises et ristournes accordés par l’entreprise",,"Produit", 71,"PRODUCTION STOCKÉE (OU DÉSTOCKAGE)",,"Produit", 713,"Variation des stocks (en-cours de production, produits)",,"Produit", 72,"PRODUCTION IMMOBILISÉE",,"Produit", 721,"Immobilisations incorporelles",,"Produit", 722,"Immobilisations corporelles",,"Produit", 74,"SUBVENTIONS D’EXPLOITATION",,"Produit", 75,"AUTRES PRODUITS DE GESTION COURANTE",,"Produit", 751,"Redevances pour concessions, brevets, licences, marques, procédés, logiciels, droits et valeurs similaires",,"Produit", 752,"Revenus des immeubles non affectés à des activités professionnelles",,"Produit", 753,"Jetons de présence et rémunérations d’administrateurs, gérants…",,"Produit", 754,"Ristournes perçues des coopératives (provenant des excédents)",,"Produit", 755,"Quotes-parts de résultat sur opérations faites en commun",,"Produit", 758,"Produits divers de gestion courante",,"Produit", 76,"PRODUITS FINANCIERS",,"Produit", 761,"PRODUITS DE PARTICIPATIONS",,"Produit", 762,"Produits des autres immobilisations financières",,"Produit", 763,"Revenus des autres créances",,"Produit", 764,"Revenus des valeurs mobilières de placement",,"Produit", 765,"Escomptes obtenus",,"Produit", 766,"Gains de change",,"Produit", 767,"Produits nets sur cessions de valeurs mobilières de placement",,"Produit", 768,"Autres produits financiers",,"Produit", 77,"Produits exceptionnels",,"Produit", 771,"Produits exceptionnels sur opérations de gestion",,"Produit", 772,"(Compte à la disposition des entités pour enregistrer, en cours d’exercice, les produits sur exercices antérieurs)",,"Produit", 775,"Produits des cessions d’éléments d’actif",,"Produit", 777,"Quote-part des subventions d’investissement virée au résultat de l’exercice",,"Produit", 778,"Autres produits exceptionnels",,"Produit", 78,"REPRISES SUR AMORTISSEMENTS ET PROVISIONS",,"Produit", 781,"Reprises sur amortissements et provisions (à inscrire dans les produits d’exploitation)",,"Produit", 786,"Reprises sur provisions pour risques (à inscrire dans les produits financiers)",,"Produit", 787,"Reprises sur provisions (à inscrire dans les produits exceptionnels)",,"Produit", 79,"TRANSFERTS DE CHARGES",,"Produit", 791,"Transferts de charges d’exploitation",,"Produit", 796,"Transferts de charges financières",,"Produit", 797,"Transferts de charges exceptionnelles",,"Produit", 89,"COMPTES DE BILAN",,, 890,"Bilan d'ouverture",,"Actif ou passif","Ouverture" 891,"Bilan de clôture",,"Actif ou passif","Clôture" |
Modified src/include/data/schema.sql from [2d18870586] to [0336fc6ade].
︙ | ︙ | |||
60 61 62 63 64 65 66 | description TEXT NULL, amount INTEGER NULL, formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01) id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL if fee is not linked to accounting, this is reset using a trigger if the year is deleted | | > | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | description TEXT NULL, amount INTEGER NULL, formula TEXT NULL, -- Formule de calcul du montant de la cotisation, si cotisation dynamique (exemple : membres.revenu_imposable * 0.01) id_service INTEGER NOT NULL REFERENCES services (id) ON DELETE CASCADE, id_account INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL CHECK (id_account IS NULL OR id_year IS NOT NULL), -- NULL if fee is not linked to accounting, this is reset using a trigger if the year is deleted id_year INTEGER NULL REFERENCES acc_years (id) ON DELETE SET NULL, -- NULL if fee is not linked to accounting id_analytical INTEGER NULL REFERENCES acc_accounts (id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS services_users -- Enregistrement des cotisations et activités ( id INTEGER NOT NULL PRIMARY KEY, id_user INTEGER NOT NULL REFERENCES membres (id) ON DELETE CASCADE, |
︙ | ︙ | |||
148 149 150 151 152 153 154 | type INTEGER NOT NULL DEFAULT 0, -- Type de compte spécial : banque, caisse, en attente d'encaissement, etc. user INTEGER NOT NULL DEFAULT 1 -- 0 = fait partie du plan comptable original, 1 = a été ajouté par l'utilisateur ); -- Balance des comptes par exercice CREATE VIEW IF NOT EXISTS acc_accounts_sums AS | > > > > > > > > > > > > | | | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | type INTEGER NOT NULL DEFAULT 0, -- Type de compte spécial : banque, caisse, en attente d'encaissement, etc. user INTEGER NOT NULL DEFAULT 1 -- 0 = fait partie du plan comptable original, 1 = a été ajouté par l'utilisateur ); -- Balance des comptes par exercice CREATE VIEW IF NOT EXISTS acc_accounts_sums AS SELECT *, CASE WHEN position = 3 -- 3 = dynamic asset or liability depending on balance THEN CASE WHEN balance < 0 THEN ABS(balance) ELSE balance END WHEN position IN (1, 4) -- 1 = asset, 4 = expense THEN ABS(balance) ELSE balance END AS balance FROM ( SELECT t.id_year, a.id, a.label, a.code, a.position, SUM(l.credit) AS credit, SUM(l.debit) AS debit, SUM(l.debit - l.credit) AS balance FROM acc_accounts a LEFT JOIN acc_transactions_lines l ON l.id_account = a.id LEFT JOIN acc_transactions t ON t.id = l.id_transaction GROUP BY t.id_year, a.id ); CREATE UNIQUE INDEX IF NOT EXISTS acc_accounts_codes ON acc_accounts (code, id_chart); CREATE INDEX IF NOT EXISTS acc_accounts_type ON acc_accounts (type); CREATE INDEX IF NOT EXISTS acc_accounts_position ON acc_accounts (position); -- Balance des comptes par exercice CREATE VIEW IF NOT EXISTS acc_accounts_balances AS SELECT id_year, id, label, code, type, debit, credit, CASE -- 3 = dynamic asset or liability depending on balance WHEN position = 3 AND (debit - credit) > 0 THEN 1 -- 1 = Asset (actif) comptes fournisseurs, tiers créditeurs WHEN position = 3 THEN 2 -- 2 = Liability (passif), comptes clients, tiers débiteurs ELSE position END AS position, CASE WHEN position IN (1, 4) -- 1 = asset, 4 = expense OR (position = 3 AND (debit - credit) > 0) THEN debit - credit ELSE credit - debit END AS balance, CASE WHEN debit - credit > 0 THEN 1 ELSE 0 END AS is_debt FROM ( SELECT t.id_year, a.id, a.label, a.code, a.position, a.type, SUM(l.credit) AS credit, SUM(l.debit) AS debit FROM acc_accounts a INNER JOIN acc_transactions_lines l ON l.id_account = a.id INNER JOIN acc_transactions t ON t.id = l.id_transaction GROUP BY t.id_year, a.id ); CREATE TABLE IF NOT EXISTS acc_years -- Exercices ( id INTEGER NOT NULL PRIMARY KEY, label TEXT NOT NULL, |
︙ | ︙ | |||
275 276 277 278 279 280 281 282 283 284 285 286 287 288 | ( signal TEXT NOT NULL, plugin TEXT NOT NULL REFERENCES plugins (id), callback TEXT NOT NULL, PRIMARY KEY (signal, plugin) ); ---------- FILES ---------------- CREATE TABLE IF NOT EXISTS files -- Files metadata ( id INTEGER NOT NULL PRIMARY KEY, path TEXT NOT NULL, | > > > > > > > > > > > > > | 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 | ( 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, last_use TEXT NULL, access_level INT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS api_credentials_key ON api_credentials (key); ---------- FILES ---------------- CREATE TABLE IF NOT EXISTS files -- Files metadata ( id INTEGER NOT NULL PRIMARY KEY, path TEXT NOT NULL, |
︙ | ︙ | |||
377 378 379 380 381 382 383 | id INTEGER NOT NULL PRIMARY KEY, document TEXT NOT NULL, key TEXT NULL, value TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS documents_data_key ON documents_data (document, key); | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | id INTEGER NOT NULL PRIMARY KEY, document TEXT NOT NULL, key TEXT NULL, value TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS documents_data_key ON documents_data (document, key); CREATE TABLE IF NOT EXISTS emails ( -- List of emails addresses -- We are not storing actual email addresses here for privacy reasons -- So that we can keep the record (for opt-out reasons) even when the -- email address has been removed from the users table id INTEGER NOT NULL PRIMARY KEY, hash TEXT NOT NULL, verified INTEGER NOT NULL DEFAULT 0, optout INTEGER NOT NULL DEFAULT 0, invalid INTEGER NOT NULL DEFAULT 0, fail_count INTEGER NOT NULL DEFAULT 0, sent_count INTEGER NOT NULL DEFAULT 0, fail_log TEXT NULL, last_sent TEXT NULL, added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS emails_hash ON emails (hash); CREATE TABLE IF NOT EXISTS emails_queue ( -- List of emails waiting to be sent id INTEGER NOT NULL PRIMARY KEY, sender TEXT NULL, recipient TEXT NOT NULL, recipient_hash TEXT NOT NULL, subject TEXT NOT NULL, content TEXT NOT NULL, content_html TEXT NULL, sending INTEGER NOT NULL DEFAULT 0, -- Will be changed to 1 when the queue run will start sending_started TEXT NULL, -- Will be filled with the datetime when the email sending was started context INTEGER NOT NULL ); |
Modified src/include/init.php from [63c4993e30] to [cb86702b49].
︙ | ︙ | |||
58 59 60 61 62 63 64 65 66 67 68 69 70 71 | if (null !== $level) { return $level; } if (!CONTRIBUTOR_LICENSE) { return null; } $key = CONTRIBUTOR_LICENSE; $key = gzinflate(base64_decode($key)); list($email, $level, $hash) = explode('==', $key); $level = (int)hexdec($level); if (substr(sha1($email . $level), 0, 10) != $hash) { | > > > > | 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | if (null !== $level) { return $level; } if (!CONTRIBUTOR_LICENSE) { return null; } if (is_int(CONTRIBUTOR_LICENSE)) { return CONTRIBUTOR_LICENSE; } $key = CONTRIBUTOR_LICENSE; $key = gzinflate(base64_decode($key)); list($email, $level, $hash) = explode('==', $key); $level = (int)hexdec($level); if (substr(sha1($email . $level), 0, 10) != $hash) { |
︙ | ︙ | |||
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 | readfile(ROOT . '/sous-domaine.html'); exit; } define('Garradin\WWW_URI', $uri); unset($uri); } if (!defined('Garradin\WWW_URL')) { $host = \KD2\HTTP::getHost(); } if (WWW_URI === null || (!empty($host) && $host == 'host.unknown')) { $title = 'Impossible de détecter automatiquement l\'URL du site web.'; $info = 'Consulter l\'aide pour configurer manuellement l\'URL avec la directive WWW_URL et WWW_URI.'; $url ='https://fossil.kd2.org/garradin/wiki?name=Installation'; if (PHP_SAPI == 'cli') { printf("\n/!\\ %s\n%s\n-> %s\n\n", $title, $info, $url); } else { printf('<h2 style="color: red">%s</h2><p><a href="%s">%s</a></p>', $title, $url, $info); } exit(1); } | > > | < > > < > > > > > > > > > > > > > > > | 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 | readfile(ROOT . '/sous-domaine.html'); exit; } define('Garradin\WWW_URI', $uri); unset($uri); } $host = null; if (!defined('Garradin\WWW_URL')) { $host = \KD2\HTTP::getHost(); } if (WWW_URI === null || (!empty($host) && $host == 'host.unknown')) { $title = 'Impossible de détecter automatiquement l\'URL du site web.'; $info = 'Consulter l\'aide pour configurer manuellement l\'URL avec la directive WWW_URL et WWW_URI.'; $url ='https://fossil.kd2.org/garradin/wiki?name=Installation'; if (PHP_SAPI == 'cli') { printf("\n/!\\ %s\n%s\n-> %s\n\n", $title, $info, $url); } else { printf('<h2 style="color: red">%s</h2><p><a href="%s">%s</a></p>', $title, $url, $info); } exit(1); } if (!defined('Garradin\WWW_URL') && $host !== null) { define('Garradin\WWW_URL', \KD2\HTTP::getScheme() . '://' . $host . WWW_URI); } static $default_config = [ 'CACHE_ROOT' => DATA_ROOT . '/cache', 'SHARED_CACHE_ROOT' => DATA_ROOT . '/cache/shared', 'DB_FILE' => DATA_ROOT . '/association.sqlite', 'DB_SCHEMA' => ROOT . '/include/data/schema.sql', 'PLUGINS_ROOT' => DATA_ROOT . '/plugins', 'PREFER_HTTPS' => false, 'ALLOW_MODIFIED_IMPORT' => true, 'SHOW_ERRORS' => true, 'MAIL_ERRORS' => false, 'ERRORS_REPORT_URL' => null, 'ENABLE_TECH_DETAILS' => true, 'ENABLE_UPGRADES' => true, 'USE_CRON' => false, 'ENABLE_XSENDFILE' => 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' => '#9c4f15', 'ADMIN_COLOR2' => '#d98628', 'FILE_STORAGE_BACKEND' => 'SQLite', 'FILE_STORAGE_CONFIG' => null, 'FILE_STORAGE_QUOTA' => null, 'API_USER' => null, 'API_PASSWORD' => null, 'PDF_COMMAND' => null, 'CALC_CONVERT_COMMAND' => null, 'CONTRIBUTOR_LICENSE' => null, 'SQL_DEBUG' => null, 'SYSTEM_SIGNALS' => [], ]; foreach ($default_config as $const => $value) { $const = sprintf('Garradin\\%s', $const); if (!defined($const)) { define($const, $value); } } // Check SMTP_SECURITY value if (SMTP_SECURITY) { $const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY); if (!defined($const)) { throw new \LogicException('Configuration: SMTP_SECURITY n\'a pas une valeur reconnue. Valeurs acceptées: STARTTLS, TLS, SSL, NONE.'); } } if (!defined('Garradin\ADMIN_BACKGROUND_IMAGE')) { define('Garradin\ADMIN_BACKGROUND_IMAGE', ADMIN_URL . 'static/gdin_bg.png'); } // Used for private files, just in case WWW_URL is not the same domain as ADMIN_URL define('Garradin\BASE_URL', str_replace('/admin/', '/', ADMIN_URL)); const HELP_URL = 'https://garradin.eu/aide'; const WEBSITE = 'https://fossil.kd2.org/garradin/'; const PLUGINS_URL = 'https://garradin.eu/plugins/list.json'; const USER_TEMPLATES_CACHE_ROOT = CACHE_ROOT . '/utemplates'; const STATIC_CACHE_ROOT = CACHE_ROOT . '/static'; |
︙ | ︙ | |||
245 246 247 248 249 250 251 | } else { ini_set('date.timezone', 'Europe/Paris'); } } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | } else { ini_set('date.timezone', 'Europe/Paris'); } } class ValidationException extends UserException { } class APIException extends \LogicException { } |
︙ | ︙ | |||
381 382 383 384 385 386 387 388 389 390 391 392 393 394 | echo $e->getMessage(); } else { $tpl = Template::getInstance(); $tpl->assign('error', $e->getMessage()); $tpl->assign('admin_url', ADMIN_URL); $tpl->display('error.tpl'); } exit; } | > | 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | echo $e->getMessage(); } else { $tpl = Template::getInstance(); $tpl->assign('error', $e->getMessage()); $tpl->assign('html_error', $e->getHTMLMessage()); $tpl->assign('admin_url', ADMIN_URL); $tpl->display('error.tpl'); } exit; } |
︙ | ︙ |
Modified src/include/lib/Garradin/API.php from [a8567c62c9] to [208f05a8c5].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; class API { protected $body; protected $params; protected $method; protected function body(): string { if (null == $this->body) { $this->body = trim(file_get_contents('php://input')); } | > > > > > > > > > > > > > | 1 2 3 4 5 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\Membres\Session; use Garradin\Web\Web; use KD2\ErrorManager; class API { protected $body; protected $params; protected $method; protected int $access; protected function requireAccess(int $level) { if ($this->access < $level) { throw new APIException('You do not have enough rights to make this request', 403); } } protected function body(): string { if (null == $this->body) { $this->body = trim(file_get_contents('php://input')); } |
︙ | ︙ | |||
58 59 60 61 62 63 64 65 66 67 68 69 70 71 | $fn = strtok($uri, '/'); // CSV import if ($fn == 'import') { if ($this->method != 'PUT') { throw new APIException('Wrong request method', 400); } $admin_user_id = 1; // FIXME: should be NULL here $file = tempnam(CACHE_ROOT, 'tmp-import-api'); try { $stdin = fopen('php://input', 'r'); | > > | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | $fn = strtok($uri, '/'); // CSV import if ($fn == 'import') { if ($this->method != 'PUT') { throw new APIException('Wrong request method', 400); } $this->requireAccess(Session::ACCESS_ADMIN); $admin_user_id = 1; // FIXME: should be NULL here $file = tempnam(CACHE_ROOT, 'tmp-import-api'); try { $stdin = fopen('php://input', 'r'); |
︙ | ︙ | |||
99 100 101 102 103 104 105 | } $fn = strtok($uri, '/'); $param = strtok(''); switch ($fn) { case 'list': | > | > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > | | 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 | } $fn = strtok($uri, '/'); $param = strtok(''); switch ($fn) { case 'list': return [ 'categories' => array_map(fn($p) => $p->asArray(true), Web::listCategories($param)), 'pages' => array_map(fn($p) => $p->asArray(true), Web::listPages($param)), ]; case 'attachment': $attachment = Web::getAttachmentFromURI($param); if (!$attachment) { throw new APIException('Page not found', 404); } $attachment->serve(); return null; case 'html': case 'page': $page = Web::getByURI($param); if (!$page) { throw new APIException('Page not found', 404); } if ($fn == 'page') { $out = $page->asArray(true); if ($this->hasParam('html')) { $out['html'] = $page->render(); } return $out; } // HTML render echo $page->render(); return null; default: throw new APIException('Unknown web action', 404); } } public function errors(string $uri) { $fn = strtok($uri, '/'); if (!ini_get('error_log')) { throw new APIException('The error log is disabled', 404); } if (!ENABLE_TECH_DETAILS) { throw new APIException('Access to error log is disabled.', 403); } if ($uri == 'report') { if ($this->method != 'POST') { throw new APIException('Wrong request method', 400); } $this->requireAccess(Session::ACCESS_ADMIN); $body = $this->body(); $report = json_decode($body); if (!isset($report->context->id)) { throw new APIException('Invalid JSON body', 400); } $log = sprintf('=========== Error ref. %s ===========', $report->context->id) . PHP_EOL . PHP_EOL . "Report from API" . PHP_EOL . PHP_EOL . '<errorReport>' . PHP_EOL . json_encode($report, \JSON_PRETTY_PRINT) . PHP_EOL . '</errorReport>' . PHP_EOL; error_log($log); return null; } elseif ($uri == 'log') { if ($this->method != 'GET') { throw new APIException('Wrong request method', 400); } return ErrorManager::getReportsFromLog(null, null); } else { throw new APIException('Unknown errors action', 404); } } public function checkAuth(): void { if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { throw new APIException('No username or password supplied', 401); } if (API_USER && API_PASSWORD && $_SERVER['PHP_AUTH_USER'] === API_USER && $_SERVER['PHP_AUTH_PW'] === API_PASSWORD) { $this->access = Session::ACCESS_ADMIN; } elseif ($c = API_Credentials::login($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { $this->access = $c->access_level; } else { throw new APIException('Invalid username or password', 403); } } public function dispatch(string $fn, string $uri) { $this->checkAuth(); switch ($fn) { case 'sql': return $this->sql(); case 'download': return $this->download(); case 'web': return $this->web($uri); case 'user': return $this->user($uri); case 'errors': return $this->errors($uri); default: throw new APIException('Unknown path', 404); } } static public function dispatchURI(string $uri) { $fn = strtok($uri, '/'); $api = new self; $api->method = $_SERVER['REQUEST_METHOD'] ?? null; http_response_code(200); try { $return = $api->dispatch($fn, strtok('')); if (null !== $return) { echo json_encode($return, JSON_PRETTY_PRINT); } } catch (\Exception $e) { if ($e instanceof APIException) { http_response_code($e->getCode()); echo json_encode(['error' => $e->getMessage()]); } |
︙ | ︙ |
Added src/include/lib/Garradin/API_Credentials.php version [f46f4537cc].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <?php namespace Garradin; use Garradin\Entities\API_Credentials as Entity; use KD2\DB\EntityManager as EM; class API_Credentials { static public function list(): array { return EM::getInstance(Entity::class)->all('SELECT * FROM @TABLE ORDER BY key;'); } static public function create(): Entity { $e = new Entity; $e->importForm(); $e->secret = password_hash($e->secret, \PASSWORD_DEFAULT); $e->created = new \DateTime; $e->save(); return $e; } static public function generateSecret(): string { return preg_replace('/[^0-9a-z]/i', '', base64_encode(random_bytes(16))); } static public function generateKey(): string { return strtolower(substr(self::generateSecret(), 0, 10)); } static public function delete(int $id): void { EM::findOneById(Entity::class, $id)->delete(); } static public function login(string $key, string $secret): ?Entity { $e = EM::findOne(Entity::class, 'SELECT * FROM @TABLE WHERE key = ?;', $key); if (!$e || !password_verify($secret, $e->secret)) { return null; } EM::getInstance(Entity::class)->DB()->exec(sprintf('UPDATE %s SET last_use = datetime() WHERE id = %d;', Entity::TABLE, $e->id())); return $e; } } |
Modified src/include/lib/Garradin/Accounting/Accounts.php from [be892e76e5] to [fe703b65c6].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Accounting\Year; use Garradin\CSV; use Garradin\DB; use Garradin\Utils; use Garradin\UserException; use Garradin\ValidationException; use KD2\DB\EntityManager; class Accounts { | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Accounting\Year; use Garradin\Config; use Garradin\CSV; use Garradin\DB; use Garradin\DynamicList; use Garradin\Utils; use Garradin\UserException; use Garradin\ValidationException; use KD2\DB\EntityManager; class Accounts { |
︙ | ︙ | |||
36 37 38 39 40 41 42 43 44 45 46 47 48 49 | return EntityManager::getInstance(Account::class)->col('SELECT code || \' — \' || label FROM @TABLE WHERE id = ?;', $id); } public function getIdFromCode(string $code): int { return $this->em->col('SELECT id FROM @TABLE WHERE code = ? AND id_chart = ?;', $code, $this->chart_id); } /** * Return common accounting accounts from current chart * (will not return analytical and volunteering accounts) */ public function listCommonTypes(): array { | > > > > > | | | > > > | | | 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 | return EntityManager::getInstance(Account::class)->col('SELECT code || \' — \' || label FROM @TABLE WHERE id = ?;', $id); } public function getIdFromCode(string $code): int { return $this->em->col('SELECT id FROM @TABLE WHERE code = ? AND id_chart = ?;', $code, $this->chart_id); } static public function getCodeFromId(string $id): string { return EntityManager::getInstance(Account::class)->col('SELECT code FROM @TABLE WHERE id = ?;', $id); } /** * Return common accounting accounts from current chart * (will not return analytical and volunteering accounts) */ public function listCommonTypes(): array { return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? AND type != 0 AND type NOT IN (?) ORDER BY code COLLATE U_NOCASE;', $this->chart_id, Account::TYPE_ANALYTICAL); } /** * Return all accounts from current chart */ public function listAll(): array { return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? ORDER BY code COLLATE U_NOCASE;', $this->chart_id); } public function listForCodes(array $codes): array { return DB::getInstance()->getGrouped('SELECT code, id, label FROM acc_accounts WHERE id_chart = ?;', $this->chart_id); } /** * Return all accounts from current chart */ public function export(): \Generator { $res = $this->em->DB()->iterate($this->em->formatQuery('SELECT code, label, description, position, type, user AS added FROM @TABLE WHERE id_chart = ? ORDER BY code COLLATE U_NOCASE;'), $this->chart_id); foreach ($res as $row) { $row->type = Account::TYPES_NAMES[$row->type]; $row->position = Account::POSITIONS_NAMES[$row->position]; $row->added = $row->added ? 'Ajouté' : ''; yield $row; } } /** * Return only analytical accounts */ public function listAnalytical(): array { return $this->em->DB()->getAssoc($this->em->formatQuery('SELECT id, label FROM @TABLE WHERE id_chart = ? AND type = ? ORDER BY label COLLATE U_NOCASE;'), $this->chart_id, Account::TYPE_ANALYTICAL); } /** * Return only analytical accounts */ public function listVolunteering(): array { return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? AND type = ? ORDER BY code COLLATE U_NOCASE;', $this->chart_id, Account::TYPE_VOLUNTEERING); } /** * List common accounts, grouped by type * @return array */ |
︙ | ︙ | |||
123 124 125 126 127 128 129 | 'label' => $label, 'type' => $key, 'accounts' => [], ]; } } | | | | | | > > > | | | | | | | | | | | | > > | > | > > > > > > > > > | 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 | 'label' => $label, 'type' => $key, 'accounts' => [], ]; } } $query = $this->em->iterate('SELECT * FROM @TABLE WHERE id_chart = ? AND type != 0 ' . $types . ' ORDER BY type, code COLLATE U_NOCASE;', $this->chart_id); foreach ($query as $row) { if (!isset($out[$row->type])) { $out[$row->type] = (object) [ 'label' => Account::TYPES_NAMES[$row->type], 'type' => $row->type, 'accounts' => [], ]; } $out[$row->type]->accounts[] = $row; } return $out; } public function getNextCodeForType(int $type): string { $db = DB::getInstance(); $used_codes = $db->getAssoc(sprintf('SELECT code, code FROM %s WHERE type = ? AND user = 1 AND id_chart = ?;', Account::TABLE), $this->chart_id, $type); $used_codes = array_values($used_codes); $sql = sprintf('SELECT type, MIN(code) AS code, (SELECT COUNT(*) FROM %s WHERE user = 1 AND type = a.type) AS count FROM %1$s AS a WHERE id_chart = ? AND type = ? GROUP BY type;', Account::TABLE); $r = $db->first($sql, $this->chart_id, $type); if (!$r) { return ''; } $code = preg_replace('/[^\d]/', '', $r->code); $count = $r->count; $found = null; // Make sure we don't reuse an existing code while (!$found || in_array($found, $used_codes)) { // Get new account code, eg. 512A, 99AA, 99BZ etc. $letter = Utils::num2alpha($count++); $found = $code . $letter; } return $found; } static public function getPositionFromType(int $type): int { switch ($type) { case Account::TYPE_ANALYTICAL: return Account::NONE; case Account::TYPE_REVENUE; return Account::REVENUE; case Account::TYPE_EXPENSE; return Account::EXPENSE; case Account::TYPE_VOLUNTEERING: return Account::NONE; default: return Account::ASSET_OR_LIABILITY; } } public function copyFrom(int $id) { $db = DB::getInstance(); return $db->exec(sprintf('INSERT INTO %s (id_chart, code, label, description, position, type, user) SELECT %d, code, label, description, position, type, user FROM %1$s WHERE id_chart = %d;', Account::TABLE, $this->chart_id, $id)); |
︙ | ︙ | |||
212 213 214 215 216 217 218 219 220 221 222 223 224 225 | if (!isset($types[$row['type']])) { throw new ValidationException('Type inconnu : ' . $row['type']); } $row['position'] = $positions[$row['position']]; $row['type'] = $types[$row['type']]; $account->importForm($row); $account->save(); } catch (ValidationException $e) { throw new UserException(sprintf('Ligne %d : %s', $line, $e->getMessage())); } } | > > | 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | if (!isset($types[$row['type']])) { throw new ValidationException('Type inconnu : ' . $row['type']); } $row['position'] = $positions[$row['position']]; $row['type'] = $types[$row['type']]; $row['user'] = empty($row['added']) ? 0 : 1; $account->importForm($row); $account->save(); } catch (ValidationException $e) { throw new UserException(sprintf('Ligne %d : %s', $line, $e->getMessage())); } } |
︙ | ︙ | |||
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | return DB::getInstance()->count(Account::TABLE, 'id_chart = ? AND type = ?', $this->chart_id, $type); } public function getSingleAccountForType(int $type) { return DB::getInstance()->first('SELECT * FROM acc_accounts WHERE type = ? AND id_chart = ? LIMIT 1;', $type, $this->chart_id); } public function getClosingAccountId() { return DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE type = ? AND id_chart = ?;', Account::TYPE_CLOSING, $this->chart_id); } /* FIXME: implement closing of accounts public function closeRevenueExpenseAccounts(Year $year, int $user_id) { $closing_id = $this->getClosingAccountId(); if (!$closing_id) { | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | return DB::getInstance()->count(Account::TABLE, 'id_chart = ? AND type = ?', $this->chart_id, $type); } public function getSingleAccountForType(int $type) { return DB::getInstance()->first('SELECT * FROM acc_accounts WHERE type = ? AND id_chart = ? LIMIT 1;', $type, $this->chart_id); } public function getOpeningAccountId(): ?int { return DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE type = ? AND id_chart = ?;', Account::TYPE_OPENING, $this->chart_id) ?: null; } public function getClosingAccountId() { return DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE type = ? AND id_chart = ?;', Account::TYPE_CLOSING, $this->chart_id); } public function listUserAccounts(int $year_id): DynamicList { $id_field = Config::getInstance()->champ_identite; $columns = [ 'id' => [ 'select' => 'u.id', ], 'user_number' => [ 'select' => 'u.numero', 'label' => 'N° membre', ], 'user_identity' => [ 'select' => 'u.' . $id_field, 'label' => 'Membre', ], 'balance' => [ 'select' => 'SUM(l.debit - l.credit)', 'label' => 'Solde', //'order' => 'balance != 0 %s, balance < 0 %1$s', ], 'status' => [ 'select' => null, 'label' => 'Statut', ], ]; $tables = 'acc_transactions_users tu INNER JOIN membres u ON u.id = tu.id_user INNER JOIN acc_transactions t ON tu.id_transaction = t.id INNER JOIN acc_transactions_lines l ON t.id = l.id_transaction INNER JOIN acc_accounts a ON a.id = l.id_account'; $conditions = 'a.type = ' . Account::TYPE_THIRD_PARTY . ' AND t.id_year = ' . $year_id; $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('balance', false); $list->groupBy('u.id'); $list->setCount('COUNT(*)'); $list->setPageSize(null); $list->setExportCallback(function (&$row) { $row->balance = Utils::money_format($row->balance, '.', '', false); }); return $list; } /* FIXME: implement closing of accounts public function closeRevenueExpenseAccounts(Year $year, int $user_id) { $closing_id = $this->getClosingAccountId(); if (!$closing_id) { |
︙ | ︙ |
Added src/include/lib/Garradin/Accounting/AssistedReconciliation.php version [53fcb114af].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin\Accounting; use Garradin\CSV_Custom; use Garradin\UserException; use Garradin\Utils; use Garradin\Membres\Session; use Garradin\Entities\Accounting\Transaction; /** * Provides assisted reconciliation */ class AssistedReconciliation { const COLUMNS = [ 'label' => 'Libellé', 'date' => 'Date', //'notes' => 'Remarques', //'reference' => 'Numéro pièce comptable', //'p_reference' => 'Référence paiement', 'amount' => 'Montant', 'debit' => 'Débit', 'credit' => 'Crédit', 'balance' => 'Solde', ]; protected $csv; public function __construct() { $this->csv = new CSV_Custom(Session::getInstance(), 'acc_reconcile_csv'); $this->csv->setColumns(self::COLUMNS); $this->csv->setMandatoryColumns(['label', 'date']); $this->csv->setModifier(function (\stdClass $line): \stdClass { $date = \DateTime::createFromFormat('!d/m/Y', $line->date); if (!$date) { throw new UserException(sprintf('Date invalide : %s (format attendu : JJ/MM/AAAA)', $line->date)); } $line->date = $date; static $has_amount = null; if (null === $has_amount) { $has_amount = in_array('amount', $this->csv->getTranslationTable()); } if (!$has_amount) { $line->amount = $line->credit ?: '-' . ltrim($line->debit, '- \t\r\n'); } $line->amount = (substr($line->amount, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->amount); 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->format('Y-m-d'), 't' => $line->amount < 0 ? Transaction::TYPE_EXPENSE : Transaction::TYPE_REVENUE, ]); return $line; }); } public function csv(): CSV_Custom { return $this->csv; } public function setSettings(array $translation_table, int $skip): void { $this->csv->setTranslationTable($translation_table); if (!((in_array('credit', $translation_table) && in_array('debit', $translation_table)) || in_array('amount', $translation_table))) { throw new UserException('Il est nécessaire de sélectionner une colonne "montant" ou deux colonnes "débit" et "crédit"'); } $this->csv->skip($skip); } public function getStartAndEndDates(): ?array { $start = $end = null; if (!$this->csv->ready()) { return compact('start', 'end'); } foreach ($this->csv->iterate() as $line) { if (null === $start || $line->date < $start) { $start = $line->date; } if (null === $end || $line->date > $end) { $end = $line->date; } } return compact('start', 'end'); } public function mergeJournal(\Generator $journal) { $lines = []; $csv = iterator_to_array($this->csv->iterate()); $journal = iterator_to_array($journal); $i = 0; $sum = 0; foreach ($journal as $j) { $id = $j->date->format('Ymd') . '.' . $i++; $row = (object) ['csv' => null, 'journal' => $j]; if (isset($j->debit)) { foreach ($csv as &$line) { if (!isset($line->date)) { continue; } // Match date, amount and label if ($j->date->format('Ymd') == $line->date->format('Ymd') && ($j->credit * -1 == $line->amount || $j->debit == $line->amount) && strtolower($j->label) == strtolower($line->label)) { $row->csv = $line; $line = null; break; } } } $lines[$id] = $row; } unset($line, $row, $j); // Second round to match only amount and label foreach ($lines as $row) { if ($row->csv || !isset($row->journal->debit)) { continue; } $j = $row->journal; foreach ($csv as &$line) { if (!isset($line->date)) { continue; } if ($j->date->format('Ymd') == $line->date->format('Ymd') && ($j->credit * -1 == $line->amount || $j->debit == $line->amount)) { $row->csv = $line; $line = null; break; } } } unset($j); // Then add CSV lines on the right foreach ($csv as $line) { if (null == $line) { continue; } $id = $line->date->format('Ymd') . '.' . ($i++); $lines[$id] = (object) ['csv' => $line, 'journal' => null]; } ksort($lines); $prev = null; foreach ($lines as &$line) { $line->add = false; if (isset($line->csv)) { $sum += $line->csv->amount; $line->csv->running_sum = $sum; if ($prev && ($prev->date->format('Ymd') != $line->csv->date->format('Ymd') || $prev->label != $line->csv->label)) { $prev = null; } } if (isset($line->csv) && isset($line->journal)) { $prev = null; } if (isset($line->csv) && !isset($line->journal) && !$prev) { $line->add = true; $prev = $line->csv; } } return $lines; } } |
Modified src/include/lib/Garradin/Accounting/Charts.php from [729075d18b] to [1afa92c251].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Chart; use Garradin\Utils; use Garradin\DB; use KD2\DB\EntityManager; class Charts { static public function get(int $id) { return EntityManager::findOneById(Chart::class, $id); } static public function list() { $em = EntityManager::getInstance(Chart::class); return $em->all('SELECT * FROM @TABLE ORDER BY country, label;'); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < < | < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Chart; use Garradin\Utils; use Garradin\DB; use KD2\DB\EntityManager; use const Garradin\ROOT; class Charts { const BUNDLED_CHARTS = [ 'fr_pca_1999' => 'Plan comptable associatif 1999', 'fr_pca_2018' => 'Plan comptable associatif 2020 (Règlement ANC n°2018-06)', 'fr_pcg_2014' => 'Plan comptable général, pour entreprises (Règlement ANC n° 2014-03, consolidé 1er janvier 2019)', 'fr_cse_2015' => 'Plan comptable des CSE (Comité Social et Économique) (Règlement ANC n°2015-01)', 'fr_pcc_2020' => 'Plan comptable des copropriétés (2005 révisé en 2020)', 'be_pcmn_2019' => 'Plan comptable minimum normalisé des associations et fondations 2019', ]; static public function install(string $chart_code): Chart { if (!array_key_exists($chart_code, self::BUNDLED_CHARTS)) { throw new \InvalidArgumentException('Le plan comptable demandé n\'existe pas.'); } $file = sprintf('%s/include/data/charts/%s.csv', ROOT, $chart_code); if (!file_exists($file)) { throw new \LogicException('Le plan comptable demandé n\'a pas de fichier CSV'); } $country = strtoupper(substr($chart_code, 0, 2)); $code = strtoupper(substr($chart_code, 3)); if (DB::getInstance()->test(Chart::TABLE, 'country = ? AND code = ?', $country, $code)) { throw new \RuntimeException('Ce plan comptable est déjà installé'); } $chart = new Chart; $chart->label = self::BUNDLED_CHARTS[$chart_code]; $chart->country = $country; $chart->code = $code; $chart->save(); $chart->accounts()->importCSV($file); return $chart; } static public function listInstallable(): array { $installed = DB::getInstance()->getAssoc('SELECT id, LOWER(country || \'_\' || code) FROM acc_charts;'); $out = []; foreach (self::BUNDLED_CHARTS as $code => $label) { if (in_array($code, $installed)) { continue; } $out[$code] = sprintf('%s — %s', Utils::getCountryName(substr($code, 0, 2)), $label); } return $out; } static public function get(int $id) { return EntityManager::findOneById(Chart::class, $id); } static public function list() { $em = EntityManager::getInstance(Chart::class); return $em->all('SELECT * FROM @TABLE ORDER BY country, label;'); } static public function listByCountry(bool $filter_archived = false) { $where = $filter_archived ? ' AND archived = 0' : ''; $sql = sprintf('SELECT id, country, label FROM %s WHERE 1 %s ORDER BY country, code DESC, label;', Chart::TABLE, $where); $list = DB::getInstance()->getGrouped($sql); $out = []; foreach ($list as $row) { $country = Utils::getCountryName($row->country); if (!array_key_exists($country, $out)) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Graph.php from [b470030203] to [dabf37ba31].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Utils; use Garradin\Config; use Garradin\DB; | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Utils; use Garradin\Config; use Garradin\DB; use const Garradin\ADMIN_COLOR1; use const Garradin\ADMIN_COLOR2; use const Garradin\ADMIN_URL; use KD2\DB\EntityManager; use KD2\Graphics\SVG\Plot; use KD2\Graphics\SVG\Plot_Data; |
︙ | ︙ | |||
32 33 34 35 36 37 38 | ADMIN_URL . 'acc/reports/graph_pie.php?type=revenue&%s' => 'Répartition recettes', ADMIN_URL . 'acc/reports/graph_pie.php?type=expense&%s' => 'Répartition dépenses', ADMIN_URL . 'acc/reports/graph_pie.php?type=assets&%s' => 'Répartition actif', ]; const PLOT_TYPES = [ 'assets' => [ | | | < < < < < < < < < < < < < < < < < < < < < < < | 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 | ADMIN_URL . 'acc/reports/graph_pie.php?type=revenue&%s' => 'Répartition recettes', ADMIN_URL . 'acc/reports/graph_pie.php?type=expense&%s' => 'Répartition dépenses', ADMIN_URL . 'acc/reports/graph_pie.php?type=assets&%s' => 'Répartition actif', ]; const PLOT_TYPES = [ 'assets' => [ 'Total' => ['type' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING], 'exclude_position' => [Account::LIABILITY]], 'Banques' => ['type' => Account::TYPE_BANK], 'Caisses' => ['type' => Account::TYPE_CASH], 'En attente' => ['type' => Account::TYPE_OUTSTANDING, 'exclude_position' => [Account::LIABILITY]], ], 'result' => [ 'Recettes' => ['position' => Account::REVENUE], 'Dépenses' => ['position' => Account::EXPENSE], ], 'debts' => [ 'Comptes de tiers' => ['type' => Account::TYPE_THIRD_PARTY], ], ]; const PIE_TYPES = [ 'revenue' => ['position' => Account::REVENUE, 'exclude_type' => Account::TYPE_VOLUNTEERING], 'expense' => ['position' => Account::EXPENSE, 'exclude_type' => Account::TYPE_VOLUNTEERING], 'assets' => ['type' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]], ]; const WEEKLY_INTERVAL = 604800; // 7 days const MONTHLY_INTERVAL = 2635200; // 1 month static public function plot(string $type, array $criterias, int $interval = self::WEEKLY_INTERVAL, int $width = 700) { if (!array_key_exists($type, self::PLOT_TYPES)) { throw new \InvalidArgumentException('Unknown type'); } $plot = new Plot($width, 300); $lines = self::PLOT_TYPES[$type]; $data = []; foreach ($lines as $label => $line_criterias) { $line_criterias = array_merge($criterias, $line_criterias); |
︙ | ︙ | |||
138 139 140 141 142 143 144 | if ($i >= count($colors)) $i = 0; } } $out = $plot->output(); | < < < < < < < < | < | | | | < < < < < < < < < < < < < < < | < < | 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 | if ($i >= count($colors)) $i = 0; } } $out = $plot->output(); return $out; } static public function pie(string $type, array $criterias) { if (!array_key_exists($type, self::PIE_TYPES)) { throw new \InvalidArgumentException('Unknown type'); } $pie = new Pie(700, 300); $pie_criterias = self::PIE_TYPES[$type]; $data = Reports::getAccountsBalances(array_merge($criterias, $pie_criterias), 'balance DESC'); $others = 0; $colors = self::getColors(); $max = count($colors); $total = 0; $count = 0; $i = 0; foreach ($data as $row) { $total += $row->balance; } foreach ($data as $row) { if ($i++ >= $max || $count > $total*0.95) { $others += $row->balance; } else { $label = strlen($row->label) > 40 ? substr($row->label, 0, 38) . '…' : $row->label; $pie->add(new Pie_Data(abs($row->balance) / 100, $label, $colors[$i-1])); } $count += $row->balance; } if ($others != 0) { $pie->add(new Pie_Data(abs($others) / 100, 'Autres', '#ccc')); } $pie->togglePercentage(true); $out = $pie->output(); return $out; } static public function bar(string $type, array $criterias) { if (!array_key_exists($type, self::PLOT_TYPES)) { throw new \InvalidArgumentException('Unknown type'); } $bar = new Bar(600, 300); $lines = self::PLOT_TYPES[$type]; $data = []; $colors = self::getColors(); foreach ($lines as $label => $line_criterias) { $color = current($colors); next($colors); $line_criterias = array_merge($criterias, $line_criterias); $years = Reports::getSumsPerYear($line_criterias); if (count($years) < 1) { continue; } foreach ($years as $year) { $start = Utils::date_fr($year->start_date, 'Y'); $end = Utils::date_fr($year->end_date, 'Y'); $year_label = $start == $end ? $start : sprintf('%s-%s', $start, substr($end, -2)); $year_id = $year_label . '-' . $year->id; if (!isset($data[$year_id])) { $data[$year_id] = new Bar_Data_Set($year_label); } $data[$year_id]->add((int) $year->balance / 100, $label, $color); } } ksort($data); foreach ($data as $group) { $bar->add($group); } $out = $bar->output(); return $out; } static protected function getColors() { $config = Config::getInstance(); $c1 = $config->get('couleur1') ?: ADMIN_COLOR1; |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Reports.php from [4b7b6a6df1] to [31315d71f7].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Utils; use Garradin\DB; use KD2\DB\EntityManager; class Reports { | | > > > > | < | > < | > | | | | | | | > > > > | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Utils; use Garradin\DB; use KD2\DB\EntityManager; class Reports { static public function getWhereClause(array $criterias, string $transactions_alias = '', string $lines_alias = '', string $accounts_alias = ''): string { $where = []; $transactions_alias = $transactions_alias ? $transactions_alias . '.' : ''; $lines_alias = $lines_alias ? $lines_alias . '.' : ''; $accounts_alias = $accounts_alias ? $accounts_alias . '.' : ''; if (!empty($criterias['year'])) { $where[] = sprintf($transactions_alias . 'id_year = %d', $criterias['year']); } if (!empty($criterias['position'])) { $criterias['position'] = array_map('intval', (array)$criterias['position']); $where[] = sprintf($accounts_alias . 'position IN (%s)', implode(',', $criterias['position'])); } if (!empty($criterias['exclude_position'])) { $criterias['exclude_position'] = array_map('intval', (array)$criterias['exclude_position']); $where[] = sprintf($accounts_alias . 'position NOT IN (%s)', implode(',', $criterias['exclude_position'])); } if (!empty($criterias['type'])) { $criterias['type'] = array_map('intval', (array)$criterias['type']); $where[] = sprintf($accounts_alias . 'type IN (%s)', implode(',', $criterias['type'])); } if (!empty($criterias['exclude_type'])) { $criterias['exclude_type'] = array_map('intval', (array)$criterias['exclude_type']); $where[] = sprintf($accounts_alias . 'type NOT IN (%s)', implode(',', $criterias['exclude_type'])); } if (!empty($criterias['user'])) { $where[] = sprintf($transactions_alias . 'id IN (SELECT id_transaction FROM acc_transactions_users WHERE id_user = %d)', $criterias['user']); } if (!empty($criterias['creator'])) { $where[] = sprintf($transactions_alias . 'id_creator = %d', $criterias['creator']); } if (!empty($criterias['subscription'])) { $where[] = sprintf($transactions_alias . 'id IN (SELECT tu.id_transaction FROM acc_transactions_users tu WHERE id_service_user = %d)', $criterias['subscription']); } if (!empty($criterias['analytical'])) { $where[] = sprintf($lines_alias . 'id_analytical = %d', $criterias['analytical']); } if (!empty($criterias['account'])) { $where[] = sprintf($accounts_alias . 'id = %d', $criterias['account']); } if (!empty($criterias['analytical_only'])) { $where[] = $lines_alias . 'id_analytical IS NOT NULL'; } if (!empty($criterias['has_type'])) { $where[] = $accounts_alias . 'type != 0'; } if (!count($where)) { throw new \LogicException('Unknown criteria'); } return implode(' AND ', $where); |
︙ | ︙ | |||
87 88 89 90 91 92 93 | FROM acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction INNER JOIN acc_accounts a ON a.id = l.id_analytical INNER JOIN acc_years y ON y.id = t.id_year GROUP BY %s ORDER BY %s;'; | | | | | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | FROM acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction INNER JOIN acc_accounts a ON a.id = l.id_analytical INNER JOIN acc_years y ON y.id = t.id_year GROUP BY %s ORDER BY %s;'; $order = $order_code ? 'a.code COLLATE U_NOCASE' : 'a.label COLLATE U_NOCASE'; if ($by_year) { $group = 'y.id, a.code'; $order = 'y.start_date DESC, ' . $order; } else { $group = 'a.code, y.id'; $order = $order . ', y.id'; } $sql = sprintf($sql, Account::EXPENSE, Account::REVENUE, $group, $order); $current = null; |
︙ | ︙ | |||
121 122 123 124 125 126 127 | $out->{$s} = $current->{$s}; } return $out; }; foreach (DB::getInstance()->iterate($sql) as $row) { | | | > | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | $out->{$s} = $current->{$s}; } return $out; }; foreach (DB::getInstance()->iterate($sql) as $row) { $id = $by_year ? $row->id_year : $row->account_code; if (null !== $current && $current->selector !== $id) { $current->items[] = $total($current, $by_year); yield $current; $current = null; } if (null === $current) { $current = (object) [ 'selector' => $id, 'id' => $by_year ? $row->id_year : $row->id_account, 'label' => $by_year ? $row->year_label : ($order_code ? $row->account_code . ' - ' : '') . $row->account_label, 'description' => !$by_year ? $row->account_description : null, 'items' => [] ]; foreach ($sums as $s) { |
︙ | ︙ | |||
164 165 166 167 168 169 170 | yield $current; } static public function getSumsPerYear(array $criterias): array { $where = self::getWhereClause($criterias); | | | < < | | | | 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 | yield $current; } static public function getSumsPerYear(array $criterias): array { $where = self::getWhereClause($criterias); $sql = sprintf('SELECT y.id, y.start_date, y.end_date, y.label, SUM(b.balance) AS balance FROM acc_accounts_balances b INNER JOIN acc_years y ON y.id = b.id_year WHERE %s GROUP BY b.id_year ORDER BY y.end_date;', $where); return DB::getInstance()->getGrouped($sql); } static public function getSumsByInterval(array $criterias, int $interval) { $where = self::getWhereClause($criterias, 't', 'l', 'a'); $where_interval = !empty($criterias['year']) ? sprintf(' WHERE id_year = %d', $criterias['year']) : ''; $db = DB::getInstance(); $sql = sprintf('SELECT strftime(\'%%s\', MIN(date)) / %d AS start_interval, strftime(\'%%s\', MAX(date)) / %1$d AS end_interval |
︙ | ︙ | |||
229 230 231 232 233 234 235 | unset($v); return $out; } static public function getResult(array $criterias): int { | > | > | > | < < > | < | > | | | | < | > > > > > > > | | < > | | | | | | | > | > > | > | | | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > | > > | > > > > > > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > | > > | > > > > > | > | | > > > > | | > > | > > > > > > | > > | > > | > > > > > > > > > > > > | < < < | | < < | < < < | < < < < < | < < < < < < < < < < < | < < < < | < | | | | < < < < < < < | < < < < < < | < < < | | < < | < < < < < < < | | < < < | 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 | unset($v); return $out; } static public function getResult(array $criterias): int { if (!empty($criterias['analytical']) || !empty($criterias['analytical_only'])) { $where = self::getWhereClause($criterias, 't', 'l', 'a'); $sql = self::getBalancesSQL(['inner_select' => 'l.id_analytical', 'inner_where' => $where]); $sql = sprintf('SELECT position, SUM(balance) FROM (%s) GROUP BY position;', $sql); } 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, balance, is_debt FROM ( SELECT %s t.id_year, a.id, a.label, a.code, a.type, SUM(l.credit) AS credit, SUM(l.debit) AS debit, CASE -- 3 = dynamic asset or liability depending on balance WHEN position = 3 AND SUM(l.debit - l.credit) > 0 THEN 1 -- 1 = Asset (actif) comptes fournisseurs, tiers créditeurs WHEN position = 3 THEN 2 -- 2 = Liability (passif), comptes clients, tiers débiteurs ELSE position END AS position, CASE WHEN position IN (1, 4) -- 1 = asset, 4 = expense OR (position = 3 AND SUM(l.debit - l.credit) > 0) THEN SUM(l.debit - l.credit) ELSE SUM(l.credit - l.debit) END AS balance, CASE WHEN SUM(l.debit - l.credit) > 0 THEN 1 ELSE 0 END AS is_debt FROM acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction INNER JOIN acc_accounts a ON a.id = l.id_account %s %s GROUP BY %s ) %s %s ORDER BY %s', isset($parts['select']) ? $parts['select'] . ',' : '', isset($parts['inner_select']) ? $parts['inner_select'] . ',' : '', $parts['inner_join'] ?? '', isset($parts['inner_where']) ? 'WHERE ' . $parts['inner_where'] : '', $parts['inner_group'] ?? 'a.id, t.id_year', isset($parts['where']) ? 'WHERE ' . $parts['where'] : '', isset($parts['group']) ? 'GROUP BY ' . $parts['group'] : '', $order ?? 'code' ); } /** * Returns SQL query for accounts balances according to $criterias * @param array $criterias List of criterias, see self::getWhereClause * @param string|null $order Order of rows (SQL clause), if NULL will order by CODE * @param bool $remove_zero Remove accounts where the balance is zero from the list */ static protected function getAccountsBalancesInnerSQL(array $criterias, ?string $order = null, bool $remove_zero = true): string { $group = 'code'; $having = ''; if ($remove_zero) { $having = 'HAVING balance != 0'; } $table = null; if (empty($criterias['analytical']) && empty($criterias['user']) && empty($criterias['creator']) && empty($criterias['subscription'])) { $table = 'acc_accounts_balances'; } // Specific queries that can't rely on acc_accounts_balances if (!$table) { $where = null; // The position if (!empty($criterias['position'])) { $criterias['position'] = (array)$criterias['position']; if (in_array(Account::LIABILITY, $criterias['position']) || in_array(Account::ASSET, $criterias['position'])) { $where = self::getWhereClause(['position' => $criterias['position']]); $criterias['position'][] = Account::ASSET_OR_LIABILITY; } } $inner_where = self::getWhereClause($criterias, 't', 'l', 'a'); $remove_zero = $remove_zero ? ', ' . $remove_zero : ''; $inner_group = empty($criterias['year']) ? 'a.id' : null; $sql = self::getBalancesSQL(['group' => 'code ' . $having] + compact('order', 'inner_where', 'where', 'inner_group')); } else { $where = self::getWhereClause($criterias); $query = 'SELECT *, SUM(credit) AS credit, SUM(debit) AS debit, SUM(balance) AS balance FROM %s WHERE %s GROUP BY %s %s ORDER BY %s'; $sql = sprintf($query, $table, $where, $group, $having, $order); } return $sql; } /** * Returns accounts balances according to $criterias * @param array $criterias List of criterias, see self::getWhereClause * @param string|null $order Order of rows (SQL clause), if NULL will order by CODE * @param bool $remove_zero Remove accounts where the balance is zero from the list */ static public function getAccountsBalances(array $criterias, ?string $order = null, bool $remove_zero = true): array { $db = DB::getInstance(); $order = $order ?: 'code COLLATE NOCASE'; $sql = self::getAccountsBalancesInnerSQL($criterias, $order, $remove_zero); // SQLite does not support OUTER JOIN yet :( if (isset($criterias['compare_year'])) { $criterias2 = array_merge($criterias, ['year' => $criterias['compare_year']]); $sql2 = self::getAccountsBalancesInnerSQL($criterias2, $order, true); $sql_union = 'SELECT a.id, a.code AS code, a.label, a.position, a.type, a.debit, a.credit, a.balance, IFNULL(b.balance, 0) AS balance2, IFNULL(a.balance - b.balance, a.balance) AS change FROM (%1$s) AS a LEFT JOIN %3$s b ON b.code = a.code AND a.position = b.position AND b.id_year = %4$d UNION ALL -- Select balances of second year accounts that are =zero in first year SELECT NULL AS id, c.code AS code, c.label, c.position, c.type, c.debit, c.credit, 0 AS balance, c.balance AS balance2, c.balance * -1 AS change FROM (%2$s) AS c LEFT JOIN %3$s d ON d.code = c.code AND d.balance != 0 AND d.position = c.position AND d.id_year = %5$d WHERE d.id IS NULL ORDER BY code COLLATE NOCASE;'; $sql = sprintf($sql_union, $sql, $sql2, 'acc_accounts_balances', $criterias['compare_year'], $criterias['year']); } $out = $db->get($sql); return $out; } static public function getTrialBalance(array $criterias, bool $simple = false): \Iterator { unset($criterias['compare_year']); $out = self::getAccountsBalances($criterias, null, false); $sums = [ 'debit' => 0, 'credit' => 0, 'balance' => null, 'label' => 'Total', ]; foreach ($out as $row) { if (!$simple) { $row->balance = $row->debit - $row->credit; } $sums['debit'] += $row->debit; $sums['credit'] += $row->credit; yield $row; } yield (object) $sums; } /** * Return a table line with the year result */ static public function getResultLine(array $criterias): \stdClass { $balance = self::getResult($criterias); $balance2 = null; $change = null; $label = $balance > 0 ? 'Résultat de l\'exercice courant (excédent)' : 'Résultat de l\'exercice courant (perte)'; if (!empty($criterias['compare_year'])) { $balance2 = self::getResult(array_merge($criterias, ['year' => $criterias['compare_year']])); $change = $balance - $balance2; } if (!empty($criterias['compare_year']) || $balance == 0) { $label = 'Résultat de l\'exercice'; } return (object) compact('balance', 'balance2', 'label', 'change'); } /** * Return a table line with totals */ static public function getTotalLine(array $rows, string $label = 'Total'): \stdClass { $balance = 0; $balance2 = 0; $change = 0; foreach ($rows as $row) { $balance += $row->balance; $balance2 += $row->balance2 ?? 0; $change += $row->change ?? 0; } return (object) compact('label', 'balance', 'balance2', 'change'); } /** * Statement / Compte de résultat */ static public function getStatement(array $criterias): \stdClass { $out = new \stdClass; $out->caption_left = 'Charges'; $out->caption_right = 'Produits'; $out->body_left = self::getAccountsBalances($criterias + ['position' => Account::EXPENSE]); $out->body_right = self::getAccountsBalances($criterias + ['position' => Account::REVENUE]); $out->foot_left = [self::getTotalLine($out->body_left, 'Total charges')]; $out->foot_right = [self::getTotalLine($out->body_right, 'Total produits')]; $r = self::getResultLine($criterias); if ($r->balance < 0) { // Deficit should go to expense column $out->foot_left[] = $r; } else { $out->foot_right[] = $r; } return $out; } /** * Bilan / Balance sheet */ static public function getBalanceSheet(array $criterias): \stdClass { $out = new \stdClass; $out->caption_left = 'Actif'; $out->caption_right = 'Passif'; $out->body_left = self::getAccountsBalances($criterias + ['position' => Account::ASSET]); $out->body_right = self::getAccountsBalances($criterias + ['position' => Account::LIABILITY]); // Append result to liability $r = self::getResultLine($criterias); $out->body_right[] = $r; // Calculate the total sum for assets and liabilities $out->foot_left = [self::getTotalLine($out->body_left, 'Total actif')]; $out->foot_right = [self::getTotalLine($out->body_right, 'Total passif')]; return $out; } /** * Return list of favorite accounts (accounts with a type), grouped by type, with their current sum * @return \Generator list of accounts grouped by type */ static public function getClosingSumsFavoriteAccounts(array $criterias): \Generator { $types = [Account::TYPE_EXPENSE, Account::TYPE_REVENUE, Account::TYPE_BANK, Account::TYPE_OUTSTANDING, Account::TYPE_CASH, Account::TYPE_THIRD_PARTY, Account::TYPE_VOLUNTEERING]; $accounts = self::getAccountsBalances($criterias + ['type' => $types], 'type, code COLLATE NOCASE', false); $group = null; foreach ($accounts as $row) { if (null !== $group && $row->type !== $group->type) { yield $group; $group = null; } if (null === $group) { $group = (object) [ 'label' => Account::TYPES_NAMES[$row->type], 'type' => $row->type, 'accounts' => [] ]; } $group->accounts[] = $row; } if (null !== $group) { yield $group; } } |
︙ | ︙ | |||
427 428 429 430 431 432 433 | t.id_year, a.id AS id_account, t.id, t.date, t.reference, l.debit, l.credit, l.reference AS line_reference, t.label, l.label AS line_label, a.label AS account_label, a.code AS account_code FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id INNER JOIN %s WHERE %s | | | 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 | t.id_year, a.id AS id_account, t.id, t.date, t.reference, l.debit, l.credit, l.reference AS line_reference, t.label, l.label AS line_label, a.label AS account_label, a.code AS account_code FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id INNER JOIN %s WHERE %s ORDER BY a.code COLLATE U_NOCASE, t.date, t.id;', $join, $where); $account = null; $debit = $credit = 0; foreach ($db->iterate($sql) as $row) { if (null !== $account && $account->id != $row->id_account) { yield $account; |
︙ | ︙ | |||
477 478 479 480 481 482 483 | $account->all_credit = $credit; yield $account; } static public function getJournal(array $criterias): \Generator { | | | 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 | $account->all_credit = $credit; yield $account; } static public function getJournal(array $criterias): \Generator { $where = self::getWhereClause($criterias, 't', 'l', 'a'); $sql = sprintf('SELECT t.id_year, l.id_account, l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, a.label AS account_label, a.code AS account_code FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id |
︙ | ︙ | |||
525 526 527 528 529 530 531 | if (null === $transaction) { return; } yield $transaction; } | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 666 667 668 669 670 671 672 673 | if (null === $transaction) { return; } yield $transaction; } } |
Modified src/include/lib/Garradin/Accounting/Transactions.php from [31743804a6] to [7f3d458a38].
︙ | ︙ | |||
13 14 15 16 17 18 19 | use Garradin\DB; use Garradin\DynamicList; use Garradin\Utils; use Garradin\UserException; class Transactions { | | > > > | > > > > > > > > > > > > | > > > > > | > > > > > | > > > | > > | | | | | | | | > > > | > > > > > > > > > > > > > > > > | 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | use Garradin\DB; use Garradin\DynamicList; use Garradin\Utils; use Garradin\UserException; class Transactions { const EXPORT_FULL = 'full'; const EXPORT_GROUPED = 'grouped'; const EXPORT_SIMPLE = 'simple'; const EXPORT_NAMES = [ self::EXPORT_FULL => 'Complet', self::EXPORT_GROUPED => 'Groupé', self::EXPORT_SIMPLE => 'Simplifié', ]; const EXPORT_COLUMNS_FULL = [ 'id' => 'Numéro d\'écriture', 'type' => 'Type', 'status' => 'Statut', 'label' => 'Libellé', 'date' => 'Date', 'notes' => 'Remarques', 'reference' => 'Numéro pièce comptable', // Lines 'line_id' => 'Numéro ligne', 'account' => 'Compte', 'debit' => 'Débit', 'credit' => 'Crédit', 'line_reference' => 'Référence ligne', 'line_label' => 'Libellé ligne', 'reconciled' => 'Rapprochement', 'analytical' => 'Compte analytique', 'linked_users' => 'Membres associés', ]; const EXPORT_COLUMNS = [ self::EXPORT_GROUPED => self::EXPORT_COLUMNS_FULL, self::EXPORT_FULL => self::EXPORT_COLUMNS_FULL, self::EXPORT_SIMPLE => [ 'id' => 'Numéro d\'écriture', 'type' => 'Type', 'status' => 'Statut', 'label' => 'Libellé', 'date' => 'Date', 'notes' => 'Remarques', 'reference' => 'Numéro pièce comptable', 'p_reference' => 'Référence paiement', 'debit_account' => 'Compte de débit', 'credit_account' => 'Compte de crédit', 'amount' => 'Montant', 'analytical' => 'Compte analytique', 'linked_users' => 'Membres associés', ], ]; const MANDATORY_COLUMNS = [ self::EXPORT_GROUPED => [ 'type', 'label', 'date', 'account', 'credit', 'debit', ], self::EXPORT_SIMPLE => [ 'label', 'date', 'credit_account', 'debit_account', 'amount' ], ]; static public function get(int $id) { return EntityManager::findOneById(Transaction::class, $id); } static public function saveReconciled(\Generator $journal, ?array $checked) |
︙ | ︙ | |||
81 82 83 84 85 86 87 88 89 90 91 92 93 94 | } $ids[] = (int)$row->id; $line = new Line; $line->importForm([ 'reference' => $row->line_reference, 'id_account' => $row->id_account, ]); $line->credit = $row->debit; $transaction->addLine($line); } | > | 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | } $ids[] = (int)$row->id; $line = new Line; $line->importForm([ 'reference' => $row->line_reference, 'label' => $row->line_label, 'id_account' => $row->id_account, ]); $line->credit = $row->debit; $transaction->addLine($line); } |
︙ | ︙ | |||
113 114 115 116 117 118 119 | { return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id); } /** * Return all transactions from year */ | | | | | | | | | | | | | | | | | < | > > > | > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | > > > > > | | > | > > > > | | > > > | > > > > > > > > | | > > > > > > > > > > > > > < < < < | | 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 | { return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id); } /** * Return all transactions from year */ static public function export(Year $year, string $format, string $type): void { $header = null; if (!array_key_exists($type, self::EXPORT_COLUMNS)) { throw new \InvalidArgumentException('Unknown type: ' . $type); } CSV::export( $format, sprintf('Export comptable %s - %s - %s', strtolower(self::EXPORT_NAMES[$type]), Config::getInstance()->get('nom_asso'), $year->label), self::iterateExport($year->id(), $type), array_values(self::EXPORT_COLUMNS[$type]) ); } static public function getExportExamples(Year $year) { $out = []; foreach (self::EXPORT_NAMES as $type => $label) { $i = 0; $out[$type] = [self::EXPORT_COLUMNS[$type]]; foreach (self::iterateExport($year->id(), $type) as $row) { $out[$type][] = $row; if (++$i > 1) { break; } } } return $out; } static protected function iterateExport(int $year_id, string $type): \Generator { $id_field = Config::getInstance()->get('champ_identite'); if (self::EXPORT_SIMPLE == $type) { $sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference, l1.reference AS p_reference, a1.code AS debit_account, a2.code AS credit_account, l1.debit AS amount, a3.code AS analytical, GROUP_CONCAT(u.%s) AS linked_users FROM acc_transactions t INNER JOIN acc_transactions_lines l1 ON l1.id_transaction = t.id AND l1.debit != 0 INNER JOIN acc_transactions_lines l2 ON l2.id_transaction = t.id AND l2.credit != 0 INNER JOIN acc_accounts a1 ON a1.id = l1.id_account INNER JOIN acc_accounts a2 ON a2.id = l2.id_account LEFT JOIN acc_accounts a3 ON a3.id = l1.id_analytical LEFT JOIN acc_transactions_users tu ON tu.id_transaction = t.id LEFT JOIN membres u ON u.id = tu.id_user WHERE t.id_year = ? AND t.type != %d GROUP BY t.id ORDER BY t.date, t.id;'; $sql = sprintf($sql, $id_field, Transaction::TYPE_ADVANCED); } else { $sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference, l.id AS line_id, a.code AS account, l.debit AS debit, l.credit AS credit, l.reference AS line_reference, l.label AS line_label, l.reconciled, a2.code AS analytical, GROUP_CONCAT(u.%s) AS linked_users FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id INNER JOIN acc_accounts a ON a.id = l.id_account LEFT JOIN acc_accounts a2 ON a2.id = l.id_analytical LEFT JOIN acc_transactions_users tu ON tu.id_transaction = t.id LEFT JOIN membres u ON u.id = tu.id_user WHERE t.id_year = ? GROUP BY t.id, l.id ORDER BY t.date, t.id, l.id;'; $sql = sprintf($sql, $id_field); } $res = DB::getInstance()->iterate($sql, $year_id); $previous_id = null; foreach ($res as $row) { if ($previous_id === $row->id && $type == self::EXPORT_GROUPED) { // Remove transaction data to differentiate lines and transactions $row->id = $row->type = $row->status = $row->label = $row->date = $row->notes = $row->reference = null; } else { $row->type = Transaction::TYPES_NAMES[$row->type]; $status = []; foreach (Transaction::STATUS_NAMES as $k => $v) { if ($row->status & $k) { $status[] = $v; } } $row->status = implode(', ', $status); $row->date = \DateTime::createFromFormat('Y-m-d', $row->date); $row->date = $row->date->format('d/m/Y'); $previous_id = $row->id; } if ($type == self::EXPORT_SIMPLE) { $row->amount = Utils::money_format($row->amount, ',', ''); } else { $row->credit = Utils::money_format($row->credit, ',', ''); $row->debit = Utils::money_format($row->debit, ',', ''); } yield $row; } } static public function import(string $type, Year $year, CSV_Custom $csv, int $user_id, bool $ignore_ids = false) { if ($type != self::EXPORT_GROUPED && $type != self::EXPORT_SIMPLE) { throw new \InvalidArgumentException('Invalid type value'); } if ($year->closed) { throw new \InvalidArgumentException('Closed year'); } $db = DB::getInstance(); $db->begin(); $accounts = $year->accounts(); $transaction = null; $types = array_flip(Transaction::TYPES_NAMES); $l = 1; try { foreach ($csv->iterate() as $l => $row) { $row = (object) $row; // Import grouped transactions if ($type == self::EXPORT_GROUPED) { // If a line doesn't have any transaction info: this is a line following the previous transaction $has_transaction = !(empty($row->id) && empty($row->type) && empty($row->status) && empty($row->label) && empty($row->date) && empty($row->notes) && empty($row->reference) ); // New transaction, save previous one if (null !== $transaction && $has_transaction) { $transaction->save(); $transaction = null; } if (!$has_transaction && null === $transaction) { throw new UserException('cette ligne n\'est reliée à aucune écriture'); } } else { if (empty($row->type)) { $row->type = Transaction::TYPES_NAMES[Transaction::TYPE_ADVANCED]; } $transaction = null; } // Find or create transaction if (null === $transaction) { if (!empty($row->id) && !$ignore_ids) { $transaction = self::get((int)$row->id); if (!$transaction) { throw new UserException(sprintf('l\'écriture #%d est introuvable', $row->id)); } if ($transaction->id_year != $year->id()) { |
︙ | ︙ | |||
241 242 243 244 245 246 247 | throw new UserException(sprintf('le type "%s" est inconnu', $row->type)); } $transaction->type = $types[$row->type]; $fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference'])); $transaction->importForm($fields); | | > > > > < > > > | > | < > | | | < | < < < < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | > | | > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | throw new UserException(sprintf('le type "%s" est inconnu', $row->type)); } $transaction->type = $types[$row->type]; $fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference'])); $transaction->importForm($fields); // Set status if (!empty($row->status)) { $status_list = array_map('trim', explode(',', $row->status)); $status = 0; foreach (Transaction::STATUS_NAMES as $k => $v) { if (in_array($v, $status_list)) { $status |= $k; } } $transaction->status = $status; } } $data = []; if (!empty($row->analytical)) { $id_analytical = $accounts->getIdFromCode($row->analytical); if (!$id_analytical) { throw new UserException(sprintf('le compte analytique "%s" n\'existe pas dans le plan comptable', $row->analytical)); } $data['id_analytical'] = $id_analytical; } elseif (property_exists($row, 'analytical')) { $data['id_analytical'] = null; } // Add two transaction lines for each CSV line if ($type == self::EXPORT_SIMPLE) { $credit_account = $accounts->getIdFromCode($row->credit_account); $debit_account = $accounts->getIdFromCode($row->debit_account); if (!$credit_account) { throw new UserException(sprintf('Compte de crédit "%s" inconnu dans le plan comptable', $row->credit_account)); } if (!$debit_account) { throw new UserException(sprintf('Compte de débit "%s" inconnu dans le plan comptable', $row->debit_account)); } $data['reference'] = isset($row->p_reference) ? $row->p_reference : null; if (!$transaction->exists()) { $l1 = new Line; $l2 = new Line; $transaction->addLine($l1); $transaction->addLine($l2); } else { $lines = $transaction->getLines(); if (count($lines) != 2) { throw new UserException('cette écriture comporte plus de deux lignes et ne peut donc être modifiée par un import simplifié'); } // Find correct debit/credit lines if ($lines[0]->credit != 0) { $l1 = $lines[0]; $l2 = $lines[1]; } else { $l1 = $lines[1]; $l2 = $lines[0]; } } $l1->importForm($data + [ 'credit' => $row->amount, 'debit' => 0, 'id_account' => $credit_account, ]); $l2->importForm($data + [ 'credit' => 0, 'debit' => $row->amount, 'id_account' => $debit_account, ]); $transaction->save(); $transaction = null; } else { $id_account = $accounts->getIdFromCode($row->account); if (!$id_account) { throw new UserException(sprintf('le compte "%s" n\'existe pas dans le plan comptable', $row->account)); } $data = $data + [ 'credit' => $row->credit ?: 0, 'debit' => $row->debit ?: 0, 'id_account' => $id_account, 'reference' => $row->line_reference ?? null, 'label' => $row->line_label ?? null, 'reconciled' => $row->reconciled ?? false, ]; if (!empty($row->line_id) && !$ignore_ids) { $line = $transaction->getLine((int)$row->line_id); if (!$line) { throw new UserException(sprintf('le numéro de ligne "%s" n\'existe pas dans l\'écriture "%s"', $row->line_id, $transaction->id ?: 'à créer')); } } else { $line = new Line; } $line->importForm($data); // If a line_id was supplied, just changing the object is enough, no need to add it to the transaction if (empty($row->line_id)) { $transaction->addLine($line); } } } if (null !== $transaction) { $transaction->save(); } } catch (UserException $e) { $db->rollback(); $e->setMessage(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage())); if (null !== $transaction) { $e->setDetails($transaction->asDetailsArray()); } throw $e; } $db->commit(); } static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null) { $db = DB::getInstance(); if (null !== $id_analytical && !$db->test(Account::TABLE, 'type = ? AND id = ?', Account::TYPE_ANALYTICAL, $id_analytical)) { |
︙ | ︙ | |||
419 420 421 422 423 424 425 | $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_analytical = %s WHERE %s;', (int)$id_analytical ?: 'NULL', $where)); } | | > > > > > > > > | > > > > > > > > | 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 | $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_analytical = %s WHERE %s;', (int)$id_analytical ?: '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); $columns['change']['label'] = 'Montant'; $columns['code_analytical']['select'] = 'GROUP_CONCAT(b.code, \',\')'; $columns['id_analytical']['select'] = 'GROUP_CONCAT(l.id_analytical, \',\')'; if (!$type) { $columns = ['type_label' => [ 'select' => 't.type', 'label' => 'Type d\'écriture', ]] + $columns; } $tables = 'acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction INNER JOIN acc_accounts a ON a.id = l.id_account LEFT JOIN acc_accounts b ON b.id = l.id_analytical'; $conditions = sprintf('t.id_year = %d', $year_id); if (null !== $type) { $conditions .= sprintf(' AND t.type = %s', $type); } $sum = 0; $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->id_analytical, $row->code_analytical)) { $row->code_analytical = array_combine(explode(',', $row->id_analytical), explode(',', $row->code_analytical)); } else { $row->code_analytical = []; } 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); $row->code_analytical = implode(', ', $row->code_analytical); }); return $list; } } |
Modified src/include/lib/Garradin/Accounting/Years.php from [d161a36703] to [03b82c27e8].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Year; use Garradin\Utils; use Garradin\DB; use KD2\DB\EntityManager; class Years { | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin\Accounting; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Line; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Accounting\Year; use Garradin\Utils; use Garradin\DB; use KD2\DB\EntityManager; class Years { |
︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 41 42 43 44 45 | } static public function listOpenAssocExcept(int $id) { $db = EntityManager::getInstance(Year::class)->DB(); return $db->getAssoc('SELECT id, label FROM acc_years WHERE closed = 0 AND id != ? ORDER BY end_date;', $id); } static public function listAssoc() { return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years ORDER BY end_date;'); } static public function listClosedAssoc() | > > > > > > | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | } static public function listOpenAssocExcept(int $id) { $db = EntityManager::getInstance(Year::class)->DB(); return $db->getAssoc('SELECT id, label FROM acc_years WHERE closed = 0 AND id != ? ORDER BY end_date;', $id); } static public function listOpenAssoc() { $db = EntityManager::getInstance(Year::class)->DB(); return $db->getAssoc('SELECT id, label FROM acc_years WHERE closed = 0 ORDER BY end_date DESC;'); } static public function listAssoc() { return DB::getInstance()->getAssoc('SELECT id, label FROM acc_years ORDER BY end_date;'); } static public function listClosedAssoc() |
︙ | ︙ | |||
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 | return DB::getInstance()->get(sprintf('SELECT y.*, (SELECT COUNT(*) FROM acc_transactions WHERE id_year = y.id) AS nb_transactions, c.label AS chart_name FROM acc_years y INNER JOIN acc_charts c ON c.id = y.id_chart ORDER BY end_date %s;', $desc)); } static public function getNewYearDates(): array { $last_year = EntityManager::findOne(Year::class, 'SELECT * FROM @TABLE ORDER BY end_date DESC LIMIT 1;'); if ($last_year) { $start_date = clone $last_year->start_date; $start_date->modify('+1 year'); $end_date = clone $last_year->end_date; $end_date->modify('+1 year'); } else { $start_date = new \DateTime; $end_date = clone $start_date; $end_date->modify('+1 year'); } return [$start_date, $end_date]; } | > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | return DB::getInstance()->get(sprintf('SELECT y.*, (SELECT COUNT(*) FROM acc_transactions WHERE id_year = y.id) AS nb_transactions, c.label AS chart_name FROM acc_years y INNER JOIN acc_charts c ON c.id = y.id_chart ORDER BY end_date %s;', $desc)); } static public function listLastTransactions(int $count, array $years): array { $out = []; foreach ($years as $year) { $out[$year->id] = Transactions::listByType($year->id, null); $out[$year->id]->setPageSize($count); $out[$year->id]->orderBy('id', true); } return $out; } static public function getNewYearDates(): array { $last_year = EntityManager::findOne(Year::class, 'SELECT * FROM @TABLE ORDER BY end_date DESC LIMIT 1;'); if ($last_year) { $start_date = clone $last_year->start_date; $start_date->modify('+1 year'); $end_date = clone $last_year->end_date; $end_date->modify('+1 year'); } else { $start_date = new \DateTime; $end_date = clone $start_date; $end_date->modify('+1 year'); } return [$start_date, $end_date]; } /** * Crée une écriture d'affectation automatique * @param Year $year * @return Transaction|null */ static public function makeAppropriation(Year $year): ?Transaction { $balances = DB::getInstance()->getGrouped('SELECT a.type, a.id, SUM(l.credit) - SUM(l.debit) AS balance FROM acc_accounts a INNER JOIN acc_transactions_lines l ON l.id_account = a.id INNER JOIN acc_transactions t ON t.id = l.id_transaction WHERE t.id_year = ? AND (a.type = ? OR a.type = ?) GROUP BY a.type;', $year->id, Account::TYPE_NEGATIVE_RESULT, Account::TYPE_POSITIVE_RESULT ); if (!count($balances)) { return null; } $positive_appropriation = DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE type = ? AND id_chart = ?;', Account::TYPE_APPROPRIATION_RESULT, $year->id_chart); $negative_appropriation = DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE type = ? AND id_chart = ?;', Account::TYPE_DEBIT_REPORT, $year->id_chart); if (!$positive_appropriation || !$negative_appropriation) { return null; } $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 l\'ouverture de l\'exercice'; $t->date = new \DateTime; if ($t->date > $year->end_date) { $t->date = $year->end_date; } if ($t->date < $year->start_date) { $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); $t->addLine($line); $sum += $account->balance; } if (!empty($balances[Account::TYPE_POSITIVE_RESULT])) { $account = $balances[Account::TYPE_POSITIVE_RESULT]; $line = Line::create($account->id, 0, abs($account->balance)); $t->addLine($line); $sum += $account->balance; } if ($sum > 0) { $line = Line::create($positive_appropriation, $sum, 0); } else { $line = Line::create($negative_appropriation, 0, abs($sum)); } $t->addLine($line); return $t; } } |
Modified src/include/lib/Garradin/CSV.php from [1989ff97c6] to [dc24caf412].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin; use KD2\Office\Calc\Writer as ODSWriter; class CSV { static public function readAsArray(string $path) { if (!file_exists($path) || !is_readable($path)) { throw new \RuntimeException('Fichier inconnu : '.$path); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; use KD2\Office\Calc\Writer as ODSWriter; class CSV { /** * Convert a file to CSV if required (and if CALC_CONVERT_COMMAND is set) */ static public function convertUploadIfRequired(string $path): string { if (!CALC_CONVERT_COMMAND) { return $path; } $mime = @mime_content_type($path); // XLSX if ($mime == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { $ext = 'xlsx'; } elseif ($mime == 'application/vnd.ms-excel') { $ext = 'xls'; } elseif ($mime == 'application/vnd.oasis.opendocument.spreadsheet') { $ext = 'ods'; } // Assume raw CSV else { return $path; } $r = md5(random_bytes(10)); $a = sprintf('%s/convert_%s.%s', CACHE_ROOT, $r, $ext); $b = sprintf('%s/convert_%s.csv', CACHE_ROOT, $r); $is_upload = is_uploaded_file($path); try { if ($is_upload) { move_uploaded_file($path, $a); } else { copy($path, $a); } self::convertXLSX($a, $b); return $b; } finally { @unlink($a); } } static public function convertXLSX(string $from, string $to): string { $tool = substr(CALC_CONVERT_COMMAND, 0, strpos(CALC_CONVERT_COMMAND, ' ') ?: strlen(CALC_CONVERT_COMMAND)); if ($tool == 'unoconv') { $cmd = CALC_CONVERT_COMMAND . ' -i FilterOptions=44,34,76 -o %2$s %1$s'; } elseif ($tool == 'ssconvert') { $cmd = CALC_CONVERT_COMMAND . ' %1$s %2$s'; } elseif ($tool == 'unoconvert') { $cmd = CALC_CONVERT_COMMAND . ' %1$s %2$s'; } else { throw new \LogicException(sprintf('Conversion tool "%s" is not supported', $tool)); } $cmd = sprintf($cmd, escapeshellarg($from), escapeshellarg($to)); $cmd .= ' 2>&1'; $return = shell_exec($cmd); //var_dump($cmd, $return); exit; if (!file_exists($to)) { throw new UserException('Impossible de convertir le fichier. Vérifier que le fichier est un format supporté.'); } return $to; } static public function supportsXLSExport(): bool { return CALC_CONVERT_COMMAND ? true : false; } static public function readAsArray(string $path) { if (!file_exists($path) || !is_readable($path)) { throw new \RuntimeException('Fichier inconnu : '.$path); } |
︙ | ︙ | |||
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | } if (count($row) != $nb_columns) { throw new UserException('Erreur sur la ligne ' . $line . ' : incohérence dans le nombre de colonnes avec la première ligne.'); } $out[$line] = $row; } fclose($fp); return $out; } static public function open(string $file) { | > > > < | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | } if (count($row) != $nb_columns) { throw new UserException('Erreur sur la ligne ' . $line . ' : incohérence dans le nombre de colonnes avec la première ligne.'); } // Make sure the data is UTF-8 encoded $row = array_map(fn ($a) => Utils::utf8_encode(trim($a)), $row); $out[$line] = $row; } fclose($fp); return $out; } static public function open(string $file) { return fopen($file, 'r'); } static public function findDelimiter(&$fp) { $line = ''; |
︙ | ︙ | |||
101 102 103 104 105 106 107 | } static public function row($row): string { $row = (array) $row; array_walk($row, function (&$field) { | | > > > | > > > | 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 | } static public function row($row): string { $row = (array) $row; array_walk($row, function (&$field) { $field = strtr((string) $field, ['"' => '""', "\r\n" => "\n"]); }); return sprintf("\"%s\"\r\n", implode('","', $row)); } static public function export(string $format, string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void { if ('csv' == $format) { self::toCSV(... array_slice(func_get_args(), 1)); } elseif ('xlsx' == $format && CALC_CONVERT_COMMAND) { self::toXLSX(... array_slice(func_get_args(), 1)); } elseif ('ods' == $format) { self::toODS(... array_slice(func_get_args(), 1)); } else { throw new \InvalidArgumentException('Unknown export format'); } } static protected function rowToArray($row, ?callable $row_map_callback) { if (null !== $row_map_callback) { call_user_func_array($row_map_callback, [&$row]); } |
︙ | ︙ | |||
139 140 141 142 143 144 145 | throw new \UnexpectedValueException(sprintf('Unexpected value for "%s": %s', $key, gettype($v))); } } return $row; } | | > | | | > > > > | 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 | throw new \UnexpectedValueException(sprintf('Unexpected value for "%s": %s', $key, gettype($v))); } } return $row; } static public function toCSV(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null, string $output = null): void { if (null === $output) { header('Content-type: application/csv'); header(sprintf('Content-Disposition: attachment; filename="%s.csv"', $name)); $fp = fopen('php://output', 'w'); } else { $fp = fopen($output, 'w'); } if ($header) { fputs($fp, self::row($header)); } if (!($iterator instanceof \Iterator) || $iterator->valid()) { foreach ($iterator as $row) { |
︙ | ︙ | |||
178 179 180 181 182 183 184 | fputs($fp, self::row($row)); } } fclose($fp); } | | > | | > | 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | fputs($fp, self::row($row)); } } fclose($fp); } static public function toODS(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null, string $output = null): void { if (null === $output) { header('Content-type: application/vnd.oasis.opendocument.spreadsheet'); header(sprintf('Content-Disposition: attachment; filename="%s.ods"', $name)); } $ods = new ODSWriter; $ods->table_name = $name; if ($header) { $ods->add((array) $header); } |
︙ | ︙ | |||
204 205 206 207 208 209 210 | $header = true; } $ods->add((array) $row); } } | | > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | $header = true; } $ods->add((array) $row); } } $ods->output($output); } static public function toXLSX(string $name, iterable $iterator, ?array $header = null, ?callable $row_map_callback = null): void { if (!CALC_CONVERT_COMMAND) { throw new \LogicException('CALC_CONVERT_COMMAND is not set'); } $tmpfile1 = sprintf('%s/export_%s.ods', STATIC_CACHE_ROOT, md5(random_bytes(10))); $tmpfile2 = substr($tmpfile1, 0, -3) . 'xlsx'; try { self::toODS($name, $iterator, $header, $row_map_callback, $tmpfile1); self::convertXLSX($tmpfile1, $tmpfile2); header('Content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header(sprintf('Content-Disposition: attachment; filename="%s.xlsx"', $name)); readfile($tmpfile2); } finally { @unlink($tmpfile1); @unlink($tmpfile2); } } static public function importUpload(array $file, array $expected_columns): \Generator { if (empty($file['size']) || empty($file['tmp_name'])) { throw new UserException('Fichier invalide'); } |
︙ | ︙ | |||
231 232 233 234 235 236 237 | // Find the delimiter $delim = self::findDelimiter($fp); self::skipBOM($fp); $line = 0; $columns = fgetcsv($fp, 4096, $delim); | > > | | 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | // Find the delimiter $delim = self::findDelimiter($fp); self::skipBOM($fp); $line = 0; $columns = fgetcsv($fp, 4096, $delim); // Make sure the data is UTF-8 encoded $columns = array_map(fn ($a) => Utils::utf8_encode(trim($a)), $columns); // Check for required columns foreach ($expected_columns as $column) { if (!in_array($column, $columns, true)) { throw new UserException(sprintf('La colonne "%s" est absente du fichier importé', $column)); } } |
︙ | ︙ | |||
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | } if (count($row) != count($columns)) { throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.'); } $row = array_combine($columns, $row); yield $line => $row; } fclose($fp); } } | > > > | 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 | } if (count($row) != count($columns)) { throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.'); } // Make sure the data is UTF-8 encoded $row = array_map(fn ($a) => Utils::utf8_encode(trim($a)), $row); $row = array_combine($columns, $row); yield $line => $row; } fclose($fp); } } |
Modified src/include/lib/Garradin/CSV_Custom.php from [3c866a40b5] to [a5c926a4f6].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin; use KD2\UserSession; class CSV_Custom { protected $session; protected $key; protected $csv; protected $translation; protected $columns; | | > | > | > > > > > > | > | > | | > | > | > > | > > | > > | | | | | | | | | | > > > | > > > > > > | | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; use KD2\UserSession; class CSV_Custom { protected $session; protected $key; protected $csv; protected $translation; protected $columns; protected array $mandatory_columns = []; protected int $skip = 1; protected $modifier = null; protected array $_default; public function __construct(UserSession $session, string $key) { $this->session = $session; $this->key = $key; $this->csv = $this->session->get($this->key); $this->translation = $this->session->get($this->key . '_translation') ?: []; $this->skip = $this->session->get($this->key . '_skip') ?: 1; } public function load(array $file) { if (empty($file['size']) || empty($file['tmp_name']) || empty($file['name'])) { throw new UserException('Fichier invalide'); } $path = $file['tmp_name']; if (CALC_CONVERT_COMMAND && strtolower(substr($file['name'], -4)) != '.csv') { $path = CSV::convertUploadIfRequired($path); } $csv = CSV::readAsArray($path); if (!count($csv)) { throw new UserException('Ce fichier est vide (aucune ligne trouvée).'); } $this->session->set($this->key, $csv); } public function iterate(): \Generator { if (empty($this->csv)) { throw new \LogicException('No file has been loaded'); } if (!$this->columns || !$this->translation) { throw new \LogicException('Missing columns or translation table'); } for ($i = 0; $i < count($this->csv); $i++) { if ($i <= $this->skip) { continue; } yield $i => $this->getLine($i); } } public function getLine(int $i): ?\stdClass { if (!isset($this->csv[$i])) { return null; } if (!isset($this->_default)) { $this->_default = array_map(function ($a) { return null; }, array_flip($this->translation)); } $row = $this->_default; foreach ($this->csv[$i] as $col => $value) { if (!isset($this->translation[$col])) { continue; } $row[$this->translation[$col]] = trim($value); } $row = (object) $row; if (null !== $this->modifier) { try { $row = call_user_func($this->modifier, $row); } catch (UserException $e) { throw new UserException(sprintf('Ligne %d : %s', $i, $e->getMessage())); } } return $row; } public function getFirstLine(): array { if (!$this->loaded()) { throw new \LogicException('No file has been loaded'); } return current($this->csv); } public function setModifier(callable $callback): void { $this->modifier = $callback; } public function getSelectedTable(?array $source = null): array { if (null === $source && isset($_POST['translation_table'])) { $source = $_POST['translation_table']; } |
︙ | ︙ | |||
151 152 153 154 155 156 157 158 159 160 161 162 163 164 | } public function clear(): void { $this->session->set($this->key, null); $this->session->set($this->key . '_translation', null); $this->session->set($this->key . '_skip', null); } public function loaded(): bool { return null !== $this->csv; } | > | 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | } public function clear(): void { $this->session->set($this->key, null); $this->session->set($this->key . '_translation', null); $this->session->set($this->key . '_skip', null); $this->translation = null; } public function loaded(): bool { return null !== $this->csv; } |
︙ | ︙ | |||
201 202 203 204 205 206 207 | public function getColumns(): array { return $this->columns; } public function getMandatoryColumns(): array { | | | 234 235 236 237 238 239 240 241 242 243 | public function getColumns(): array { return $this->columns; } public function getMandatoryColumns(): array { return array_intersect_key($this->columns, array_flip($this->mandatory_columns)); } } |
Modified src/include/lib/Garradin/Config.php from [f20bf044b0] to [0e94a62511].
︙ | ︙ | |||
187 188 189 190 191 192 193 | public function importForm($source = null): void { if (null === $source) { $source = $_POST; } // N'enregistrer les couleurs que si ce ne sont pas les couleurs par défaut | | | | 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | public function importForm($source = null): void { if (null === $source) { $source = $_POST; } // N'enregistrer les couleurs que si ce ne sont pas les couleurs par défaut if (isset($source['couleur1'], $source['couleur2']) && ($source['couleur1'] == ADMIN_COLOR1 && $source['couleur2'] == ADMIN_COLOR2)) { $source['couleur1'] = null; $source['couleur2'] = null; } parent::importForm($source); } |
︙ | ︙ | |||
242 243 244 245 246 247 248 | $this->assert(!empty($champs->get($this->champ_identite)), sprintf('Le champ spécifié pour identité, "%s" n\'existe pas', $this->champ_identite)); $this->assert(!empty($champs->get($this->champ_identifiant)), sprintf('Le champ spécifié pour identifiant, "%s" n\'existe pas', $this->champ_identifiant)); $db = DB::getInstance(); // Check that this field is actually unique if (isset($this->_modified['champ_identifiant'])) { | | | 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | $this->assert(!empty($champs->get($this->champ_identite)), sprintf('Le champ spécifié pour identité, "%s" n\'existe pas', $this->champ_identite)); $this->assert(!empty($champs->get($this->champ_identifiant)), sprintf('Le champ spécifié pour identifiant, "%s" n\'existe pas', $this->champ_identifiant)); $db = DB::getInstance(); // Check that this field is actually unique if (isset($this->_modified['champ_identifiant'])) { $sql = sprintf('SELECT (COUNT(DISTINCT %s COLLATE U_NOCASE) = COUNT(*)) FROM membres WHERE %1$s IS NOT NULL AND %1$s != \'\';', $this->champ_identifiant); $is_unique = (bool) $db->firstColumn($sql); $this->assert($is_unique, sprintf('Le champ "%s" comporte des doublons et ne peut donc pas servir comme identifiant unique de connexion.', $this->champ_identifiant)); } $this->assert($db->test('users_categories', 'id = ?', $this->categorie_membres), 'Catégorie de membres inconnue'); } |
︙ | ︙ | |||
280 281 282 283 284 285 286 | } return null; } $params = $params ? $params . '&' : ''; | | | 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | } return null; } $params = $params ? $params . '&' : ''; return BASE_URL . self::FILES[$key] . '?' . $params . substr(md5($this->files[$key]), 0, 10); } public function hasFile(string $key): bool { return $this->files[$key] ? true : false; } |
︙ | ︙ |
Modified src/include/lib/Garradin/DB.php from [f878c985d1] to [e7b3d0adf7].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin; use KD2\DB\SQLite3; class DB extends SQLite3 { /** * Application ID pour SQLite * @link https://www.sqlite.org/pragma.html#pragma_application_id */ const APPID = 0x5da2d811; static protected $_instance = null; protected $_version = -1; static protected $unicode_patterns_cache = []; | > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; use KD2\DB\SQLite3; use KD2\DB\DB_Exception; use KD2\ErrorManager; class DB extends SQLite3 { /** * Application ID pour SQLite * @link https://www.sqlite.org/pragma.html#pragma_application_id */ const APPID = 0x5da2d811; static protected $_instance = null; protected $_version = -1; static protected $unicode_patterns_cache = []; protected $_log_last = null; protected $_log_start = null; protected $_log_store = []; static public function getInstance() { if (null === self::$_instance) { self::$_instance = new DB('sqlite', ['file' => DB_FILE]); } return self::$_instance; } static public function deleteInstance() { self::$_instance = null; } private function __clone() { // Désactiver le clonage, car on ne veut qu'une seule instance } public function __construct(string $driver, array $params) { if (self::$_instance !== null) { throw new \LogicException('Cannot start instance'); } parent::__construct($driver, $params); // Enable SQL debug log if configured if (SQL_DEBUG) { $this->callback = [$this, 'log']; $this->_log_start = microtime(true); } } public function __destruct() { parent::__destruct(); if (null !== $this->callback) { $this->saveLog(); } } /** * Disable logging if enabled * useful to disable logging when reloading log page */ public function disableLog(): void { $this->callback = null; $this->_log_store = []; } /** * Saves the log in a different database at the end of the script */ protected function saveLog(): void { if (!count($this->_log_store)) { return; } $db = new SQLite3('sqlite', ['file' => SQL_DEBUG]); $db->exec('CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY, date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, script TEXT, user TEXT); CREATE TABLE IF NOT EXISTS log (session INTEGER NOT NULL REFERENCES sessions (id), time INTEGER, duration INTEGER, sql TEXT, trace TEXT);'); $user = $_SESSION['userSession']->id ?? null; $db->insert('sessions', ['script' => str_replace(ROOT, '', $_SERVER['SCRIPT_NAME']), 'user' => $user]); $id = $db->lastInsertId(); $db->begin(); foreach ($this->_log_store as $row) { $db->insert('log', array_merge($row, ['session' => $id])); } $db->commit(); $db->close(); } /** * Log current SQL query */ protected function log(string $method, ?string $timing, $object, ...$params): void { if ($method != 'execute' && $method != 'exec') { return; } if ($timing == 'before') { $this->_log_last = microtime(true); return; } $now = microtime(true); $duration = round(($now - $this->_log_last) * 1000 * 1000); $time = round(($now - $this->_log_start) * 1000 * 1000); if ($method == 'execute') { $sql = $params[0]->getSQL(true); } else { $sql = $params[0]; } $sql = preg_replace('/^\s+/m', ' ', $sql); $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); $trace = ''; foreach ($backtrace as $line) { if (!isset($line['file']) || in_array(basename($line['file']), ['DB.php', 'SQLite3.php']) || strstr($line['file'], 'lib/KD2')) { continue; } $file = isset($line['file']) ? str_replace(ROOT . '/', '', $line['file']) : ''; $trace .= sprintf("%s:%d\n", $file, $line['line']); } $this->_log_store[] = compact('duration', 'time', 'sql', 'trace'); } /** * Return a debug log session using its ID */ static public function getDebugSession(int $id): ?\stdClass { $db = new SQLite3('sqlite', ['file' => SQL_DEBUG]); $s = $db->first('SELECT * FROM sessions WHERE id = ?;', $id); if ($s) { $s->list = $db->get('SELECT * FROM log WHERE session = ? ORDER BY time;', $id); foreach ($s->list as &$row) { try { $explain = DB::getInstance()->get('EXPLAIN QUERY PLAN ' . $row->sql); $row->explain = ''; foreach ($explain as $e) { $row->explain .= $e->detail . "\n"; } } catch (DB_Exception $e) { $row->explain = 'Error: ' . $e->getMessage(); } } } $db->close(); return $s; } /** * Return the list of all debug sessions */ static public function getDebugSessionsList(): array { $db = new SQLite3('sqlite', ['file' => SQL_DEBUG]); $s = $db->get('SELECT s.*, SUM(l.duration) AS duration, COUNT(l.rowid) AS count FROM sessions s INNER JOIN log l ON l.session = s.id GROUP BY s.id ORDER BY s.date DESC;'); $db->close(); return $s; } public function connect(): void { if (null !== $this->db) { return; } |
︙ | ︙ | |||
55 56 57 58 59 60 61 | } static public function registerCustomFunctions($db) { $db->createFunction('dirname', [Utils::class, 'dirname']); $db->createFunction('basename', [Utils::class, 'basename']); $db->createFunction('like', [self::class, 'unicodeLike']); | > | > | | > > > > | 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 | } static public function registerCustomFunctions($db) { $db->createFunction('dirname', [Utils::class, 'dirname']); $db->createFunction('basename', [Utils::class, 'basename']); $db->createFunction('like', [self::class, 'unicodeLike']); $db->createFunction('email_hash', [Entities\Users\Email::class, 'getHash']); $db->createCollation('U_NOCASE', [Utils::class, 'unicodeCaseComparison']); } public function version(): ?string { if (-1 === $this->_version) { $this->connect(); $this->_version = self::getVersion($this->db); } return $this->_version; } static public function getVersion($db) { $v = (int) $db->querySingle('PRAGMA user_version;'); $v = self::parseVersion($v); if (null === $v) { try { // For legacy version before 1.1.0 $v = $db->querySingle('SELECT valeur FROM config WHERE cle = \'version\';'); } catch (\Exception $e) { throw new \RuntimeException('Cannot find application version', 0, $e); } } return $v ?: null; } static public function parseVersion(int $v): ?string { |
︙ | ︙ | |||
153 154 155 156 157 158 159 | $version += $match[5] + 75; } } $this->db->exec(sprintf('PRAGMA user_version = %d;', $version)); } | < < < < < < | 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | $version += $match[5] + 75; } } $this->db->exec(sprintf('PRAGMA user_version = %d;', $version)); } public function beginSchemaUpdate() { $this->toggleForeignKeys(false); $this->begin(); } public function commitSchemaUpdate() |
︙ | ︙ | |||
207 208 209 210 211 212 213 214 215 216 217 | * This is probably not the best way to do that, but we have to resort to that * as ICU extension is rarely available. * * @see https://www.sqlite.org/c3ref/strlike.html * @see https://sqlite.org/src/file?name=ext/icu/icu.c&ci=trunk */ static public function unicodeLike($pattern, $value, $escape = null) { $id = md5($pattern . $escape); if (!array_key_exists($id, self::$unicode_patterns_cache)) { $escape = $escape ? '(?!' . preg_quote($escape, '/') . ')' : ''; | > > > > > > > | | | | 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 | * This is probably not the best way to do that, but we have to resort to that * as ICU extension is rarely available. * * @see https://www.sqlite.org/c3ref/strlike.html * @see https://sqlite.org/src/file?name=ext/icu/icu.c&ci=trunk */ static public function unicodeLike($pattern, $value, $escape = null) { if (null === $pattern || null === $value) { return false; } $pattern = str_replace('’', '\'', $pattern); // Normalize French apostrophe $value = str_replace('’', '\'', $value); $id = md5($pattern . $escape); if (!array_key_exists($id, self::$unicode_patterns_cache)) { $escape = $escape ? '(?!' . preg_quote($escape, '/') . ')' : ''; preg_match_all('/('.$escape.'[%_])|(\pL+)|(.+?)/iu', $pattern, $parts, PREG_SET_ORDER); $pattern = ''; foreach ($parts as $part) { if (isset($part[3])) { $pattern .= preg_quote(strtolower($part[0]), '/'); } elseif (isset($part[2])) { $pattern .= preg_quote(Utils::unicodeCaseFold($part[2]), '/'); } elseif ($part[1] == '%') { $pattern .= '.*'; } elseif ($part[1] == '_') { $pattern .= '.'; } |
︙ | ︙ |
Modified src/include/lib/Garradin/DynamicList.php from [19f54eeec5] to [69e24b02bd].
︙ | ︙ | |||
93 94 95 96 97 98 99 | $columns[] = $key; continue; } $columns[] = $column['label']; } | < | < < < < < < < | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | $columns[] = $key; continue; } $columns[] = $column['label']; } CSV::export($format, $name, $this->iterate(false), $this->getHeaderColumns(true), $this->export_callback); } public function asArray(): array { $out = []; foreach ($this->iterate(true) as $row) { |
︙ | ︙ |
Added src/include/lib/Garradin/Entities/API_Credentials.php version [f8f4575a16].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 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\Entities; use Garradin\Membres\Session; use Garradin\Entity; class API_Credentials extends Entity { const TABLE = 'api_credentials'; protected ?int $id; protected string $label; protected string $key; protected string $secret; protected \DateTime $created; protected ?\DateTime $last_use; protected int $access_level; const ACCESS_LEVELS = [ Session::ACCESS_READ => 'Peut lire les données', Session::ACCESS_WRITE => 'Peut lire et modifier les données', Session::ACCESS_ADMIN => 'Peut tout faire, y compris supprimer les données', ]; public function selfCheck(): void { parent::selfCheck(); $this->assert(trim($this->label) !== '', 'La description ne peut être laissée vide.'); $this->assert(trim($this->key) !== '', 'La clé ne peut être laissée vide.'); $this->assert(trim($this->secret) !== '', 'Le secret ne peut être laissé vide.'); $this->assert(array_key_exists($this->access_level, self::ACCESS_LEVELS)); $this->assert(preg_match('/^[a-z0-9_]+$/', $this->key), 'L\'identifiant ne peut contenir que des lettres, des chiffres et des tirets bas.'); } } |
Modified src/include/lib/Garradin/Entities/Accounting/Account.php from [71842e7b88] to [283323d3dd].
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | use Garradin\ValidationException; use Garradin\Accounting\Charts; class Account extends Entity { const TABLE = 'acc_accounts'; // Actif const ASSET = 1; // Passif const LIABILITY = 2; // Passif ou actif | > > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | use Garradin\ValidationException; use Garradin\Accounting\Charts; class Account extends Entity { const TABLE = 'acc_accounts'; const NONE = 0; // Actif const ASSET = 1; // Passif const LIABILITY = 2; // Passif ou actif |
︙ | ︙ | |||
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 | const TYPE_OPENING = 9; const TYPE_CLOSING = 10; const TYPE_POSITIVE_RESULT = 11; const TYPE_NEGATIVE_RESULT = 12; const TYPES_NAMES = [ '', 'Banque', 'Caisse', 'Attente d\'encaissement', 'Tiers', 'Dépenses', 'Recettes', 'Analytique', 'Bénévolat', 'Ouverture', 'Clôture', 'Résultat excédentaire', 'Résultat déficitaire', ]; const LIST_COLUMNS = [ 'id' => [ 'select' => 't.id', 'label' => 'N°', ], | > > > > > > > > | 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 | const TYPE_OPENING = 9; const TYPE_CLOSING = 10; const TYPE_POSITIVE_RESULT = 11; const TYPE_NEGATIVE_RESULT = 12; const TYPE_APPROPRIATION_RESULT = 13; const TYPE_CREDIT_REPORT = 14; const TYPE_DEBIT_REPORT = 15; const TYPES_NAMES = [ '', 'Banque', 'Caisse', 'Attente d\'encaissement', 'Tiers', 'Dépenses', 'Recettes', 'Analytique', 'Bénévolat', 'Ouverture', 'Clôture', 'Résultat excédentaire', 'Résultat déficitaire', 'Affectation du résultat', 'Report à nouveau créditeur', 'Report à nouveau débiteur', ]; const LIST_COLUMNS = [ 'id' => [ 'select' => 't.id', 'label' => 'N°', ], |
︙ | ︙ | |||
97 98 99 100 101 102 103 | 'label' => 'Débit', ], 'credit' => [ 'select' => 'l.credit', 'label' => 'Crédit', ], 'change' => [ | | | 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 'label' => 'Débit', ], 'credit' => [ 'select' => 'l.credit', 'label' => 'Crédit', ], 'change' => [ 'select' => '(l.debit - l.credit) * %d', 'label' => 'Mouvement', ], 'sum' => [ 'select' => NULL, 'label' => 'Solde cumulé', 'only_with_order' => 'date', ], |
︙ | ︙ | |||
160 161 162 163 164 165 166 | 'user' => 'int', ]; protected $_form_rules = [ 'code' => 'required|string|alpha_num|max:10', 'label' => 'required|string|max:200', 'description' => 'string|max:2000', | < < > > | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 'user' => 'int', ]; protected $_form_rules = [ 'code' => 'required|string|alpha_num|max:10', 'label' => 'required|string|max:200', 'description' => 'string|max:2000', ]; protected $_position = []; public function selfCheck(): void { $db = DB::getInstance(); $this->assert(!empty($this->id_chart), 'Aucun plan comptable lié'); |
︙ | ︙ | |||
195 196 197 198 199 200 201 | $tables = 'acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction LEFT JOIN acc_accounts b ON b.id = l.id_analytical'; $conditions = sprintf('l.id_account = %d AND t.id_year = %d', $this->id(), $year_id); $sum = 0; | | > > < | | > > > > | | > > > > > > > > > > > > > > > > > > > > > > > > | 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 | $tables = 'acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction LEFT JOIN acc_accounts b ON b.id = l.id_analytical'; $conditions = sprintf('l.id_account = %d AND t.id_year = %d', $this->id(), $year_id); $sum = 0; $reverse = $this->isReversed($simple, $year_id) ? -1 : 1; if ($start) { $conditions .= sprintf(' AND t.date >= %s', $db->quote($start->format('Y-m-d'))); $sum = $this->getSumAtDate($year_id, $start) * $reverse; } if ($end) { $conditions .= sprintf(' AND t.date <= %s', $db->quote($end->format('Y-m-d'))); } $columns['change']['select'] = sprintf($columns['change']['select'], $reverse); if ($simple) { unset($columns['debit']['label'], $columns['credit']['label'], $columns['line_label']); $columns['line_reference']['label'] = 'Réf. paiement'; } else { unset($columns['change']['label']); } $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('date', false); $list->setCount('COUNT(*)'); $list->setPageSize(null); $list->setModifier(function (&$row) use (&$sum) { if (property_exists($row, 'sum')) { $sum += $row->change; $row->sum = $sum; } $row->date = \DateTime::createFromFormat('!Y-m-d', $row->date); }); $list->setExportCallback(function (&$row) { static $columns = ['change', 'sum', 'credit', 'debit']; foreach ($columns as $key) { if (isset($row->$key)) { $row->$key = Utils::money_format($row->$key, '.', '', false); } } }); return $list; } /** * Renvoie TRUE si le solde du compte est inversé en vue simplifiée (= crédit - débit, au lieu de débit - crédit) * @return boolean */ public function isReversed(bool $simple, int $id_year): bool { if ($simple && in_array($this->type, [self::TYPE_BANK, self::TYPE_CASH, self::TYPE_OUTSTANDING, self::TYPE_EXPENSE, self::TYPE_THIRD_PARTY])) { return false; } $position = $this->getPosition($id_year); if ($position == self::ASSET || $position == self::EXPENSE) { return false; } return true; } public function getPosition(int $id_year): int { $position = $this->_position[$id_year] ?? $this->position; if ($position == self::ASSET_OR_LIABILITY) { $balance = DB::getInstance()->firstColumn('SELECT debit - credit FROM acc_accounts_balances WHERE id = ? AND id_year = ?;', $this->id, $id_year); $position = $balance > 0 ? self::ASSET : self::LIABILITY; } $this->_position[$id_year] = $position; return $position; } public function getReconcileJournal(int $year_id, DateTimeInterface $start_date, DateTimeInterface $end_date, bool $only_non_reconciled = false) { if ($end_date < $start_date) { throw new ValidationException('La date de début ne peut être avant la date de fin.'); } |
︙ | ︙ | |||
289 290 291 292 293 294 295 | } if (!$only_non_reconciled) { yield (object) ['sum' => $sum, 'reconciled_sum' => $reconciled_sum, 'date' => $end_date]; } } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | } if (!$only_non_reconciled) { yield (object) ['sum' => $sum, 'reconciled_sum' => $reconciled_sum, 'date' => $end_date]; } } 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;', |
︙ | ︙ | |||
434 435 436 437 438 439 440 | 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); } | | | | < | < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | 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 getSum(int $year_id, bool $simple = false): ?\stdClass { $sum = DB::getInstance()->first('SELECT balance, credit, debit FROM acc_accounts_balances WHERE id = ? AND id_year = ?;', $this->id(), $year_id); return $sum ?: null; } public function getSumAtDate(int $year_id, DateTimeInterface $date, bool $reconciled_only = false): int { $sql = sprintf('SELECT SUM(l.credit) - SUM(l.debit) FROM acc_transactions_lines l INNER JOIN acc_transactions t ON t.id = l.id_transaction WHERE l.id_account = ? AND t.id_year = ? AND t.date < ? %s;', $reconciled_only ? 'AND l.reconciled = 1' : ''); return (int) DB::getInstance()->firstColumn($sql, $this->id(), $year_id, $date->format('Y-m-d')); } public function importLimitedForm(?array $source = null) { if (null === $source) { $source = $_POST; } $data = array_intersect_key($source, array_flip(['type', 'description'])); |
︙ | ︙ | |||
536 537 538 539 540 541 542 543 544 545 546 | return Charts::get($this->id_chart); } public function save(): bool { $c = Config::getInstance(); $c->set('last_chart_change', time()); return parent::save(); } } | > | 429 430 431 432 433 434 435 436 437 438 439 440 | return Charts::get($this->id_chart); } public function save(): bool { $c = Config::getInstance(); $c->set('last_chart_change', time()); $c->save(); return parent::save(); } } |
Modified src/include/lib/Garradin/Entities/Accounting/Line.php from [a9f6e82800] to [155afe8618].
︙ | ︙ | |||
36 37 38 39 40 41 42 43 44 45 46 47 48 49 | protected $_form_rules = [ 'id_account' => 'required|numeric|in_table:acc_accounts,id', 'id_analytical' => 'numeric|in_table:acc_accounts,id', 'reference' => 'string|max:200', 'label' => 'string|max:200', ]; public function filterUserValue(string $type, $value, string $key) { if ($key == 'credit' || $key == 'debit') { $value = Utils::moneyToInteger($value); } elseif ($key == 'id_analytical' && $value == 0) { | > > > > > > > > > > > > | 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 | protected $_form_rules = [ 'id_account' => 'required|numeric|in_table:acc_accounts,id', 'id_analytical' => 'numeric|in_table:acc_accounts,id', 'reference' => 'string|max:200', 'label' => 'string|max:200', ]; static public function create(int $id_account, int $credit, int $debit, ?string $label = null, ?string $reference = null): Line { $line = new self; $line->id_account = $id_account; $line->credit = $credit; $line->debit = $debit; $line->label = $label; $line->reference = $reference; return $line; } public function filterUserValue(string $type, $value, string $key) { if ($key == 'credit' || $key == 'debit') { $value = Utils::moneyToInteger($value); } elseif ($key == 'id_analytical' && $value == 0) { |
︙ | ︙ | |||
59 60 61 62 63 64 65 | { parent::selfCheck(); $this->assert($this->credit || $this->debit, 'Aucun montant au débit ou au crédit'); $this->assert($this->credit >= 0 && $this->debit >= 0, 'Le montant ne peut être négatif'); $this->assert(($this->credit * $this->debit) === 0 && ($this->credit + $this->debit) > 0, 'Ligne non équilibrée : crédit ou débit doit valoir zéro.'); $this->assert($this->id_transaction, 'Aucun mouvement n\'a été indiqué pour cette ligne.'); $this->assert($this->reconciled === 0 || $this->reconciled === 1); | < < < < < < | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | { parent::selfCheck(); $this->assert($this->credit || $this->debit, 'Aucun montant au débit ou au crédit'); $this->assert($this->credit >= 0 && $this->debit >= 0, 'Le montant ne peut être négatif'); $this->assert(($this->credit * $this->debit) === 0 && ($this->credit + $this->debit) > 0, 'Ligne non équilibrée : crédit ou débit doit valoir zéro.'); $this->assert($this->id_transaction, 'Aucun mouvement n\'a été indiqué pour cette ligne.'); $this->assert($this->reconciled === 0 || $this->reconciled === 1); } public function asDetailsArray(): array { return [ 'Compte' => $this->id_account ? Accounts::getSelectorLabel($this->id_account) : null, 'Libellé' => $this->label, 'Référence' => $this->reference, 'Crédit' => Utils::money_format($this->credit), 'Débit' => Utils::money_format($this->debit), ]; } } |
Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [fac4ec7e24] to [7279703790].
︙ | ︙ | |||
295 296 297 298 299 300 301 | return [ $type->accounts[0]->position == 'credit' ? $credit : $debit, $type->accounts[1]->position == 'credit' ? $credit : $debit, ]; } | | | | 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 | return [ $type->accounts[0]->position == 'credit' ? $credit : $debit, $type->accounts[1]->position == 'credit' ? $credit : $debit, ]; } /**duplic * Creates a new Transaction entity (not saved) from an existing one, * trying to adapt to a different chart if possible * @param int $id * @param Year $year Target year * @return Transaction */ public function duplicate(Year $year): Transaction { $new = new Transaction; $copy = ['type', 'status', 'label', 'notes', 'reference']; foreach ($copy as $field) { $new->$field = $this->$field; } $copy = ['credit', 'debit', 'id_account', 'label', 'reference', 'id_analytical']; $lines = DB::getInstance()->get('SELECT |
︙ | ︙ | |||
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | $line->$field = $l->$field; } $new->addLine($line); } return $new; } /* public function getHash() { if (!$this->id_year) { throw new \LogicException('Il n\'est pas possible de hasher un mouvement qui n\'est pas associé à un exercice'); | > > > > > > > > > > > > > > > > > > | 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 | $line->$field = $l->$field; } $new->addLine($line); } // Only set date if valid if ($this->date >= $year->start_date && $this->date <= $year->end_date) { $new->date = clone $this->date; } $new->status = 0; return $new; } public function payment_reference(): ?string { $line = current($this->getLines()); if (!$line) { return null; } return $line->reference; } /* public function getHash() { if (!$this->id_year) { throw new \LogicException('Il n\'est pas possible de hasher un mouvement qui n\'est pas associé à un exercice'); |
︙ | ︙ | |||
476 477 478 479 480 481 482 483 | $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(); | > | > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > | 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 | $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(); $accounts_ids = []; foreach ($lines as $k => $line) { $this->assert($line->credit || $line->debit, sprintf('Ligne %d: Aucun montant au débit ou au crédit', $k)); $this->assert($line->credit >= 0 && $line->debit >= 0, sprintf('Ligne %d: Le montant ne peut être négatif', $k)); $this->assert(($line->credit * $line->debit) === 0 && ($line->credit + $line->debit) > 0, sprintf('Ligne %d: non équilibrée, crédit ou débit doit valoir zéro.', $k)); $accounts_ids = [$line->id_account]; $total += $line->credit; $total -= $line->debit; } $this->assert(0 === $total, sprintf('Écriture non équilibrée : déséquilibre (%s) entre débits et crédits', Utils::money_format($total))); $this->assert($db->test('acc_years', 'id = ?', $this->id_year), 'L\'exercice sélectionné n\'existe pas'); $this->assert($this->id_creator === null || $db->test('membres', 'id = ?', $this->id_creator), 'Le compte membre créateur de l\'écriture n\'existe pas'); $found_accounts = $db->getAssoc(sprintf('SELECT id, id FROM acc_accounts WHERE %s AND id_chart = (SELECT id_chart FROM acc_years WHERE id = %d);', $db->where('id', $accounts_ids), $this->id_year)); $diff = array_diff($accounts_ids, $found_accounts); $this->assert(count($diff) == 0, sprintf('Certains comptes (%s) ne sont pas liés au bon plan comptable', implode(', ', $diff))); $this->assert(!$this->id_related || $db->test('acc_transactions', 'id = ?', $this->id_related), 'L\'écriture liée indiquée n\'existe pas'); $this->assert(!$this->id_related || !$this->exists() || $this->id_related != $this->id, 'Il n\'est pas possible de lier une écriture à elle-même'); } public function importFromDepositForm(?array $source = null): void { if (null === $source) { $source = $_POST; } if (empty($source['amount'])) { throw new UserException('Montant non précisé'); } $this->type = self::TYPE_ADVANCED; $amount = $source['amount']; $key = 'account_transfer'; if (empty($source[$key]) || !count($source[$key])) { throw new ValidationException('Aucun compte de dépôt n\'a été sélectionné'); } $account = key($source[$key]); $line = new Line; $line->importForm([ 'debit' => $amount, 'credit' => 0, 'id_account' => $account, ]); $this->addLine($line); $this->importForm($source); } public function importForm(array $source = null) { if (null === $source) { $source = $_POST; } if (isset($source['id_related']) && empty($source['id_related'])) { $source['id_related'] = null; } return parent::importForm($source); } public function importFromNewForm(?array $source = null): void { if (null === $source) { $source = $_POST; } |
︙ | ︙ | |||
538 539 540 541 542 543 544 | if (!isset($source['lines']) || !is_array($source['lines'])) { throw new ValidationException('Aucune ligne dans la saisie'); } $lines = Utils::array_transpose($source['lines']); foreach ($lines as $i => $line) { | | > | < < | > | 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 | if (!isset($source['lines']) || !is_array($source['lines'])) { throw new ValidationException('Aucune ligne dans la saisie'); } $lines = Utils::array_transpose($source['lines']); foreach ($lines as $i => $line) { if (empty($line['account']) || !count($line['account'])) { throw new ValidationException(sprintf('Ligne %d : aucun compte n\'a été sélectionné', $i + 1)); } $line['id_account'] = key($line['account']); $line = (new Line)->import($line); $this->addLine($line); } } else { $details = self::getTypesDetails(); |
︙ | ︙ | |||
572 573 574 575 576 577 578 579 | $amount = $source['amount']; // Fill lines using a pre-defined setup obtained from getTypesDetails foreach ($details[$type]->accounts as $k => $account) { $credit = $account->position == 'credit' ? $amount : 0; $debit = $account->position == 'debit' ? $amount : 0; $key = sprintf('account_%d_%d', $type, $k); | > > > > > > | | 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 | $amount = $source['amount']; // Fill lines using a pre-defined setup obtained from getTypesDetails foreach ($details[$type]->accounts as $k => $account) { $credit = $account->position == 'credit' ? $amount : 0; $debit = $account->position == 'debit' ? $amount : 0; $key = sprintf('account_%d_%d', $type, $k); if (empty($source[$key]) || !count($source[$key])) { throw new ValidationException(sprintf('Ligne %d : aucun compte n\'a été sélectionné', $k+1)); } $account = key($source[$key]); $line = new Line; $line->importForm([ 'reference' => !empty($source['payment_reference']) ? $source['payment_reference'] : null, 'credit' => $credit, 'debit' => $debit, 'id_account' => $account, |
︙ | ︙ | |||
597 598 599 600 601 602 603 604 605 606 607 608 609 610 | public function importFromEditForm(?array $source = null): void { if (null === $source) { $source = $_POST; } $this->resetLines(); $this->importFromNewForm($source); } public function importFromBalanceForm(Year $year, ?array $source = null): void { | > > > | 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 | public function importFromEditForm(?array $source = null): void { if (null === $source) { $source = $_POST; } if (empty($source['id_related'])) { unset($source['id_related']); } $this->resetLines(); $this->importFromNewForm($source); } public function importFromBalanceForm(Year $year, ?array $source = null): void { |
︙ | ︙ | |||
627 628 629 630 631 632 633 | catch (\LogicException $e) { throw new ValidationException('Aucun compte sélectionné pour certaines lignes.'); } $debit = $credit = 0; foreach ($lines as $k => $line) { | > > > > | | | | 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 | catch (\LogicException $e) { throw new ValidationException('Aucun compte sélectionné pour certaines lignes.'); } $debit = $credit = 0; foreach ($lines as $k => $line) { if (empty($line['account']) || !count($line['account'])) { throw new ValidationException(sprintf('Ligne %d : aucun compte n\'a été sélectionné', $k+1)); } $line['id_account'] = key($line['account']); try { $line = (new Line)->importForm($line); $this->addLine($line); } catch (ValidationException $e) { throw new ValidationException(sprintf('Ligne %d : %s', $k+1, $e->getMessage()), 0, $e); } $debit += $line->debit; $credit += $line->credit; } if ($debit != $credit) { // Add final balance line $line = new Line; if ($debit > $credit) { $line->credit = $debit - $credit; } else { $line->debit = $credit - $debit; } $open_account = EntityManager::findOne(Account::class, 'SELECT * FROM @TABLE WHERE id_chart = ? AND type = ? LIMIT 1;', $year->id_chart, Account::TYPE_OPENING); if (!$open_account) { throw new ValidationException('Aucun compte favori de bilan d\'ouverture n\'existe dans le plan comptable'); } |
︙ | ︙ | |||
683 684 685 686 687 688 689 | return File::CONTEXT_TRANSACTION . '/' . $this->id(); } public function linkToUser(int $user_id, ?int $service_id = null) { $db = EntityManager::getInstance(self::class)->DB(); | | | | 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 | return File::CONTEXT_TRANSACTION . '/' . $this->id(); } public function linkToUser(int $user_id, ?int $service_id = null) { $db = EntityManager::getInstance(self::class)->DB(); return $db->preparedQuery('REPLACE INTO acc_transactions_users (id_transaction, id_user, id_service_user) VALUES (?, ?, ?);', $this->id(), $user_id, $service_id); } public function updateLinkedUsers(array $users) { $db = EntityManager::getInstance(self::class)->DB(); $db->begin(); $sql = sprintf('DELETE FROM acc_transactions_users WHERE id_transaction = ? AND %s;', $db->where('id_user', 'NOT IN', $users)); $db->preparedQuery($sql, $this->id()); foreach ($users as $id) { $db->preparedQuery('INSERT OR IGNORE INTO acc_transactions_users (id_transaction, id_user) VALUES (?, ?);', $this->id(), $id); } $db->commit(); |
︙ | ︙ | |||
715 716 717 718 719 720 721 | return $db->get($sql, $this->id()); } public function listLinkedUsersAssoc() { $db = EntityManager::getInstance(self::class)->DB(); $identity_column = Config::getInstance()->get('champ_identite'); | | > > > > > > > > | 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 | return $db->get($sql, $this->id()); } public function listLinkedUsersAssoc() { $db = EntityManager::getInstance(self::class)->DB(); $identity_column = Config::getInstance()->get('champ_identite'); $sql = sprintf('SELECT m.id, m.%s AS identity, l.id_service_user FROM membres m INNER JOIN acc_transactions_users l ON l.id_user = m.id WHERE l.id_transaction = ?;', $identity_column); return $db->getAssoc($sql, $this->id()); } public function listRelatedTransactions() { return EntityManager::getInstance(self::class)->all('SELECT * FROM @TABLE WHERE id_related = ?;', $this->id); } static public function getTypesDetails() { $details = [ self::TYPE_REVENUE => [ 'accounts' => [ [ |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [478d431189] to [215a66eaee].
︙ | ︙ | |||
90 91 92 93 94 95 96 97 98 99 100 101 102 103 | $t->import([ 'id_year' => $this->id(), 'label' => sprintf('Exercice réouvert le %s', date('d/m/Y à H:i:s')), 'type' => Transaction::TYPE_ADVANCED, 'date' => $this->end_date->format('d/m/Y'), 'id_creator' => $user_id, 'validated' => 1, ]); $line = new Line; $line->import([ 'debit' => 0, 'credit' => 1, 'id_account' => $closing_id, | > | 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | $t->import([ 'id_year' => $this->id(), 'label' => sprintf('Exercice réouvert le %s', date('d/m/Y à H:i:s')), 'type' => Transaction::TYPE_ADVANCED, 'date' => $this->end_date->format('d/m/Y'), 'id_creator' => $user_id, 'validated' => 1, 'notes' => 'Écriture automatique créée lors de la réouverture, à des fins de transparence. Cette écriture ne peut pas être supprimée ni modifiée.', ]); $line = new Line; $line->import([ 'debit' => 0, 'credit' => 1, 'id_account' => $closing_id, |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Files/File.php from [389075004c] to [ad63cf1eb1].
︙ | ︙ | |||
15 16 17 18 19 20 21 | use Garradin\Static_Cache; use Garradin\Utils; use Garradin\Entities\Web\Page; use Garradin\Web\Render\Render; use Garradin\Files\Files; | | > > > > > > > | 15 16 17 18 19 20 21 22 23 24 25 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 | use Garradin\Static_Cache; use Garradin\Utils; use Garradin\Entities\Web\Page; use Garradin\Web\Render\Render; use Garradin\Files\Files; use const Garradin\{WWW_URL, BASE_URL, ENABLE_XSENDFILE}; /** * This is a virtual entity, it cannot be saved to a SQL table */ class File extends Entity { const TABLE = 'files'; protected $id; /** * Parent directory of file */ protected $parent; /** * File name */ protected $name; /** * Complete file path (parent + '/' + name) */ protected $path; /** * Type of file: file or directory */ protected $type = self::TYPE_FILE; protected $mime; protected $size; protected $modified; protected $image; protected $_types = [ |
︙ | ︙ | |||
591 592 593 594 595 596 597 598 599 | case UPLOAD_ERR_EXTENSION: return 'Une extension du serveur a interrompu l\'envoi du fichier.'; default: return 'Erreur inconnue: ' . $error; } } public function url(bool $download = false): string { | > > > < < < < < < | | < > > > > > > > > > > > > > | 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 | case UPLOAD_ERR_EXTENSION: return 'Une extension du serveur a interrompu l\'envoi du fichier.'; default: return 'Erreur inconnue: ' . $error; } } /** * Full URL with https://... */ public function url(bool $download = false): string { $base = in_array($this->context(), [self::CONTEXT_WEB, self::CONTEXT_SKELETON, self::CONTEXT_CONFIG]) ? WWW_URL : BASE_URL; $url = $base . $this->uri(); if ($download) { $url .= '?download'; } return $url; } /** * Returns local URI, eg. user/1245/file.jpg */ public function uri(): string { if ($this->context() == self::CONTEXT_WEB) { return Utils::basename(Utils::dirname($this->path)) . '/' . Utils::basename($this->path); } else { return $this->path; } } public function thumb_url($size = null): string { if (is_int($size)) { $size .= 'px'; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Services/Fee.php from [7491493b33] to [cf78e1ab65].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin\Entities\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Entity; use Garradin\ValidationException; use Garradin\Utils; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Year; use KD2\DB\EntityManager; class Fee extends Entity { const TABLE = 'services_fees'; | > | | | | | | | | < | < < < < < < < < < > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | <?php namespace Garradin\Entities\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Entity; use Garradin\ValidationException; use Garradin\Utils; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Year; use KD2\DB\EntityManager; use KD2\DB\DB_Exception; class Fee extends Entity { const TABLE = 'services_fees'; protected ?int $id; protected string $label; protected ?string $description = null; protected ?int $amount = null; protected ?string $formula = null; protected int $id_service; protected ?int $id_account = null; protected ?int $id_year = null; protected ?int $id_analytical = null; public function filterUserValue(string $type, $value, string $key) { if ($key == 'amount' && $value !== null) { $value = Utils::moneyToInteger($value); } return $value; } public function importForm(array $source = null) { if (null === $source) { $source = $_POST; } if (isset($source['account']) && is_array($source['account'])) { $source['id_account'] = (int)key($source['account']); } if (isset($source['analytical']) && is_array($source['analytical'])) { $source['id_analytical'] = (int)key($source['analytical']); } if (isset($source['amount_type'])) { if ($source['amount_type'] == 2) { $source['amount'] = null; } elseif ($source['amount_type'] == 1) { $source['formula'] = null; |
︙ | ︙ | |||
79 80 81 82 83 84 85 | public function selfCheck(): void { $db = DB::getInstance(); parent::selfCheck(); $this->assert(trim($this->label) !== '', 'Le libellé doit être renseigné'); | | | > | > > > > | 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 | public function selfCheck(): void { $db = DB::getInstance(); parent::selfCheck(); $this->assert(trim($this->label) !== '', 'Le libellé doit être renseigné'); $this->assert(strlen((string) $this->label) <= 200, 'Le libellé doit faire moins de 200 caractères'); $this->assert(strlen((string) $this->description) <= 2000, 'La description doit faire moins de 2000 caractères'); $this->assert(null === $this->amount || $this->amount > 0, 'Le montant est invalide : ' . $this->amount); $this->assert($this->id_service, 'Aucun service n\'a été indiqué pour ce tarif.'); $this->assert((null === $this->id_account && null === $this->id_year) || (null !== $this->id_account && null !== $this->id_year), 'Le compte doit être indiqué avec l\'exercice'); $this->assert(null === $this->id_account || $db->test(Account::TABLE, 'id = ?', $this->id_account), 'Le compte indiqué n\'existe pas'); $this->assert(null === $this->id_year || $db->test(Year::TABLE, 'id = ?', $this->id_year), 'L\'exercice indiqué n\'existe pas'); $this->assert(null === $this->id_account || $db->test(Account::TABLE, 'id = ? AND id_chart = (SELECT id_chart FROM acc_years WHERE id = ?)', $this->id_account, $this->id_year), 'Le compte sélectionné ne correspond pas à l\'exercice'); $this->assert(null === $this->id_analytical || $db->test(Account::TABLE, 'id = ? AND id_chart = (SELECT id_chart FROM acc_years WHERE id = ?)', $this->id_analytical, $this->id_year), 'Le projet sélectionné ne correspond pas à l\'exercice'); if (null !== $this->formula && ($error = $this->checkFormula())) { throw new ValidationException('Formule de calcul invalide: ' . $error); } $this->assert(null === $this->amount || null === $this->formula, 'Il n\'est pas possible de spécifier à la fois une formule et un montant'); } public function getAmountForUser(int $user_id): ?int { if ($this->amount) { return $this->amount; |
︙ | ︙ | |||
110 111 112 113 114 115 116 | } protected function getFormulaSQL() { return sprintf('SELECT %s FROM membres WHERE id = ?;', $this->formula); } | | | | | | | 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 | } protected function getFormulaSQL() { return sprintf('SELECT %s FROM membres WHERE id = ?;', $this->formula); } protected function checkFormula(): ?string { try { $db = DB::getInstance(); $sql = $this->getFormulaSQL(); $db->protectSelect(['membres' => null], $sql); return null; } catch (DB_Exception $e) { return $e->getMessage(); } } public function service() { return EntityManager::findOneById(Service::class, $this->id_service); } public function activeUsersList(): DynamicList { $identity = Config::getInstance()->get('champ_identite'); $columns = [ 'id_user' => [ 'select' => 'su.id_user', ], 'identity' => [ |
︙ | ︙ | |||
159 160 161 162 163 164 165 | $tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_fee) AS su2 ON su2.id = su.id LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id LEFT JOIN acc_transactions_lines l ON l.id_transaction = tu.id_transaction'; | | | | | 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 | $tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_fee) AS su2 ON su2.id = su.id LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id LEFT JOIN acc_transactions_lines l ON l.id_transaction = tu.id_transaction'; $conditions = sprintf('su.id_fee = %d AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list = new DynamicList($columns, $tables, $conditions); $list->groupBy('su.id_user'); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT su.id_user)'); $list->setExportCallback(function (&$row) { $row->paid_amount = $row->paid_amount ? Utils::money_format($row->paid_amount, '.', '', false) : null; }); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->activeUsersList(); $conditions = sprintf('su.id_fee = %d AND su.paid = 0 AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function expiredUsersList(): DynamicList { $list = $this->activeUsersList(); $conditions = sprintf('su.id_fee = %d AND su.expiry_date < date() AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } } |
Modified src/include/lib/Garradin/Entities/Services/Service.php from [bac627a89f] to [b496e0d5d1].
︙ | ︙ | |||
29 30 31 32 33 34 35 | 'start_date' => '?date', 'end_date' => '?date', ]; public function selfCheck(): void { parent::selfCheck(); | | | | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 'start_date' => '?date', 'end_date' => '?date', ]; public function selfCheck(): void { parent::selfCheck(); $this->assert(trim((string) $this->label) !== '', 'Le libellé doit être renseigné'); $this->assert(strlen((string) $this->label) <= 200, 'Le libellé doit faire moins de 200 caractères'); $this->assert(strlen((string) $this->description) <= 2000, 'La description doit faire moins de 2000 caractères'); $this->assert(!isset($this->duration, $this->start_date, $this->end_date) || $this->duration || ($this->start_date && $this->end_date), 'Seulement une option doit être choisie : durée ou dates de début et de fin de validité'); $this->assert(null === $this->start_date || $this->start_date instanceof \DateTimeInterface); $this->assert(null === $this->end_date || $this->end_date instanceof \DateTimeInterface); $this->assert(null === $this->duration || (is_int($this->duration) && $this->duration > 0), 'La durée n\'est pas valide'); $this->assert(null === $this->start_date || $this->end_date > $this->start_date, 'La date de fin de validité doit être après la date de début'); } |
︙ | ︙ | |||
65 66 67 68 69 70 71 | } public function fees() { return new Fees($this->id()); } | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | } public function fees() { return new Fees($this->id()); } public function activeUsersList(): DynamicList { $identity = Config::getInstance()->get('champ_identite'); $columns = [ 'id_user' => [ ], 'end_date' => [ ], |
︙ | ︙ | |||
102 103 104 105 106 107 108 | 'select' => 'su.date', ], ]; $tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services s ON s.id = su.id_service | | | | | | > > > > > > > > > > > > > > | 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 | 'select' => 'su.date', ], ]; $tables = 'services_users su INNER JOIN membres m ON m.id = su.id_user INNER JOIN services s ON s.id = su.id_service LEFT JOIN services_fees sf ON sf.id = su.id_fee INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) AS su2 ON su2.id = su.id'; $conditions = sprintf('su.id_service = %d AND (su.expiry_date >= date() OR su.expiry_date IS NULL) AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list = new DynamicList($columns, $tables, $conditions); $list->groupBy('su.id_user'); $list->orderBy('date', true); $list->setCount('COUNT(DISTINCT su.id_user)'); return $list; } public function unpaidUsersList(): DynamicList { $list = $this->activeUsersList(); $conditions = sprintf('su.id_service = %d AND su.paid = 0 AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function expiredUsersList(): DynamicList { $list = $this->activeUsersList(); $conditions = sprintf('su.id_service = %d AND su.expiry_date < date() AND m.id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)', $this->id()); $list->setConditions($conditions); return $list; } public function getUsers(bool $paid_only = false) { $where = $paid_only ? 'AND paid = 1' : ''; $id_field = Config::getInstance()->champ_identite; $sql = sprintf('SELECT su.id_user, u.%s FROM services_users su INNER JOIN membres u ON u.id = su.id_user WHERE su.id_service = ? %s;', $id_field, $where); return DB::getInstance()->getAssoc($sql, $this->id()); } public function long_label(): string { if ($this->duration) { $duration = sprintf('%d jours', $this->duration); } elseif ($this->start_date) $duration = sprintf('du %s au %s', $this->start_date->format('d/m/Y'), $this->end_date->format('d/m/Y')); else { $duration = 'ponctuelle'; } return sprintf('%s — %s', $this->label, $duration); } } |
Modified src/include/lib/Garradin/Entities/Services/Service_User.php from [9c410c5349] to [e92524dd0e].
︙ | ︙ | |||
55 56 57 58 59 60 61 62 | 'id_service' => $this->id_service, ]; if ($using_date) { $params['date'] = $this->date->format('Y-m-d'); } if ($this->exists()) { | > > > | < < < | | | | 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 | 'id_service' => $this->id_service, ]; if ($using_date) { $params['date'] = $this->date->format('Y-m-d'); } $where = array_map(fn($k) => sprintf('%s = ?', $k), array_keys($params)); $where = implode(' AND ', $where); if ($this->exists()) { $where .= sprintf(' AND id != %d', $this->id()); } return DB::getInstance()->test(self::TABLE, $where, array_values($params)); } public function importForm(?array $source = null) { if (null === $source) { $source = $_POST; } if (!empty($source['id_service']) && empty($source['expiry_date'])) { $service = $this->_service = Services::get((int) $source['id_service']); if (!$service) { throw new \LogicException('The requested service is not found'); } if ($service->duration) { $dt = new \DateTime; $dt->modify(sprintf('+%d days', $service->duration)); $this->set('expiry_date', $dt); } elseif ($service->end_date) { $this->set('expiry_date', $service->end_date); } else { $this->set('expiry_date', null); } } return parent::importForm($source); } public function service(): Service |
︙ | ︙ | |||
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | if (null === $source) { $source = $_POST; } if (!$this->id_fee) { throw new \RuntimeException('Cannot add a payment to a subscription that is not linked to a fee'); } $transaction = new Transaction; $transaction->id_creator = $user_id; $transaction->id_year = $this->fee()->id_year; $source['type'] = Transaction::TYPE_REVENUE; $key = sprintf('account_%d_', $source['type']); $source[$key . '0'] = [$this->fee()->id_account => '']; $source[$key . '1'] = isset($source['account']) ? $source['account'] : null; $label = $this->service()->label; if ($this->fee()->label != $label) { $label .= ' - ' . $this->fee()->label; } $label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user)); $source['label'] = $label; $transaction->importFromNewForm($source); $transaction->save(); $transaction->linkToUser($this->id_user, $this->id()); return $transaction; } | > > > > > | > > > > | | 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 | if (null === $source) { $source = $_POST; } if (!$this->id_fee) { throw new \RuntimeException('Cannot add a payment to a subscription that is not linked to a fee'); } if (!$this->fee()->id_year) { throw new ValidationException('Le tarif indiqué ne possède pas d\'exercice lié'); } $transaction = new Transaction; $transaction->id_creator = $user_id; $transaction->id_year = $this->fee()->id_year; $source['type'] = Transaction::TYPE_REVENUE; $key = sprintf('account_%d_', $source['type']); $source[$key . '0'] = [$this->fee()->id_account => '']; $source[$key . '1'] = isset($source['account']) ? $source['account'] : null; $label = $this->service()->label; if ($this->fee()->label != $label) { $label .= ' - ' . $this->fee()->label; } $label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user)); $source['label'] = $label; $source['id_analytical'] = $this->fee()->id_analytical; $transaction->importFromNewForm($source); $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; } $db = DB::getInstance(); $db->begin(); if (!count($users)) { throw new ValidationException('Aucun membre n\'a été sélectionné.'); } foreach ($users as $id => $name) { $su = new self; $su->date = new \DateTime; $su->importForm($source); $su->id_user = (int) $id; 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; } |
︙ | ︙ |
Added src/include/lib/Garradin/Entities/Users/Email.php version [f93e1cad4f].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php declare(strict_types=1); namespace Garradin\Entities\Users; use Garradin\Entity; use Garradin\UserException; use Garradin\Users\Emails; use KD2\SMTP; use const Garradin\{WWW_URL, SECRET_KEY}; class Email extends Entity { const TABLE = 'emails'; /** * Antispam services that require to do a manual action to accept emails */ const BLACKLIST_MANUAL_VALIDATION_MX = '/mailinblack\.com|spamenmoins\.com/'; const COMMON_DOMAINS = ['laposte.net', 'gmail.com', 'hotmail.fr', 'hotmail.com', 'wanadoo.fr', 'free.fr', 'sfr.fr', 'yahoo.fr', 'orange.fr', 'live.fr', 'outlook.fr', 'yahoo.com', 'neuf.fr', 'outlook.com', 'icloud.com', 'riseup.net', 'vivaldi.net', 'aol.com', 'gmx.de', 'lilo.org', 'mailo.com', 'protonmail.com']; protected int $id; protected string $hash; protected bool $verified = false; protected bool $optout = false; protected bool $invalid = false; protected int $sent_count = 0; protected int $fail_count = 0; protected ?string $fail_log; protected \DateTime $added; protected ?\DateTime $last_sent; /** * Normalize email address and create a hash from this */ static public function getHash(string $email): string { $email = strtolower(trim($email)); $host = substr($email, strrpos($email, '@')+1); $host = idn_to_ascii($host); $email = substr($email, 0, strrpos($email, '@')+1) . $host; return sha1($email); } static public function getOptoutURL(string $hash = null): string { $hash = hex2bin($hash); $hash = base64_encode($hash); // Make base64 hash valid for URLs $hash = rtrim(strtr($hash, '+/', '-_'), '='); return sprintf('%s?un=%s', WWW_URL, $hash); } public function getVerificationCode(): string { $code = sha1($this->hash . SECRET_KEY); return substr($code, 0, 10); } public function sendVerification(string $email): void { if (self::getHash($email) !== $this->hash) { throw new UserException('Adresse email inconnue'); } $message = "Bonjour,\n\nPour vérifier votre adresse e-mail pour notre association,\ncliquez sur le lien ci-dessous :\n\n"; $message.= self::getOptoutURL($this->hash) . '&v=' . $this->getVerificationCode(); $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le."; Emails::queue(Emails::CONTEXT_SYSTEM, [$email => null], null, 'Confirmez votre adresse e-mail', $message); } public function verify(string $code): bool { if ($code !== $this->getVerificationCode()) { return false; } $this->set('verified', true); $this->set('optout', false); $this->set('invalid', false); $this->set('fail_count', 0); $this->set('fail_log', null); return true; } public function validate(string $email): bool { if (!$this->canSend()) { return false; } try { self::validateAddress($email); } catch (UserException $e) { $this->hasFailed(['type' => 'permanent', 'message' => $e->getMessage()]); return false; } return true; } static public function validateAddress(string $email): void { $pos = strrpos($email, '@'); if ($pos === false) { throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.'); } $user = substr($email, 0, $pos); $host = substr($email, $pos+1); // Ce domaine n'existe pas (MX inexistant), erreur de saisie courante if ($host == 'gmail.fr') { throw new UserException('Adresse invalide : "gmail.fr" n\'existe pas, il faut utiliser "gmail.com"'); } if (preg_match('![/@]!', $user)) { throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.'); } if (!SMTP::checkEmailIsValid($email, false)) { if (!trim($host)) { throw new UserException('Adresse e-mail invalide : vérifiez que vous n\'avez pas fait une faute de frappe.'); } foreach (self::COMMON_DOMAINS as $common_domain) { similar_text($common_domain, $host, $percent); 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.'); } $mx_list = array_filter($mx_list, fn ($mx) => !preg_match(self::BLACKLIST_MANUAL_VALIDATION_MX, $mx) ); if (!count($mx_list)) { 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.'); } } public function canSend(): bool { if (!empty($this->optout)) { return false; } if (!empty($this->invalid)) { return false; } if ($this->hasReachedFailLimit()) { return false; } return true; } public function hasReachedFailLimit(): bool { return !empty($this->fail_count) && ($this->fail_count >= Emails::FAIL_LIMIT); } public function incrementSentCount(): void { $this->set('sent_count', $this->sent_count+1); } public function setOptout(): void { $this->set('optout', true); $this->appendFailLog('Demande de désinscription'); } public function appendFailLog(string $message): void { $log = $this->fail_log ?? ''; if ($log) { $log .= "\n"; } $log .= date('d/m/Y H:i:s - ') . trim($message); $this->set('fail_log', $log); } public function hasFailed(array $return): void { if (!isset($return['type'])) { throw new \InvalidArgumentException('Bounce email type not supplied in argument.'); } // Treat complaints as opt-out if ($return['type'] == 'complaint') { $this->set('optout', true); $this->appendFailLog("Un signalement de spam a été envoyé par le destinataire.\n: " . $return['message']); } elseif ($return['type'] == 'permanent') { $this->set('invalid', true); $this->set('fail_count', $this->fail_count+1); $this->appendFailLog($return['message']); } elseif ($return['type'] == 'temporary') { $this->set('fail_count', $this->fail_count+1); $this->appendFailLog($return['message']); } } } |
Modified src/include/lib/Garradin/Entities/Web/Page.php from [d8d50d5663] to [cb4f32a782].
︙ | ︙ | |||
43 44 45 46 47 48 49 | 'status' => 'string', 'format' => 'string', 'published' => 'DateTime', 'modified' => 'DateTime', 'content' => 'string', ]; | | < < | < | | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 'status' => 'string', 'format' => 'string', 'published' => 'DateTime', 'modified' => 'DateTime', 'content' => 'string', ]; const FORMATS_LIST = [ //Render::FORMAT_BLOCKS => 'Blocs (beta)', Render::FORMAT_SKRIV => 'SkrivML', Render::FORMAT_MARKDOWN => 'MarkDown', Render::FORMAT_ENCRYPTED => 'Chiffré', ]; const STATUS_ONLINE = 'online'; const STATUS_DRAFT = 'draft'; const STATUS_LIST = [ self::STATUS_ONLINE => 'En ligne', |
︙ | ︙ | |||
73 74 75 76 77 78 79 80 81 82 83 84 85 86 | self::TYPE_CATEGORY => 'category.html', ]; const DUPLICATE_URI_ERROR = 42; protected $_file; protected $_attachments; static public function create(int $type, ?string $parent, string $title, string $status = self::STATUS_ONLINE): self { $page = new self; $data = compact('type', 'parent', 'title', 'status'); $data['content'] = ''; | > | 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | self::TYPE_CATEGORY => 'category.html', ]; const DUPLICATE_URI_ERROR = 42; protected $_file; protected $_attachments; protected $_tagged_attachments; static public function create(int $type, ?string $parent, string $title, string $status = self::STATUS_ONLINE): self { $page = new self; $data = compact('type', 'parent', 'title', 'status'); $data['content'] = ''; |
︙ | ︙ | |||
104 105 106 107 108 109 110 | if (null === $this->_file || $force_reload) { $this->_file = Files::get($this->filepath()); } return $this->_file; } | | > > | 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | if (null === $this->_file || $force_reload) { $this->_file = Files::get($this->filepath()); } return $this->_file; } public function load(array $data): self { parent::load($data); if ($this->file() && $this->file()->modified > $this->modified) { $this->loadFromFile($this->file()); $this->save(); } return $this; } public function url(): string { return WWW_URL . $this->uri; } |
︙ | ︙ | |||
190 191 192 193 194 195 196 | } $this->syncSearch(); } public function syncSearch(): void { | | | 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | } $this->syncSearch(); } public function syncSearch(): void { $content = $this->format == Render::FORMAT_ENCRYPTED ? null : strip_tags($this->render()); $this->file()->indexForSearch(null, $content, $this->title); } public function save(): bool { $change_parent = null; |
︙ | ︙ | |||
268 269 270 271 272 273 274 | $parent = $this->parent; if (isset($source['title']) && is_null($this->path)) { $source['uri'] = $source['title']; } if (isset($source['uri'])) { | | | | | 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 | $parent = $this->parent; if (isset($source['title']) && is_null($this->path)) { $source['uri'] = $source['title']; } if (isset($source['uri'])) { $source['uri'] = strtolower(Utils::transformTitleToURI($source['uri'])); $source['path'] = trim($parent . '/' . $source['uri'], '/'); } $uri = $source['uri'] ?? $this->uri; if (array_key_exists('parent', $source)) { if (is_array($source['parent'])) { $source['parent'] = key($source['parent']); } if (empty($source['parent'])) { $source['parent'] = ''; } $parent = $source['parent']; $source['path'] = trim($parent . '/' . $uri, '/'); } if (!empty($source['encryption']) ) { $this->set('format', Render::FORMAT_ENCRYPTED); } else { $this->set('format', Render::FORMAT_SKRIV); } return parent::importForm($source); } public function getBreadcrumbs(): array { |
︙ | ︙ | |||
327 328 329 330 331 332 333 | $this->_attachments = $list; } return $this->_attachments; } | > > > | > > > > | > > | > > > > | > > | > > > > > > > | 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 | $this->_attachments = $list; } return $this->_attachments; } /** * List attachments that are cited in the text content */ public function listTaggedAttachments(): array { if (null === $this->_tagged_attachments) { $this->render(); $this->_tagged_attachments = Render::listAttachments($this->file()); } return $this->_tagged_attachments; } /** * List attachments that are *NOT* cited in the text content */ public function listOrphanAttachments(): array { $used = $this->listTaggedAttachements(); $orphans = []; foreach ($this->listAttachments() as $file) { if (!in_array($file->uri(), $used)) { $orphans[] = $file->uri(); } } return $orphans; } /** * Return list of images * If $all is FALSE then this will only return images that are not present in the content */ public function getImageGallery(bool $all = true): array |
︙ | ︙ | |||
354 355 356 357 358 359 360 | */ public function getAttachmentsGallery(bool $all = true, bool $images = false): array { $out = []; $tagged = []; if (!$all) { | | | 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | */ public function getAttachmentsGallery(bool $all = true, bool $images = false): array { $out = []; $tagged = []; if (!$all) { $tagged = $this->listTaggedAttachments($this->content); } foreach ($this->listAttachments() as $a) { if ($images && !$a->image) { continue; } elseif (!$images && $a->image) { |
︙ | ︙ | |||
391 392 393 394 395 396 397 | $out = ''; foreach ($meta as $key => $value) { $out .= sprintf("%s: %s\n", $key, $value); } | > | | | 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | $out = ''; foreach ($meta as $key => $value) { $out .= sprintf("%s: %s\n", $key, $value); } $content = preg_replace("/\r\n?/", "\n", $this->content); $out .= "\n----\n\n" . $content; return $out; } public function importFromRaw(string $str): bool { $str = preg_replace("/\r\n?/", "\n", $str); $str = explode("\n\n----\n\n", $str, 2); if (count($str) !== 2) { return false; } list($meta, $content) = $str; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entity.php from [46fdcfd8d1] to [e47a8d6cfd].
︙ | ︙ | |||
30 31 32 33 34 35 36 | return $this->import($source); } protected function filterUserValue(string $type, $value, string $key) { if ($type == 'date') { | | | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | return $this->import($source); } protected function filterUserValue(string $type, $value, string $key) { if ($type == 'date') { if (!trim((string) $value)) { return null; } if (preg_match('!^\d{2}/\d{2}/\d{2}$!', $value)) { return \DateTime::createFromFormat('d/m/y', $value); } elseif (preg_match('!^\d{2}/\d{2}/\d{4}$!', $value)) { |
︙ | ︙ | |||
75 76 77 78 79 80 81 | } } // Add plugin signals to save/delete public function save(): bool { $name = get_class($this); | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | } } // Add plugin signals to save/delete public function save(): bool { $name = get_class($this); $name = str_replace('Garradin\Entities\\', '', $name); $name = 'entity.' . $name . '.save'; // Specific entity signal if (Plugin::fireSignal($name . '.before', ['entity' => $this])) { return true; } |
︙ | ︙ | |||
99 100 101 102 103 104 105 | return $return; } public function delete(): bool { $name = get_class($this); | | | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | return $return; } public function delete(): bool { $name = get_class($this); $name = str_replace('Garradin\Entities\\', '', $name); $name = 'entity.' . $name . '.delete'; if (Plugin::fireSignal($name . '.before', ['entity' => $this])) { return true; } // Generic entity signal |
︙ | ︙ |
Modified src/include/lib/Garradin/Files/Storage/FileSystem.php from [286067b967] to [3a2659d851].
︙ | ︙ | |||
263 264 265 266 267 268 269 | } $total = 0; $path = self::_getRoot(); foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD) as $p) { | > > > > > > | > > > > | 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 | } $total = 0; $path = self::_getRoot(); foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD) as $p) { if (substr($p->getBaseName(), 0, 1) == '.') { // Ignore dot files continue; } try { $total += $p->getSize(); } catch (\RuntimeException $e) { // Ignore file that vanished } } self::$_size = (float) $total; return self::$_size; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Files/Storage/SQLite.php from [3cfb59d324] to [99e5f6c9f5].
︙ | ︙ | |||
118 119 120 121 122 123 124 | { $sql = 'SELECT * FROM @TABLE WHERE path = ? LIMIT 1;'; return EM::findOne(File::class, $sql, $path); } static public function list(string $path): array { | | | 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | { $sql = 'SELECT * FROM @TABLE WHERE path = ? LIMIT 1;'; return EM::findOne(File::class, $sql, $path); } static public function list(string $path): array { return EM::getInstance(File::class)->all('SELECT * FROM @TABLE WHERE parent = ? ORDER BY type DESC, name COLLATE U_NOCASE ASC;', $path); } static public function listDirectoriesRecursively(string $path): array { $files = []; $it = DB::getInstance()->iterate('SELECT path FROM files WHERE (parent = ? OR parent LIKE ?) AND type = ? ORDER BY path;', $path, $path . '/%', File::TYPE_DIRECTORY); |
︙ | ︙ |
Modified src/include/lib/Garradin/Form.php from [7d0dc0a0a9] to [b4e4fab8c9].
︙ | ︙ | |||
25 26 27 28 29 30 31 | && isset($_SERVER['REQUEST_METHOD']) && !empty($_SERVER['CONTENT_LENGTH']) && strtoupper($_SERVER['REQUEST_METHOD']) == 'POST') { $this->addError('Le fichier envoyé dépasse la taille autorisée'); } } | | | | 25 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 | && isset($_SERVER['REQUEST_METHOD']) && !empty($_SERVER['CONTENT_LENGTH']) && 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; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Install.php from [a878545951] to [68a0642e6b].
1 2 3 4 | <?php namespace Garradin; | | | > > > | > > > > > > > > > > > > > | > > > > > > | > > > > > > > > > > > > > > > > > > > > | | | < < | | < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | <?php namespace Garradin; use Garradin\Accounting\Charts; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Year; use Garradin\Entities\Users\Category; use Garradin\Entities\Files\File; use Garradin\Membres\Session; /** * Pour procéder à l'installation de l'instance Garradin * Utile pour automatiser l'installation sans passer par la page d'installation */ class Install { /** * Reset the database to empty and create a new user with the same password */ static public function reset(Session $session, string $password, array $options = []) { $config = (object) Config::getInstance()->asArray(); $user = $session->getUser(); if (!$session->checkPassword($password, $user->passe)) { throw new UserException('Le mot de passe ne correspond pas.'); } if (!trim($config->nom_asso)) { throw new UserException('Le nom de l\'association est vide, merci de le renseigner dans la configuration.'); } if (!trim($user->identite)) { throw new UserException('L\'utilisateur connecté ne dispose pas de nom, merci de le renseigner.'); } if (!trim($user->email)) { throw new UserException('L\'utilisateur connecté ne dispose pas d\'adresse e-mail, merci de la renseigner.'); } (new Sauvegarde)->create(date('Y-m-d-His-') . 'avant-remise-a-zero'); Config::deleteInstance(); DB::getInstance()->close(); DB::deleteInstance(); file_put_contents(CACHE_ROOT . '/reset', json_encode([ 'password' => $session::hashPassword($password), 'name' => $user->identite, 'email' => $user->email, 'organization' => $config->nom_asso, ])); rename(DB_FILE, sprintf(DATA_ROOT . '/association.%s.sqlite', date('Y-m-d-His-') . 'avant-remise-a-zero')); self::showProgressSpinner('!install.php', 'Remise à zéro en cours…'); exit; } /** * Continues reset after page reload */ static public function checkReset() { if (!file_exists(CACHE_ROOT . '/reset')) { return; } $data = json_decode(file_get_contents(CACHE_ROOT . '/reset')); if (!$data) { throw new \LogicException('Invalid reset data'); } // We can't use the real password, as it might not be valid (too short or compromised) $ok = self::install($data->organization ?? 'Association', $data->name, $data->email, md5($data->password)); // Restore password DB::getInstance()->preparedQuery('UPDATE membres SET passe = ? WHERE id = 1;', [$data->password]); if (defined('\Garradin\LOCAL_LOGIN') && \Garradin\LOCAL_LOGIN) { Session::getInstance()->refresh(); } @unlink(CACHE_ROOT . '/reset'); Utils::redirect('!config/advanced/?msg=RESET'); } static protected function assert(bool $assertion, string $message) { if (!$assertion) { throw new ValidationException($message); } |
︙ | ︙ | |||
77 78 79 80 81 82 83 | } catch (\Exception $e) { @unlink(DB_FILE); throw $e; } } | | > > > > | | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | } catch (\Exception $e) { @unlink(DB_FILE); throw $e; } } static public function install(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(); $db = DB::getInstance(); // Création de la base de données $db->begin(); $db->exec('PRAGMA application_id = ' . DB::APPID . ';'); $db->setVersion(garradin_version()); $db->exec(file_get_contents(DB_SCHEMA)); $db->commit(); |
︙ | ︙ | |||
152 153 154 155 156 157 158 | $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 | | < < < < < | 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | $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::install('fr_pca_2018'); // Create first accounting year $year = new Year; $year->label = sprintf('Exercice %d', date('Y')); $year->start_date = new \DateTime('January 1st'); $year->end_date = new \DateTime('December 31'); $year->id_chart = $chart->id(); |
︙ | ︙ | |||
226 227 228 229 230 231 232 | // Install welcome plugin if available $has_welcome_plugin = Plugin::getPath('welcome', false); if ($has_welcome_plugin) { Plugin::install('welcome', true); } | | | 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | // Install welcome plugin if available $has_welcome_plugin = Plugin::getPath('welcome', false); if ($has_welcome_plugin) { Plugin::install('welcome', true); } $config->save(); } static public function checkAndCreateDirectories() { // Vérifier que les répertoires vides existent, sinon les créer $paths = [ DATA_ROOT, |
︙ | ︙ | |||
293 294 295 296 297 298 299 | $config = '<?php' . PHP_EOL . 'namespace Garradin;' . PHP_EOL . PHP_EOL . $new_line . PHP_EOL; } return file_put_contents($path, $config); } | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | $config = '<?php' . PHP_EOL . 'namespace Garradin;' . PHP_EOL . PHP_EOL . $new_line . PHP_EOL; } return file_put_contents($path, $config); } static public function showProgressSpinner(?string $next = null, string $message = '') { $next = $next ? sprintf('<meta http-equiv="refresh" content="0;url=%s" />', Utils::getLocalURL($next)) : ''; printf('<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style type="text/css"> body { font-family: sans-serif; } h2, p { margin: 0; margin-bottom: 1rem; } div { position: relative; border: 1px solid #999; max-width: 500px; padding: 1em; border-radius: .5em; } .spinner h2::after { display: block; content: " "; margin: 1rem auto; width: 50px; height: 50px; border: 5px solid #000; border-radius: 50%%; border-top-color: #999; animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } </style> %s </head> <body> <div class="spinner"> <h2>%s</h2> </div>', $next, htmlspecialchars($message)); } } |
Modified src/include/lib/Garradin/Membres.php from [d8d2f9c7cf] to [081983cad8].
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\SMTP; use Garradin\Membres\Session; use Garradin\Files\Files; use Garradin\Entities\Files\File; class Membres { const ITEMS_PER_PAGE = 50; // Gestion des données /////////////////////////////////////////////////////// | > > > > | 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 KD2\Security; use KD2\SMTP; use Garradin\Membres\Session; use Garradin\Files\Files; use Garradin\Entities\Files\File; use Garradin\Entities\Users\Email; use Garradin\Users\Emails; use Garradin\UserTemplate\UserTemplate; class Membres { const ITEMS_PER_PAGE = 50; // Gestion des données /////////////////////////////////////////////////////// |
︙ | ︙ | |||
87 88 89 90 91 92 93 | $data[$key] = 0; } } elseif ($config->type == 'email') { $data[$key] = strtolower(trim($data[$key])); | | > > > > | > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | $data[$key] = 0; } } elseif ($config->type == 'email') { $data[$key] = strtolower(trim($data[$key])); if (trim($data[$key]) !== '') { try { Email::validateAddress($data[$key]); } catch (UserException $e) { throw new UserException(sprintf('Champ "%s" : %s', $config->title, $e->getMessage())); } } } elseif ($config->type == 'select' && !in_array($data[$key], $config->options)) { throw new UserException('Le champ "' . $config->title . '" ne correspond pas à un des choix proposés.'); } elseif ($config->type == 'multiple') |
︙ | ︙ | |||
120 121 122 123 124 125 126 | } } $data[$key] = $binary; } elseif (!is_numeric($data[$key]) || $data[$key] < 0 || $data[$key] > PHP_INT_MAX) { | | | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | } } $data[$key] = $binary; } elseif (!is_numeric($data[$key]) || $data[$key] < 0 || $data[$key] > PHP_INT_MAX) { throw new UserException(sprintf('Le champs "%s" ne contient pas une valeur binaire.', $key)); } } // Un champ texte vide c'est un champ NULL if (is_string($data[$key]) && trim($data[$key]) === '') { $data[$key] = null; |
︙ | ︙ | |||
157 158 159 160 161 162 163 | { throw new UserException('Ce numéro de membre est déjà attribué à un autre membre.'); } } $this->_checkFields($data, true, $require_password); | | | 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | { throw new UserException('Ce numéro de membre est déjà attribué à un autre membre.'); } } $this->_checkFields($data, true, $require_password); if (isset($data[$id]) && $db->test('membres', $id . ' = ? COLLATE U_NOCASE', $data[$id])) { throw new UserException('La valeur du champ '.$id.' est déjà utilisée par un autre membre, or ce champ doit être unique à chaque membre.'); } if (isset($data['passe']) && trim($data['passe']) != '') { Session::checkPasswordValidity($data['passe']); |
︙ | ︙ | |||
196 197 198 199 200 201 202 | unset($data['id']); $this->_checkFields($data, $check_editable, false); $champ_id = $config->get('champ_identifiant'); if (!empty($data[$champ_id]) | | | 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | unset($data['id']); $this->_checkFields($data, $check_editable, false); $champ_id = $config->get('champ_identifiant'); if (!empty($data[$champ_id]) && $db->firstColumn('SELECT 1 FROM membres WHERE '.$champ_id.' = ? COLLATE U_NOCASE AND id != ? LIMIT 1;', $data[$champ_id], (int)$id)) { throw new UserException('La valeur du champ '.$champ_id.' est déjà utilisée par un autre membre, or ce champ doit être unique à chaque membre.'); } if (isset($data['numero'])) { if (!preg_match('/^\d+$/', $data['numero'])) |
︙ | ︙ | |||
327 328 329 330 331 332 333 | $operator = 'LIKE ? ESCAPE \'\\\''; } $sql = sprintf('SELECT id, numero, %s AS identite FROM membres WHERE %s %s ORDER BY %1$s LIMIT 50;', $identity, $column, $operator); return DB::getInstance()->get($sql, $query); } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | 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 | $operator = 'LIKE ? ESCAPE \'\\\''; } $sql = sprintf('SELECT id, numero, %s AS identite FROM membres WHERE %s %s ORDER BY %1$s LIMIT 50;', $identity, $column, $operator); return DB::getInstance()->get($sql, $query); } public function listAllButHidden(): array { return DB::getInstance()->get('SELECT * FROM membres WHERE id_category IN (SELECT id FROM users_categories WHERE hidden = 0) AND email IS NOT NULL AND email != \'\';'); } public function listAllByCategory($id_category, $only_with_email = false) { $where = $only_with_email ? ' AND email IS NOT NULL' : ''; return DB::getInstance()->get('SELECT * FROM membres WHERE id_category = ?' . $where, (int)$id_category); } public function listByCategory(?int $id_category): DynamicList { $config = Config::getInstance(); $db = DB::getInstance(); $identity = $config->get('champ_identite'); |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Champs.php from [df5852c7ec] to [c845f8077c].
︙ | ︙ | |||
26 27 28 29 30 31 32 | 'email' => 'Adresse E-Mail', 'url' => 'Adresse URL', 'checkbox' => 'Case à cocher', 'date' => 'Date', 'datetime' => 'Date et heure', 'file' => 'Fichier', 'password' => 'Mot de passe', | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 'email' => 'Adresse E-Mail', 'url' => 'Adresse URL', 'checkbox' => 'Case à cocher', 'date' => 'Date', 'datetime' => 'Date et heure', 'file' => 'Fichier', 'password' => 'Mot de passe', 'number' => 'Nombre', 'tel' => 'Numéro de téléphone', 'select' => 'Sélecteur à choix unique', 'multiple' => 'Sélecteur à choix multiple', 'country' => 'Sélecteur de pays', 'text' => 'Texte', 'textarea' => 'Texte multi-lignes', ]; |
︙ | ︙ | |||
498 499 500 501 502 503 504 | 'id_category INTEGER NOT NULL REFERENCES users_categories(id),', 'date_connexion TEXT NULL CHECK (date_connexion IS NULL OR datetime(date_connexion) = date_connexion), -- Date de dernière connexion', 'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date_inscription) IS NOT NULL AND date(date_inscription) = date_inscription), -- Date d\'inscription', 'secret_otp TEXT NULL, -- Code secret pour TOTP', 'clef_pgp TEXT NULL, -- Clé publique PGP' ]; | | < | | < < | | 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 | 'id_category INTEGER NOT NULL REFERENCES users_categories(id),', 'date_connexion TEXT NULL CHECK (date_connexion IS NULL OR datetime(date_connexion) = date_connexion), -- Date de dernière connexion', 'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date_inscription) IS NOT NULL AND date(date_inscription) = date_inscription), -- Date d\'inscription', 'secret_otp TEXT NULL, -- Code secret pour TOTP', 'clef_pgp TEXT NULL, -- Clé publique PGP' ]; $last_one = array_key_last((array)$this->champs); foreach ($this->champs as $key=>$cfg) { if ($cfg->type == 'number' || $cfg->type == 'multiple' || $cfg->type == 'checkbox') $type = 'INTEGER'; elseif ($cfg->type == 'file') $type = 'BLOB'; else $type = 'TEXT COLLATE U_NOCASE'; $line = sprintf('%s %s', $db->quoteIdentifier($key), $type); if ($last_one != $key) { $line .= ','; } if (!empty($cfg->title)) { $line .= ' -- ' . str_replace(["\n", "\r"], '', $cfg->title); } $create[] = $line; } $sql = sprintf("CREATE TABLE %s\n(\n\t%s\n);", $table_name, implode("\n\t", $create)); return $sql; } public function getCopyFields(bool $same = false): array { // Champs à recopier $copy = [ 'id' => 'id', 'id_category' => 'id_category', 'date_connexion' => 'date_connexion', 'date_inscription' => 'date_inscription', 'secret_otp' => 'secret_otp', 'clef_pgp' => 'clef_pgp', ]; $anciens_champs = $same ? null : Config::getInstance()->get('champs_membres'); $anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll(); foreach ($this->champs as $key=>$cfg) { if (property_exists($anciens_champs, $key)) { $copy[$key] = $key; } |
︙ | ︙ | |||
590 591 592 593 594 595 596 | } public function createTable(string $table_name = self::TABLE): void { DB::getInstance()->exec($this->getSQLSchema($table_name)); } | | | | | | | | | 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 | } public function createTable(string $table_name = self::TABLE): void { DB::getInstance()->exec($this->getSQLSchema($table_name)); } public function createIndexes(string $table_name = self::TABLE, string $id_field = null): void { $db = DB::getInstance(); $id_field ??= Config::getInstance()->champ_identifiant; if ($id_field) { // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique $db->exec(sprintf('UPDATE %s SET %s = NULL WHERE %2$s = \'\';', $table_name, $id_field)); $collation = ''; if ($this->isText($id_field)) { $collation = ' COLLATE U_NOCASE'; } // Création de l'index unique $db->exec(sprintf('CREATE UNIQUE INDEX IF NOT EXISTS users_id_field ON %s (%s%s);', $table_name, $id_field, $collation)); } $db->exec(sprintf('CREATE UNIQUE INDEX IF NOT EXISTS user_number ON %s (numero);', $table_name)); $db->exec(sprintf('CREATE INDEX IF NOT EXISTS users_category ON %s (id_category);', $table_name)); // Create index on listed columns // FIXME: these indexes are currently unused by SQLite in the default user list // when there is more than one non-hidden category, as this makes SQLite merge multiple results // and so the index is not useful in that case sadly. // EXPLAIN QUERY PLAN SELECT * FROM membres WHERE "id_category" IN (3) ORDER BY "nom" ASC LIMIT 0,100; // --> SEARCH TABLE membres USING INDEX users_list_nom (id_category=?) // EXPLAIN QUERY PLAN SELECT * FROM membres WHERE "id_category" IN (3, 7) ORDER BY "nom" ASC LIMIT 0,100; // --> SEARCH TABLE membres USING INDEX user_category (id_category=?) // USE TEMP B-TREE FOR ORDER BY $listed_fields = array_keys((array) $this->getListedFields()); foreach ($listed_fields as $field) { if ($field === $id_field) { // Il y a déjà un index continue; } $collation = ''; if ($this->isText($field)) { $collation = ' COLLATE U_NOCASE'; } $db->exec(sprintf('CREATE INDEX IF NOT EXISTS users_list_%s ON %s (id_category, %s%s);', $field, $table_name, $db->quoteIdentifier($field), $collation)); } } /** * Enregistre les changements de champs en base de données * @return boolean true */ |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Import.php from [9fc2a43ba6] to [efbfcdc67d].
︙ | ︙ | |||
104 105 106 107 108 109 110 111 112 113 114 115 116 117 | $line++; if (empty($row)) { continue; } if ($line == 1) { if (empty($row[0]) || !is_string($row[0]) || is_numeric($row[0])) { $db->rollback(); throw new UserException('Erreur sur la ligne 1 : devrait contenir l\'en-tête des colonnes.'); } | > > > | 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | $line++; if (empty($row)) { continue; } // Make sure the data is UTF-8 encoded $row = array_map(fn ($a) => Utils::utf8_encode(trim($a)), $row); if ($line == 1) { if (empty($row[0]) || !is_string($row[0]) || is_numeric($row[0])) { $db->rollback(); throw new UserException('Erreur sur la ligne 1 : devrait contenir l\'en-tête des colonnes.'); } |
︙ | ︙ | |||
245 246 247 248 249 250 251 252 253 254 255 256 257 258 | public function exportRow(\stdClass $row) { if (null === $this->champs) { $this->champs = Config::getInstance()->get('champs_membres')->getAll(); } foreach ($this->champs as $id => $config) { if ($config->type == 'date') { $row->$id = \DateTime::createFromFormat('!Y-m-d', $row->$id); } elseif ($config->type == 'datetime') { $row->$id = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->$id); } // convertir les champs à choix multiple de binaire vers liste séparée par des points virgules | > > > > | 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | public function exportRow(\stdClass $row) { if (null === $this->champs) { $this->champs = Config::getInstance()->get('champs_membres')->getAll(); } foreach ($this->champs as $id => $config) { if (!isset($row->$id)) { continue; } if ($config->type == 'date') { $row->$id = \DateTime::createFromFormat('!Y-m-d', $row->$id); } elseif ($config->type == 'datetime') { $row->$id = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->$id); } // convertir les champs à choix multiple de binaire vers liste séparée par des points virgules |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Session.php from [3164774ac4] to [47bb0d9b71].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin\Membres; use Garradin\Config; use Garradin\DB; use Garradin\Utils; use Garradin\Membres; use Garradin\UserException; use Garradin\Plugin; use const Garradin\SECRET_KEY; use const Garradin\WWW_URL; use const Garradin\ADMIN_URL; use KD2\Security; use KD2\Security_OTP; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin\Membres; use Garradin\Config; use Garradin\DB; use Garradin\Utils; use Garradin\Membres; use Garradin\UserException; use Garradin\Plugin; use Garradin\Users\Emails; use const Garradin\SECRET_KEY; use const Garradin\WWW_URL; use const Garradin\ADMIN_URL; use KD2\Security; use KD2\Security_OTP; |
︙ | ︙ | |||
100 101 102 103 104 105 106 | { $champ_id = Config::getInstance()->get('champ_identifiant'); // Ne renvoie un membre que si celui-ci a le droit de se connecter $query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret FROM membres AS m INNER JOIN users_categories AS c ON c.id = m.id_category | | | 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | { $champ_id = Config::getInstance()->get('champ_identifiant'); // Ne renvoie un membre que si celui-ci a le droit de se connecter $query = 'SELECT m.id, m.%1$s AS login, m.passe AS password, m.secret_otp AS otp_secret FROM membres AS m INNER JOIN users_categories AS c ON c.id = m.id_category WHERE m.%1$s = ? COLLATE U_NOCASE AND c.perm_connect >= %2$d LIMIT 1;'; $query = sprintf($query, $champ_id, self::ACCESS_READ); return $this->db->first($query, $login); } |
︙ | ︙ | |||
156 157 158 159 160 161 162 | } protected function deleteAllRememberMeSelectors($user_id) { return $this->db->delete('membres_sessions', $this->db->where('id_membre', $user_id)); } | < > | < | < < < < < < < < < < < < > > > > > > > > > > > | > > > | 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 | } protected function deleteAllRememberMeSelectors($user_id) { return $this->db->delete('membres_sessions', $this->db->where('id_membre', $user_id)); } public function isLogged(bool $disable_local_login = false) { $logged = parent::isLogged(); // Ajout de la gestion de LOCAL_LOGIN if (!$disable_local_login && defined('\Garradin\LOCAL_LOGIN')) { $logged = $this->forceLogin(\Garradin\LOCAL_LOGIN); } return $logged; } public function forceLogin(int $id) { // On va chercher le premier membre avec le droit de gérer la config if (-1 === $id) { $id = $this->db->firstColumn('SELECT id FROM membres WHERE id_category IN (SELECT id FROM users_categories WHERE perm_config = ?) LIMIT 1', self::ACCESS_ADMIN); } $logged = parent::isLogged(); // Only login if required if ($id > 0 && (!$logged || ($logged && $this->user->id != $id))) { return $this->create($id); } return $logged; } // Ici checkOTP utilise NTP en second recours public function checkOTP($secret, $code) { if (Security_OTP::TOTP($secret, $code)) { |
︙ | ︙ | |||
231 232 233 234 235 236 237 | public function recoverPasswordSend($id) { $db = DB::getInstance(); $config = Config::getInstance(); $champ_id = $config->get('champ_identifiant'); | | | 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | public function recoverPasswordSend($id) { $db = DB::getInstance(); $config = Config::getInstance(); $champ_id = $config->get('champ_identifiant'); $membre = $db->first('SELECT id, email, passe, clef_pgp FROM membres WHERE '.$champ_id.' = ? COLLATE U_NOCASE LIMIT 1;', trim($id)); if (!$membre || trim($membre->email) == '') { return false; } // valide pour 1 heure minimum |
︙ | ︙ | |||
254 255 256 257 258 259 260 | $query = sprintf('%s.%s.%s', $id, $expire, $hash); $message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n"; $message.= "Il vous suffit de cliquer sur le lien ci-dessous pour modifier votre mot de passe.\n\n"; $message.= ADMIN_URL . 'password.php?c=' . $query; $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé."; | > > > | > > | 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | $query = sprintf('%s.%s.%s', $id, $expire, $hash); $message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n"; $message.= "Il vous suffit de cliquer sur le lien ci-dessous pour modifier votre mot de passe.\n\n"; $message.= ADMIN_URL . 'password.php?c=' . $query; $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé."; if ($membre->clef_pgp) { $content = Security::encryptWithPublicKey($membre->clef_pgp, $message); } Emails::queue(Emails::CONTEXT_SYSTEM, [$membre->email => null], null, 'Mot de passe perdu ?', $message); return true; } public function recoverPasswordCheck($code, &$membre = null) { if (substr_count($code, '.') !== 2) { return false; |
︙ | ︙ | |||
323 324 325 326 327 328 329 | $message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n"; $message.= "Votre adresse email : ".$membre->email."\n"; $message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n"; $message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler."; DB::getInstance()->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$membre->id]); | | | 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | $message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n"; $message.= "Votre adresse email : ".$membre->email."\n"; $message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n"; $message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler."; DB::getInstance()->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$membre->id]); return Emails::queue(Emails::CONTEXT_SYSTEM, [$membre->email => null], null, 'Mot de passe changé', $message); } public function editUser($data) { (new Membres)->edit($this->user->id, $data, false); $this->refresh(); |
︙ | ︙ | |||
355 356 357 358 359 360 361 | { if (!$this->getUser()) { return false; } $perm_name = 'perm_' . $category; | | | 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | { if (!$this->getUser()) { return false; } $perm_name = 'perm_' . $category; $perm = $this->getUser()->$perm_name ?? null; return ($perm >= $permission); } public function requireAccess($category, $permission) { if (!$this->canAccess($category, $permission)) |
︙ | ︙ | |||
390 391 392 393 394 395 396 | $user = $this->getUser(); $content = "Ce message vous a été envoyé par :\n"; $content.= sprintf("%s\n%s\n\n", $user->identite, $user->email); $content.= str_repeat('=', 70) . "\n\n"; $content.= $message; | < < | | | < | 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | $user = $this->getUser(); $content = "Ce message vous a été envoyé par :\n"; $content.= sprintf("%s\n%s\n\n", $user->identite, $user->email); $content.= str_repeat('=', 70) . "\n\n"; $content.= $message; $dest = $copie ? [$dest => null, $user->email => null] : [$dest => null]; return Emails::queue(Emails::CONTEXT_PRIVATE, $dest, null, $sujet, $content); } public function editSecurity(Array $data = []) { $allowed_fields = ['passe', 'clef_pgp', 'secret_otp']; foreach ($data as $key=>$value) |
︙ | ︙ |
Modified src/include/lib/Garradin/Plugin.php from [d9b7c0cb75] to [14ad010455].
︙ | ︙ | |||
25 26 27 28 29 30 31 | 'pdf' => 'application/pdf', 'png' => 'image/png', 'swf' => 'application/shockwave-flash', 'xml' => 'text/xml', 'svg' => 'image/svg+xml', ]; | | < < < < < | | 25 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 | 'pdf' => 'application/pdf', 'png' => 'image/png', 'swf' => 'application/shockwave-flash', 'xml' => 'text/xml', 'svg' => 'image/svg+xml', ]; 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; } /** * 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($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 |
︙ | ︙ | |||
193 194 195 196 197 198 199 | */ public function call($file) { $file = preg_replace('!^[./]*!', '', $file); if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file)) { | | > > > > > > | | | 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 | */ public function call($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($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.'); } if (!file_exists($path . '/www/' . $file)) { throw new UserException('Le fichier ' . $file . ' n\'existe pas dans le plugin ' . $this->id); } if (is_dir($path . '/www/' . $file)) { throw new UserException(sprintf('Sécurité : impossible de lister le répertoire "%s" du plugin "%s".', $file, $this->id)); } if (substr($file, -4) === '.php') { // Créer l'environnement d'exécution du plugin |
︙ | ︙ | |||
358 359 360 361 362 363 364 | * 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 nom;'); | < < | 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | * 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 nom;'); foreach ($plugins as &$row) { $row->disabled = !self::getPath($row->id, false); } return $plugins; } /** |
︙ | ︙ | |||
393 394 395 396 397 398 399 | $plugin->upgrade(); } unset($plugin); } } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < > | > > | | 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 | $plugin->upgrade(); } unset($plugin); } } /** * 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 = []; // First let plugins handle self::fireSignal('menu.item', compact('session'), $list); $db = DB::getInstance(); $plugins = $db->getGrouped('SELECT id, nom, menu_condition FROM plugins WHERE menu = 1 ORDER BY nom;'); // FIXME deprecated $fix_legacy = [ '{Membres::DROIT_AUCUN}' => '{ACCESS_NONE}', '{Membres::DROIT_ACCES}' => '{ACCESS_READ}', '{Membres::DROIT_ECRITURE}' => '{ACCESS_WRITE}', '{Membres::DROIT_ADMIN}' => '{ACCESS_ADMIN}', |
︙ | ︙ | |||
460 461 462 463 464 465 466 | $permissions = [ '{ACCESS_NONE}' => $session::ACCESS_NONE, '{ACCESS_READ}' => $session::ACCESS_READ, '{ACCESS_WRITE}' => $session::ACCESS_WRITE, '{ACCESS_ADMIN}' => $session::ACCESS_ADMIN, ]; | | < | 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 | $permissions = [ '{ACCESS_NONE}' => $session::ACCESS_NONE, '{ACCESS_READ}' => $session::ACCESS_READ, '{ACCESS_WRITE}' => $session::ACCESS_WRITE, '{ACCESS_ADMIN}' => $session::ACCESS_ADMIN, ]; foreach ($plugins as $id => $row) { if (!self::getPath($row->id, false)) { // Ne pas lister les plugins dont le code a disparu unset($list[$id]); continue; } if (!$row->menu_condition) { continue; } $new_condition = strtr($row->menu_condition, $fix_legacy); // FIXME: legacy if ($new_condition != $row->menu_condition) { |
︙ | ︙ | |||
504 505 506 507 508 509 510 | $query = 'SELECT 1 WHERE ' . $condition . ';'; $res = $db->protectSelect(['membres' => []], $query); if (!$db->firstColumn($query)) { | | | | > > | | 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | $query = 'SELECT 1 WHERE ' . $condition . ';'; $res = $db->protectSelect(['membres' => []], $query); if (!$db->firstColumn($query)) { unset($plugins[$id]); continue; } } foreach ($plugins as $id => $row) { $list['plugin_' . $id] = sprintf('<a href="%s%s/">%s</a>', Utils::getLocalURL('!plugin/'), $id, $row->nom); } ksort($list); return $list; } /** * Liste les plugins téléchargés mais non installés * @return array Liste des plugins téléchargés |
︙ | ︙ | |||
758 759 760 761 762 763 764 | alors que le plugin nécessite le stockage d\'une configuration.'); } $config = json_decode(file_get_contents($path . '/config.json')); if (is_null($config)) { | | | 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 | alors que le plugin nécessite le stockage d\'une configuration.'); } $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()); } $config = json_encode($config); } $data = [ 'id' => $id, |
︙ | ︙ | |||
812 813 814 815 816 817 818 | /** * 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, | | > > > > > > > > > > > > > > > < < | < | 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 | /** * 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) { // 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_signaux 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; |
︙ | ︙ |
Modified src/include/lib/Garradin/Recherche.php from [5e796b3eb4] to [77d31061bf].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use Garradin\Entities\Accounting\Transaction; class Recherche { const TYPE_JSON = 'json'; const TYPE_SQL = 'sql'; const TYPE_SQL_UNPROTECTED = 'sql_unprotected'; | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin; use Garradin\Entities\Accounting\Transaction; use Garradin\Accounting\Accounts; use Garradin\Accounting\Years; class Recherche { const TYPE_JSON = 'json'; const TYPE_SQL = 'sql'; const TYPE_SQL_UNPROTECTED = 'sql_unprotected'; |
︙ | ︙ | |||
227 228 229 230 231 232 233 | $champs = Config::getInstance()->get('champs_membres'); $columns['id_category'] = (object) [ 'textMatch'=> false, 'label' => 'Catégorie', 'type' => 'enum', 'null' => false, | | | 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | $champs = Config::getInstance()->get('champs_membres'); $columns['id_category'] = (object) [ 'textMatch'=> false, 'label' => 'Catégorie', 'type' => 'enum', 'null' => false, 'values' => $db->getAssoc('SELECT id, name FROM users_categories ORDER BY name COLLATE U_NOCASE;'), ]; foreach ($champs->getList() as $champ => $config) { $column = (object) [ 'textMatch'=> $champs->isText($champ), 'label' => $config->title, |
︙ | ︙ | |||
536 537 538 539 540 541 542 | $query_columns = array_merge(['t.id', 't.date', 't.label', 'l.debit', 'l.credit', 'a.code'], $query_columns); } $query_columns[] = $order; if ($target_columns[$order]->textMatch) { | | | 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 | $query_columns = array_merge(['t.id', 't.date', 't.label', 'l.debit', 'l.credit', 'a.code'], $query_columns); } $query_columns[] = $order; if ($target_columns[$order]->textMatch) { $order = sprintf('%s COLLATE U_NOCASE', $db->quoteIdentifier($order)); } else { $order = $db->quoteIdentifier($order); } $query_columns = array_unique($query_columns); |
︙ | ︙ | |||
610 611 612 613 614 615 616 | if (!in_array($target, self::TARGETS, true)) { throw new \InvalidArgumentException('Cible inconnue : ' . $target); } if (null !== $force_select) { | | | 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 | if (!in_array($target, self::TARGETS, true)) { throw new \InvalidArgumentException('Cible inconnue : ' . $target); } if (null !== $force_select) { $query = preg_replace('/^\s*SELECT\s+(.*)\s+FROM\s+/Uis', 'SELECT $1, ' . implode(', ', $force_select) . ' FROM ', $query); } if (!$no_limit && !preg_match('/LIMIT\s+\d+/i', $query)) { $query = preg_replace('/;?\s*$/', '', $query); $query .= ' LIMIT 100'; } |
︙ | ︙ | |||
650 651 652 653 654 655 656 | throw new UserException($message); } } public function searchQuery(string $table, $query, $order, $desc = false, $limit = 100) { | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | throw new UserException($message); } } public function searchQuery(string $table, $query, $order, $desc = false, $limit = 100) { $sql_query = $this->buildQuery($table, $query, $order, $desc, $limit); return $this->searchSQL($table, $sql_query); } public function buildSimpleMemberQuery(string $query) { $operator = 'LIKE %?%'; if (is_numeric(trim($query))) { $column = 'numero'; $operator = '= ?'; } elseif (strpos($query, '@') !== false) { $column = 'email'; } else { $column = Config::getInstance()->get('champ_identite'); } $query = [[ 'operator' => 'AND', 'conditions' => [ [ 'column' => $column, 'operator' => $operator, 'values' => [$query], ], ], ]]; return (object) [ 'query' => $query, 'order' => $column, 'desc' => false, 'limit' => 50, ]; } public function buildSimpleAccountingQuery(string $text, ?int $id_year = null) { $query = []; $text = trim($text); if ($id_year) { $query[] = [ 'operator' => 'AND', 'conditions' => [ [ 'column' => 't.id_year', 'operator' => '= ?', 'values' => [$id_year], ], ], ]; } // Match number: find transactions per credit or debit if (preg_match('/^=\s*\d+([.,]\d+)?$/', $text)) { $text = ltrim($text, "\n\t ="); $query[] = [ 'operator' => 'OR', 'conditions' => [ [ 'column' => 'l.debit', 'operator' => '= ?', 'values' => [$text], ], [ 'column' => 'l.credit', 'operator' => '= ?', 'values' => [$text], ], ], ]; } // Match account number elseif ($id_year && preg_match('/^[0-9]+[A-Z]*$/', $text) && ($year = Years::get($id_year)) && ($id = (new Accounts($year->id_chart))->getIdFromCode($text))) { return sprintf('!acc/accounts/journal.php?id=%d&year=%d', $id, $id_year); } // Match date elseif (preg_match('!^\d{2}/\d{2}/\d{4}$!', $text) && ($d = Utils::get_datetime($text))) { $query[] = [ 'operator' => 'OR', 'conditions' => [ [ 'column' => 't.date', 'operator' => '= ?', 'values' => [$d->format('Y-m-d')], ], ], ]; } // Match transaction ID elseif (preg_match('/^#[0-9]+$/', $text)) { return sprintf('!acc/transactions/details.php?id=%d', (int)substr($text, 1)); } // Or search in label or reference else { $operator = 'LIKE %?%'; $query[] = [ 'operator' => 'OR', 'conditions' => [ [ 'column' => 't.label', 'operator' => $operator, 'values' => [$text], ], [ 'column' => 't.reference', 'operator' => $operator, 'values' => [$text], ], [ 'column' => 'l.reference', 'operator' => $operator, 'values' => [$text], ], ], ]; } return (object) [ 'query' => $query, 'order' => 't.id', 'desc' => true, 'limit' => 50, ]; } public function schema(string $target) { $db = DB::getInstance(); if ($target == 'membres') { |
︙ | ︙ |
Modified src/include/lib/Garradin/Sauvegarde.php from [4c0d503058] to [955ef36c50].
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 | use KD2\ZipWriter; class Sauvegarde { const NEED_UPGRADE = 0x01 << 2; const NOT_AN_ADMIN = 0x01 << 3; const INTEGRITY_FAIL = 41; const NOT_A_DB = 42; const NO_APP_ID = 43; /** * Renvoie la liste des fichiers SQLite sauvegardés | > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | use KD2\ZipWriter; class Sauvegarde { const NEED_UPGRADE = 0x01 << 2; const NOT_AN_ADMIN = 0x01 << 3; const CHANGED_USER = 0x01 << 4; const INTEGRITY_FAIL = 41; const NOT_A_DB = 42; const NO_APP_ID = 43; /** * Renvoie la liste des fichiers SQLite sauvegardés |
︙ | ︙ | |||
117 118 119 120 121 122 123 124 125 126 127 128 129 130 | // use VACUUM INTO instead when SQLite 3.27+ is required $db->exec(sprintf('VACUUM INTO %s;', $db->quote($dest))); } else { // use ::backup since PHP 7.4.0+ // https://www.php.net/manual/en/sqlite3.backup.php $dest_db = new \SQLite3($dest); $db->backup($dest_db); $dest_db->exec('PRAGMA journal_mode = DELETE;'); $dest_db->exec('VACUUM;'); $db->close(); } } | > | 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | // use VACUUM INTO instead when SQLite 3.27+ is required $db->exec(sprintf('VACUUM INTO %s;', $db->quote($dest))); } else { // use ::backup since PHP 7.4.0+ // https://www.php.net/manual/en/sqlite3.backup.php $dest_db = new \SQLite3($dest); $dest_db->createCollation('U_NOCASE', [Utils::class, 'unicodeCaseComparison']); $db->backup($dest_db); $dest_db->exec('PRAGMA journal_mode = DELETE;'); $dest_db->exec('VACUUM;'); $db->close(); } } |
︙ | ︙ | |||
320 321 322 323 324 325 326 | return $this->restoreDB(DATA_ROOT . '/' . $file, false, false); } /** * Restaure une copie distante (fichier envoyé) * @param array $file Tableau provenant de $_FILES | < < | | | 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 | return $this->restoreDB(DATA_ROOT . '/' . $file, false, false); } /** * Restaure une copie distante (fichier envoyé) * @param array $file Tableau provenant de $_FILES * @param boolean $check_integrity Vérifier l'intégrité de la sauvegarde avant de restaurer * @return boolean true */ public function restoreFromUpload($file, $check_integrity = true) { if (empty($file['size']) || empty($file['tmp_name']) || !empty($file['error'])) { throw new UserException('Le fichier n\'a pas été correctement envoyé. Essayer de le renvoyer à nouveau.'); } if ($check_integrity) { $integrity = $this->checkIntegrity($file['tmp_name']); if ($integrity === null) { throw new UserException('Le fichier fourni n\'est pas une base de donnée SQLite3.', self::NOT_A_DB); } elseif ($integrity === false) { throw new UserException('Le fichier fourni a été modifié par un programme externe.', self::INTEGRITY_FAIL); } } $r = $this->restoreDB($file['tmp_name'], true); if ($r) { Utils::safe_unlink($file['tmp_name']); } return $r; |
︙ | ︙ | |||
408 409 410 411 412 413 414 | /** * Restauration de base de données, la fonction qui le fait vraiment * @param string $file Chemin absolu vers la base de données à utiliser * @return mixed true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB * ne correspond pas à la version de Garradin (mise à jour nécessaire). */ | | | 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | /** * Restauration de base de données, la fonction qui le fait vraiment * @param string $file Chemin absolu vers la base de données à utiliser * @return mixed true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB * ne correspond pas à la version de Garradin (mise à jour nécessaire). */ protected function restoreDB($file, $check_foreign_keys = false) { $return = 1; // Essayons déjà d'ouvrir la base de données à restaurer en lecture try { $db = new \SQLite3($file, \SQLITE3_OPEN_READONLY); } |
︙ | ︙ | |||
476 477 478 479 480 481 482 483 484 | // Vérification de l'AppID $appid = $db->querySingle('PRAGMA application_id;', false); if ($appid !== DB::APPID) { throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID); } // Empêchons l'admin de se tirer une balle dans le pied | > > | | | | 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 | // Vérification de l'AppID $appid = $db->querySingle('PRAGMA application_id;', false); if ($appid !== DB::APPID) { throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (application_id ne correspond pas).', self::NO_APP_ID); } $session = Session::getInstance(); // Empêchons l'admin de se tirer une balle dans le pied if ($session->isLogged()) { if (version_compare($version, '1.1.0', '<')) { // FIXME remove in 1.2 $sql = 'SELECT 1 FROM membres_categories WHERE id = (SELECT id_categorie FROM membres WHERE id = %d) AND droit_connexion >= %d AND droit_config >= %d'; } else { $sql = 'SELECT 1 FROM users_categories WHERE id = (SELECT id_category FROM membres WHERE id = %d) AND perm_connect >= %d AND perm_config >= %d'; } $sql = sprintf($sql, $session->getUser()->id, Session::ACCESS_READ, Session::ACCESS_ADMIN); $is_still_admin = $db->querySingle($sql); if (!$is_still_admin) { $return |= self::NOT_AN_ADMIN; } } |
︙ | ︙ | |||
516 517 518 519 520 521 522 | { rename($backup, DB_FILE); throw new \RuntimeException('Unable to copy backup DB to main location.'); } unlink($backup); | | > > > > > < < < | 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 | { rename($backup, DB_FILE); throw new \RuntimeException('Unable to copy backup DB to main location.'); } unlink($backup); if (($return & self::NOT_AN_ADMIN) && version_compare($version, '1.1.0', '>=')) { // Forcer toutes les catégories à pouvoir gérer les droits $db = DB::getInstance(); $db->update('users_categories', [ 'perm_users' => Session::ACCESS_ADMIN, 'perm_connect' => Session::ACCESS_READ ]); } if (version_compare($version, '1.1.0', '>=') && $session->isLogged(true) && !$session->refresh()) { $session->forceLogin(-1); $return |= self::CHANGED_USER; } if ($version != garradin_version()) { $return |= self::NEED_UPGRADE; } else { // Check and upgrade plugins, if a software upgrade is necessary, plugins will be upgraded after the upgrade Plugin::upgradeAllIfRequired(); } return $return; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Services/Fees.php from [f232ea6af4] to [3134c2a122].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\Users\Categories; use Garradin\Entities\Services\Fee; use Garradin\Entities\Accounting\Year; use KD2\DB\EntityManager; class Fees { protected $service_id; public function __construct(int $id) { | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\UserException; use Garradin\Users\Categories; use Garradin\Entities\Services\Fee; use Garradin\Entities\Accounting\Year; use KD2\DB\EntityManager; use KD2\DB\DB_Exception; class Fees { protected $service_id; public function __construct(int $id) { |
︙ | ︙ | |||
41 42 43 44 45 46 47 | * If $user_id is specified, then it will return a column 'user_amount' containing the amount that this specific user should pay */ static public function listAllByService(?int $user_id = null) { $db = DB::getInstance(); $sql = 'SELECT *, CASE WHEN amount THEN amount ELSE NULL END AS user_amount | | | > > > > > > > > > | 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 | * If $user_id is specified, then it will return a column 'user_amount' containing the amount that this specific user should pay */ static public function listAllByService(?int $user_id = null) { $db = DB::getInstance(); $sql = 'SELECT *, CASE WHEN amount THEN amount ELSE NULL END AS user_amount FROM services_fees ORDER BY id_service, amount IS NULL, label COLLATE U_NOCASE;'; $result = $db->get($sql); if (!$user_id) { return $result; } foreach ($result as &$row) { if (!$row->formula) { continue; } try { $sql = sprintf('SELECT %s FROM membres WHERE id = %d;', $row->formula, $user_id); $row->user_amount = $db->firstColumn($sql); } catch (DB_Exception $e) { $row->label .= sprintf(' (**FORMULE DE CALCUL INVALIDE: %s**)', $e->getMessage()); $row->description .= "\n\n**MERCI DE CORRIGER LA FORMULE**"; $row->user_amount = -1; } } return $result; } public function listWithStats() |
︙ | ︙ | |||
74 75 76 77 78 79 80 | $sql = sprintf('SELECT f.*, (%s AND (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1) AS nb_users_ok, (%1$s AND expiry_date < date()) AS nb_users_expired, (%1$s AND paid = 0) AS nb_users_unpaid FROM services_fees f WHERE id_service = ? | | | 85 86 87 88 89 90 91 92 93 94 95 96 | $sql = sprintf('SELECT f.*, (%s AND (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1) AS nb_users_ok, (%1$s AND expiry_date < date()) AS nb_users_expired, (%1$s AND paid = 0) AS nb_users_unpaid FROM services_fees f WHERE id_service = ? ORDER BY amount, label COLLATE U_NOCASE;', $condition); return $db->get($sql, $this->service_id); } } |
Modified src/include/lib/Garradin/Services/Reminders.php from [c3c5d5e6f2] to [2361eca67b].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugin; use Garradin\Utils; use Garradin\Entities\Services\Reminder; use KD2\DB\EntityManager; use const Garradin\WWW_URL; use const Garradin\ADMIN_URL; class Reminders { static public function list() { return DB::getInstance()->get('SELECT s.label AS service_label, sr.* FROM services_reminders sr INNER JOIN services s ON s.id = sr.id_service | > | | 1 2 3 4 5 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\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugin; use Garradin\Utils; use Garradin\Users\Emails; use Garradin\Entities\Services\Reminder; use KD2\DB\EntityManager; use const Garradin\WWW_URL; use const Garradin\ADMIN_URL; class Reminders { static public function list() { return DB::getInstance()->get('SELECT s.label AS service_label, sr.* FROM services_reminders sr INNER JOIN services s ON s.id = sr.id_service ORDER BY s.label COLLATE U_NOCASE;'); } static public function get(int $id) { return EntityManager::findOneById(Reminder::class, $id); } |
︙ | ︙ | |||
111 112 113 114 115 116 117 | 'delai' => $reminder->delay, ]; $subject = self::replaceTagsInContent($reminder->subject, $replace); $text = self::replaceTagsInContent($reminder->body, $replace); // Envoi du mail | | | | 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 | 'delai' => $reminder->delay, ]; $subject = self::replaceTagsInContent($reminder->subject, $replace); $text = self::replaceTagsInContent($reminder->body, $replace); // Envoi du mail Emails::queue(Emails::CONTEXT_PRIVATE, [$reminder->email => $reminder], null, $subject, $text); $db = DB::getInstance(); $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 |
︙ | ︙ |
Modified src/include/lib/Garradin/Services/Services.php from [244e4a733a] to [25df3ae571].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\Users\Categories; use Garradin\Entities\Services\Service; use KD2\DB\EntityManager; class Services { static public function get(int $id) { return EntityManager::findOneById(Service::class, $id); } static public function listAssoc() { | > | | | > > > > > > > | | | > | | > > > > > > > > > > > > | > > > > | > > > > | > > | > > | | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin\Services; use Garradin\Config; use Garradin\DB; use Garradin\DynamicList; use Garradin\Users\Categories; use Garradin\Entities\Services\Service; use KD2\DB\EntityManager; class Services { static public function get(int $id) { return EntityManager::findOneById(Service::class, $id); } static public function listAssoc() { return DB::getInstance()->getAssoc('SELECT id, label FROM services ORDER BY label COLLATE U_NOCASE;'); } static public function count() { return DB::getInstance()->count(Service::TABLE, 1); } static public function listGroupedWithFees(?int $user_id = null, bool $current_only = true) { $where = $current_only ? 'WHERE end_date IS NULL OR end_date >= date()' : 'WHERE end_date IS NOT NULL AND end_date < date()'; $sql = sprintf('SELECT id, label, duration, start_date, end_date, description, CASE WHEN end_date IS NOT NULL THEN end_date WHEN duration IS NOT NULL THEN date(\'now\', \'+\'||duration||\' days\') ELSE NULL END AS expiry_date FROM services %s ORDER BY label COLLATE U_NOCASE;', $where); $services = DB::getInstance()->getGrouped($sql); $fees = Fees::listAllByService($user_id); $out = []; foreach ($services as $service) { $out[$service->id] = $service; $out[$service->id]->fees = []; } foreach ($fees as $fee) { if (isset($out[$fee->id_service])) { $out[$fee->id_service]->fees[] = $fee; } } return $out; } static public function listWithStats(bool $current_only = true): DynamicList { $db = DB::getInstance(); $hidden_cats = array_keys(Categories::listHidden()); $sql = sprintf('DROP TABLE IF EXISTS services_list_stats; CREATE TEMP TABLE IF NOT EXISTS services_list_stats (id_service, id_user, ok, expired, paid); INSERT INTO services_list_stats SELECT id_service, id_user, CASE WHEN (expiry_date IS NULL OR expiry_date >= date()) AND paid = 1 THEN 1 ELSE 0 END, CASE WHEN expiry_date < date() THEN 1 ELSE 0 END, paid FROM services_users su INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) su2 ON su2.id = su.id INNER JOIN membres m ON m.id = su.id_user WHERE %s', $db->where('m.id_category', 'NOT IN', $hidden_cats)); $db->exec($sql); $columns = [ 'id' => [], 'duration' => [], 'start_date' => [], 'end_date' => [], 'label' => [ 'label' => 'Activité', ], 'date' => [ 'label' => 'Période', 'order' => 'start_date %s, duration %1$s', 'select' => 'CASE WHEN start_date IS NULL THEN duration ELSE NULL END', ], 'nb_users_ok' => [ 'label' => 'Membres à jour', 'order' => null, 'select' => '(SELECT COUNT(DISTINCT id_user) FROM services_list_stats WHERE id_service = services.id AND ok = 1)', ], 'nb_users_expired' => [ 'label' => 'Membres expirés', 'order' => null, 'select' => '(SELECT COUNT(DISTINCT id_user) FROM services_list_stats WHERE id_service = services.id AND expired = 1)', ], 'nb_users_unpaid' => [ 'label' => 'Membres en attente de règlement', 'order' => null, 'select' => '(SELECT COUNT(DISTINCT id_user) FROM services_list_stats WHERE id_service = services.id AND paid = 0)', ], ]; $current_condition = $current_only ? '(end_date IS NULL OR end_date >= datetime())' : '(end_date IS NOT NULL AND end_date < datetime())'; $list = new DynamicList($columns, 'services', $current_condition); $list->setPageSize(null); $list->orderBy('label', false); return $list; } static public function countOldServices(): int { return DB::getInstance()->count(Service::TABLE, 'end_date IS NOT NULL AND end_date < datetime()'); } } |
Modified src/include/lib/Garradin/Services/Services_User.php from [02511a6caf] to [403bcb312a].
︙ | ︙ | |||
20 21 22 23 24 25 26 | return DB::getInstance()->count(Service_User::TABLE, 'id_user = ?', $user_id); } static public function listDistinctForUser(int $user_id) { return DB::getInstance()->get('SELECT s.label, MAX(su.date) AS last_date, su.expiry_date AS expiry_date, sf.label AS fee_label, su.paid, s.end_date, | | > | > > > | 20 21 22 23 24 25 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 | return DB::getInstance()->count(Service_User::TABLE, 'id_user = ?', $user_id); } static public function listDistinctForUser(int $user_id) { return DB::getInstance()->get('SELECT s.label, MAX(su.date) AS last_date, su.expiry_date AS expiry_date, sf.label AS fee_label, su.paid, s.end_date, CASE WHEN su.expiry_date < date() THEN -1 WHEN su.expiry_date >= date() THEN 1 ELSE 0 END AS status, CASE WHEN s.end_date < date() THEN 1 ELSE 0 END AS archived FROM services_users su INNER JOIN services s ON s.id = su.id_service INNER JOIN services_fees sf ON sf.id = su.id_fee WHERE su.id_user = ? GROUP BY su.id_service ORDER BY expiry_date DESC;', $user_id); } static public function perUserList(int $user_id, ?int $only_id = null): DynamicList { $columns = [ 'id' => [ 'select' => 'su.id', ], 'id_account' => [ 'select' => 'sf.id_account', ], 'has_transactions' => [ 'select' => 'tu.id_user', ], 'label' => [ 'select' => 's.label', 'label' => 'Activité', ], 'date' => [ 'label' => 'Date d\'inscription', 'select' => 'su.date', |
︙ | ︙ | |||
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | $tables = 'services_users su INNER JOIN services s ON s.id = su.id_service LEFT JOIN services_fees sf ON sf.id = su.id_fee LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id LEFT JOIN acc_transactions_lines tl ON tl.id_transaction = tu.id_transaction'; $conditions = sprintf('su.id_user = %d', $user_id); $list = new DynamicList($columns, $tables, $conditions); $list->setExportCallback(function (&$row) { $row->amount = $row->amount ? Utils::money_format($row->amount, '.', '', false) : null; }); $list->orderBy('date', true); $list->groupBy('su.id'); $list->setCount('COUNT(DISTINCT su.id)'); return $list; } } | > > > > | 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | $tables = 'services_users su INNER JOIN services s ON s.id = su.id_service LEFT JOIN services_fees sf ON sf.id = su.id_fee LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id LEFT JOIN acc_transactions_lines tl ON tl.id_transaction = tu.id_transaction'; $conditions = sprintf('su.id_user = %d', $user_id); if ($only_id) { $conditions .= sprintf(' AND su.id = %d', $only_id); } $list = new DynamicList($columns, $tables, $conditions); $list->setExportCallback(function (&$row) { $row->amount = $row->amount ? Utils::money_format($row->amount, '.', '', false) : null; }); $list->orderBy('date', true); $list->groupBy('su.id'); $list->setCount('COUNT(DISTINCT su.id)'); return $list; } } |
Modified src/include/lib/Garradin/Template.php from [caab472024] to [d7d299f70a].
︙ | ︙ | |||
20 21 22 23 24 25 26 | { return self::$_instance ?: self::$_instance = new Template; } public function display($template = null) { if (isset($_GET['_pdf'])) { | < < < < < < < < < < < | > > > > > > > > > > > > > > | 20 21 22 23 24 25 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 | { return self::$_instance ?: self::$_instance = new Template; } public function display($template = null) { if (isset($_GET['_pdf'])) { return $this->PDF($template); } return parent::display($template); } public function PDF(?string $template = null, ?string $title = null) { $out = $this->fetch($template); if (!$title && preg_match('!<title>(.*)</title>!U', $out, $match)) { $title = trim($match[1]); } header('Content-type: application/pdf'); header(sprintf('Content-Disposition: attachment; filename="%s.pdf"', Utils::safeFileName($title ?: 'Page'))); Utils::streamPDF($out); return $this; } private function __clone() { } public function __construct() { |
︙ | ︙ | |||
112 113 114 115 116 117 118 | return Form::tokenHTML($params['key']); }); $this->register_modifier('strlen', 'strlen'); $this->register_modifier('dump', ['KD2\ErrorManager', 'dump']); $this->register_modifier('get_country_name', ['Garradin\Utils', 'getCountryName']); $this->register_modifier('format_tel', [$this, 'formatPhoneNumber']); | | > > > > > > > > > | 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 | return Form::tokenHTML($params['key']); }); $this->register_modifier('strlen', 'strlen'); $this->register_modifier('dump', ['KD2\ErrorManager', 'dump']); $this->register_modifier('get_country_name', ['Garradin\Utils', 'getCountryName']); $this->register_modifier('format_tel', [$this, 'formatPhoneNumber']); $this->register_modifier('abs', function($a) { return abs($a ?? 0); }); $this->register_modifier('display_champ_membre', [$this, 'displayChampMembre']); $this->register_modifier('linkify_transactions', function ($str) { return preg_replace_callback('/(?<=^|\s)#(\d+)(?=\s|$)/', function ($m) { return sprintf('<a href="%s%d">#%2$d</a>', Utils::getLocalURL('!acc/transactions/details.php?id='), $m[1] ); }, $str); }); $this->register_modifier('format_skriv', function ($str) { $skriv = new Skriv; return $skriv->render((string) $str); }); foreach (CommonModifiers::MODIFIERS_LIST as $key => $name) { |
︙ | ︙ | |||
145 146 147 148 149 150 151 | return ''; } $errors = $form->getErrorMessages(!empty($params['membre']) ? true : false); foreach ($errors as &$error) { if ($error instanceof UserException) { | > > > > | > | 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | return ''; } $errors = $form->getErrorMessages(!empty($params['membre']) ? true : false); foreach ($errors as &$error) { if ($error instanceof UserException) { if ($html = $error->getHTMLMessage()) { $message = $html; } else { $message = nl2br($this->escape($error->getMessage())); } if ($error->hasDetails()) { $message = '<h3>' . $message . '</h3>' . $error->getDetailsHTML(); } $error = $message; } |
︙ | ︙ | |||
249 250 251 252 253 254 255 256 257 258 259 260 261 262 | } return $escape ? htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8') : $value; } protected function formatPhoneNumber($n) { $country = Config::getInstance()->get('pays'); if ($country !== 'FR') { return $n; } if ('FR' === $country && $n[0] === '0' && strlen($n) === 10) { | > > > > | 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | } return $escape ? htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8') : $value; } protected function formatPhoneNumber($n) { if (empty($n)) { return ''; } $country = Config::getInstance()->get('pays'); if ($country !== 'FR') { return $n; } if ('FR' === $country && $n[0] === '0' && strlen($n) === 10) { |
︙ | ︙ | |||
301 302 303 304 305 306 307 | protected function displayChampMembre($v, $config = null) { if (is_string($config)) { $config = Config::getInstance()->get('champs_membres')->get($config); } if (null === $config) { | | > > > > > > > > | | | | > > | | 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 | protected function displayChampMembre($v, $config = null) { if (is_string($config)) { $config = Config::getInstance()->get('champs_membres')->get($config); } if (null === $config) { return htmlspecialchars((string)$v); } if ($config->type == 'checkbox') { return $v ? 'Oui' : 'Non'; } if (empty($v)) { return ''; } switch ($config->type) { case 'password': return '*****'; case 'email': return '<a href="mailto:' . rawurlencode($v) . '">' . htmlspecialchars($v) . '</a>'; case 'tel': return '<a href="tel:' . rawurlencode($v) . '">' . htmlspecialchars($this->formatPhoneNumber($v)) . '</a>'; case 'url': return '<a href="' . htmlspecialchars($v) . '" target="_blank">' . htmlspecialchars($v) . '</a>'; case 'country': return Utils::getCountryName($v); case 'date': return Utils::date_fr($v, 'd/m/Y'); case 'datetime': return Utils::date_fr($v, 'd/m/Y à H:i'); case 'number': return str_replace('.', ',', htmlspecialchars($v)); case 'multiple': // Useful for search results, if a value is not a number if (!is_numeric($v)) { return htmlspecialchars($v); } $out = []; foreach ($config->options as $b => $name) { if ($v & (0x01 << $b)) $out[] = $name; } return htmlspecialchars(implode(', ', $out)); default: return nl2br(htmlspecialchars(rtrim((string) $v))); } } protected function formChampMembre($params) { if (empty($params['config']) || empty($params['name'])) throw new \BadFunctionCallException('Paramètres type et name obligatoires.'); |
︙ | ︙ | |||
406 407 408 409 410 411 412 | // Fix for autocomplete, lpignore is for Lastpass $attributes .= 'autocomplete="off" data-lpignore="true" '; if (!empty($params['user_mode']) && empty($config->editable)) { $out = '<dt>' . htmlspecialchars($config->title, ENT_QUOTES, 'UTF-8') . '</dt>'; | | | 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | // Fix for autocomplete, lpignore is for Lastpass $attributes .= 'autocomplete="off" data-lpignore="true" '; if (!empty($params['user_mode']) && empty($config->editable)) { $out = '<dt>' . htmlspecialchars($config->title, ENT_QUOTES, 'UTF-8') . '</dt>'; $out .= '<dd>' . (trim((string) $value) === '' ? 'Non renseigné' : $this->displayChampMembre($value, $config)) . '</dd>'; return $out; } if ($type == 'select') { $field .= '<select '.$attributes.'>'; foreach ($options as $k=>$v) |
︙ | ︙ | |||
456 457 458 459 460 461 462 | $b = 0x01 << (int)$k; $field .= sprintf('<input type="checkbox" name="%s[%d]" id="f_%1$s_%2$d" value="1" %s %s /> <label for="f_%1$s_%2$d">%s</label><br />', htmlspecialchars($params['name']), $k, ($value & $b) ? 'checked="checked"' : '', $attributes, htmlspecialchars($v)); } } elseif ($type == 'textarea') { | | > > > > > | > | | 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 | $b = 0x01 << (int)$k; $field .= sprintf('<input type="checkbox" name="%s[%d]" id="f_%1$s_%2$d" value="1" %s %s /> <label for="f_%1$s_%2$d">%s</label><br />', htmlspecialchars($params['name']), $k, ($value & $b) ? 'checked="checked"' : '', $attributes, htmlspecialchars($v)); } } elseif ($type == 'textarea') { $field .= '<textarea ' . $attributes . 'cols="30" rows="5">' . htmlspecialchars((string) $value, ENT_QUOTES) . '</textarea>'; } elseif ($type == 'date') { $field = self::formInput(['required' => $config->mandatory, 'name' => $params['name'], 'value' => $value, 'type' => 'date', 'default' => $value]); } else { if ($type == 'checkbox') { if (!empty($value)) { $attributes .= 'checked="checked" '; } $value = '1'; } elseif ($type == 'number') { $attributes .= 'step="any" '; } $field .= '<input type="' . $type . '" ' . $attributes . ' value="' . htmlspecialchars((string) $value, ENT_QUOTES) . '" />'; } $out = ' <dt>'; if ($type == 'checkbox') { |
︙ | ︙ |
Modified src/include/lib/Garradin/Upgrade.php from [b7b91e2163] to [b1fff63992].
︙ | ︙ | |||
177 178 179 180 181 182 183 184 185 186 187 188 189 | }, $content); if ($content != $r->contenu) { $db->update('wiki_revisions', ['contenu' => $content], 'rowid = :id', ['id' => $r->rowid]); } } $champs = new Champs($db->firstColumn('SELECT valeur FROM config WHERE cle = \'champs_membres\';')); $db->import(ROOT . '/include/data/1.1.0_migration.sql'); // Rename membres table $champs->createTable($champs::TABLE .'_tmp'); | > | | | 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 | }, $content); if ($content != $r->contenu) { $db->update('wiki_revisions', ['contenu' => $content], 'rowid = :id', ['id' => $r->rowid]); } } $id_field = $db->firstColumn('SELECT valeur FROM config WHERE cle = \'champ_identifiant\';'); $champs = new Champs($db->firstColumn('SELECT valeur FROM config WHERE cle = \'champs_membres\';')); $db->import(ROOT . '/include/data/1.1.0_migration.sql'); // Rename membres table $champs->createTable($champs::TABLE .'_tmp'); $fields = $champs->getCopyFields(true); unset($fields['id_category']); $fields['id_categorie'] = 'id_category'; $champs->copy($champs::TABLE, $champs::TABLE . '_tmp', $fields); $db->exec(sprintf('DROP TABLE IF EXISTS %s;', $champs::TABLE)); $db->exec(sprintf('ALTER TABLE %s_tmp RENAME TO %1$s;', $champs::TABLE)); $champs->createIndexes($champs::TABLE, $id_field); $db->commitSchemaUpdate(); // Migrate to a different storage if (FILE_STORAGE_BACKEND != 'SQLite') { Files::migrateStorage('SQLite', FILE_STORAGE_BACKEND, null, FILE_STORAGE_CONFIG); Files::truncateStorage('SQLite', null); |
︙ | ︙ | |||
265 266 267 268 269 270 271 | $db->begin(); $db->import(ROOT . '/include/data/1.1.3_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.4', '<')) { // Set config file names | < < | | | < | < | 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | $db->begin(); $db->import(ROOT . '/include/data/1.1.3_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.4', '<')) { // Set config file names $file = Files::get(Config::FILES['admin_background']); $db->update('config', ['value' => $file ? Config::FILES['admin_background'] : null], 'key = :key', ['key' => 'admin_background']); $file = Files::get(Config::FILES['admin_homepage']); $db->update('config', ['value' => $file ? Config::FILES['admin_homepage'] : null], 'key = :key', ['key' => 'admin_homepage']); } if (version_compare($v, '1.1.7', '<')) { $db->begin(); $db->import(ROOT . '/include/data/1.1.7_migration.sql'); $db->commit(); } |
︙ | ︙ | |||
364 365 366 367 368 369 370 371 372 373 374 375 376 377 | } $db->begin(); $db->exec('DELETE FROM config WHERE key IN (\'admin_background\', \'admin_css\', \'admin_homepage\');'); $db->exec(sprintf('INSERT INTO config (key, value) VALUES (\'files\', %s);', $db->quote(json_encode($files)))); $db->commit(); } if (version_compare($v, '1.2.0', '<')) { $db->begin(); $db->import(ROOT . '/include/data/1.2.0_migration.sql'); $files = $db->firstColumn('SELECT value FROM config WHERE key = \'files\';'); $files = json_decode($files); $files->signature = null; | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | } $db->begin(); $db->exec('DELETE FROM config WHERE key IN (\'admin_background\', \'admin_css\', \'admin_homepage\');'); $db->exec(sprintf('INSERT INTO config (key, value) VALUES (\'files\', %s);', $db->quote(json_encode($files)))); $db->commit(); } if (version_compare($v, '1.1.18', '<')) { $db->begin(); // Re-do the 1.1.15 migration as the LIKE did not work and accounts were not updated $db->import(ROOT . '/include/data/1.1.15_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.19', '<')) { $db->exec('VACUUM;'); // This will rebuild the index correctly, fixing the corrupted DB // Some people were able to insert invalid charsets in the database, this messes up the indexes // Let's try to fix that $db->createFunction('utf8_encode', [Utils::class, 'utf8_encode']); $db->beginSchemaUpdate(); // Now let's fix the content itself $res = $db->first('SELECT * FROM membres WHERE 1;'); $columns = array_keys((array) $res); $columns = array_map(fn($c) => sprintf('"%s" = utf8_encode("%1$s")', $c), $columns); $db->exec(sprintf('UPDATE membres SET %s;', implode(', ', $columns))); // Let's re-create users table with the correct index $champs = Config::getInstance()->champs_membres; $db->exec('ALTER TABLE membres RENAME TO membres_old;'); $db->commit(); $db->close(); $db->connect(); $db->beginSchemaUpdate(); $champs->create('membres'); $champs->copy('membres_old', 'membres'); $db->exec('DROP TABLE membres_old;'); // Set new types for accounts $db->import(ROOT . '/include/data/1.1.19_migration.sql'); $db->commitSchemaUpdate(); } if (version_compare($v, '1.1.21', '<')) { $db->begin(); // Add id_analytical column to services_fees $db->import(ROOT . '/include/data/1.1.21_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.22', '<')) { $db->begin(); // Create acc_accounts_balances view $db->import(ROOT . '/include/data/1.1.0_schema.sql'); $db->commit(); } if (version_compare($v, '1.1.23', '<')) { $db->begin(); // Create acc_accounts_projects_balances view $db->import(ROOT . '/include/data/1.1.0_schema.sql'); $db->commit(); } if (version_compare($v, '1.1.24', '<')) { $db->begin(); // Delete acc_accounts_projects_ebalances view $db->exec('DROP VIEW IF EXISTS acc_accounts_projects_balances;'); $db->commit(); } if (version_compare($v, '1.1.25', '<')) { $db->begin(); // Just add email tables $db->import(ROOT . '/include/data/1.1.0_schema.sql'); // Rename signals $db->import(ROOT . '/include/data/1.1.25_migration.sql'); $db->commit(); } if (version_compare($v, '1.1.27', '<')) { // Just add api_credentials tables $db->import(ROOT . '/include/data/1.1.0_schema.sql'); } if (version_compare($v, '1.2.0', '<')) { $db->begin(); $db->import(ROOT . '/include/data/1.2.0_migration.sql'); $files = $db->firstColumn('SELECT value FROM config WHERE key = \'files\';'); $files = json_decode($files); $files->signature = null; |
︙ | ︙ | |||
415 416 417 418 419 420 421 | $db->setVersion(garradin_version()); // reset last version check $db->exec('UPDATE config SET value = NULL WHERE key = \'last_version_check\';'); Static_Cache::remove('upgrade'); | < < < > > > > | | 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 | $db->setVersion(garradin_version()); // reset last version check $db->exec('UPDATE config SET value = NULL WHERE key = \'last_version_check\';'); Static_Cache::remove('upgrade'); Plugin::upgradeAllIfRequired(); } catch (\Exception $e) { if ($db->inTransaction()) { $db->rollback(); } $s = new Sauvegarde; $s->restoreFromLocal($backup_name); $s->remove($backup_name); Static_Cache::remove('upgrade'); throw $e; } $session = Session::getInstance(); $user_is_logged = $session->isLogged(true); // Forcer à rafraîchir les données de la session si elle existe if ($user_is_logged && !headers_sent()) { $session->refresh(); } } /** * Move data from root to data/ subdirectory |
︙ | ︙ | |||
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 | foreach ($files as $file) { rename($file, ROOT . '/data/' . basename($file)); } } static public function getLatestVersion(): ?\stdClass { $config = Config::getInstance(); $last = $config->get('last_version_check'); if ($last) { $last = json_decode($last); } // Only check once every two weeks if ($last && $last->time > (time() - 3600 * 24 * 5)) { return $last; } $current_version = garradin_version(); $last = (object) ['time' => time(), 'version' => null]; $config->set('last_version_check', json_encode($last)); $config->save(); $last->version = self::getInstaller()->latest(); | > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | foreach ($files as $file) { rename($file, ROOT . '/data/' . basename($file)); } } static public function getLatestVersion(): ?\stdClass { if (!ENABLE_TECH_DETAILS && !ENABLE_UPGRADES) { return null; } $config = Config::getInstance(); $last = $config->get('last_version_check'); if ($last) { $last = json_decode($last); } // Only check once every two weeks if ($last && $last->time > (time() - 3600 * 24 * 5)) { return $last; } return null; } static public function fetchLatestVersion(): ?\stdClass { if (!ENABLE_TECH_DETAILS && !ENABLE_UPGRADES) { return null; } $config = Config::getInstance(); $last = $config->get('last_version_check'); if ($last) { $last = json_decode($last); } // Only check once every two weeks if ($last && $last->time > (time() - 3600 * 24 * 2)) { return $last; } $current_version = garradin_version(); $last = (object) ['time' => time(), 'version' => null]; $config->set('last_version_check', json_encode($last)); $config->save(); $last->version = self::getInstaller()->latest(); |
︙ | ︙ |
Added src/include/lib/Garradin/UserException.php version [41a377eb20].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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; /* * Gestion des erreurs et exceptions */ class UserException extends \LogicException { protected $details = null; protected ?string $html_message = null; public function setMessage(string $message) { $this->message = $message; } public function getHTMLMessage(): ?string { return $this->html_message; } public function setHTMLMessage(string $html): void { $this->html_message = $html; } public function setDetails($details) { $this->details = $details; } public function getDetails() { return $this->details; } public function hasDetails(): bool { return $this->details !== null; } public function getDetailsHTML() { if (func_num_args() == 1) { $details = func_get_arg(0); } else { $details = $this->details; } if (null === $details) { return '<em>(nul)</em>'; } if ($details instanceof \DateTimeInterface) { return $details->format('d/m/Y'); } if (!is_array($details)) { return nl2br(htmlspecialchars($details)); } $out = '<table>'; foreach ($details as $key => $value) { $out .= sprintf('<tr><th>%s</th><td>%s</td></tr>', htmlspecialchars($key), $this->getDetailsHTML($value)); } $out .= '</table>'; return $out; } } |
Modified src/include/lib/Garradin/UserTemplate/CommonModifiers.php from [ec639e95ae] to [296d96b3c2].
︙ | ︙ | |||
31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 'input', 'button', 'link', 'icon', 'linkbutton', ]; static public function money($number, bool $hide_empty = true, bool $force_sign = false): string { if ($hide_empty && !$number) { return ''; } $sign = ($force_sign && $number > 0) ? '+' : ''; | > > > | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 'input', 'button', 'link', 'icon', 'linkbutton', ]; /** * 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) ? '+' : ''; |
︙ | ︙ | |||
101 102 103 104 105 106 107 | } return strftime($format, $ts->getTimestamp()); } static public function date($ts, string $format = null, string $locale = 'fr'): ?string { | | | | < | | 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | } return strftime($format, $ts->getTimestamp()); } static public function date($ts, string $format = null, string $locale = 'fr'): ?string { if (null === $format) { $format = 'd/m/Y à H:i'; } elseif (preg_match('/^DATE_[\w\d]+$/', $format)) { $format = constant('DateTime::' . $format); } if ($locale == 'fr') { return Utils::date_fr($ts, $format); } $ts = Utils::get_datetime($ts); |
︙ | ︙ | |||
278 279 280 281 282 283 284 | if ($current < $total) { $out[] = ['id' => $current + 1, 'label' => 'Page suivante' . ' »', 'class' => 'next', 'accesskey' => 'z']; } return $out; } | < | | | 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 | if ($current < $total) { $out[] = ['id' => $current + 1, 'label' => 'Page suivante' . ' »', 'class' => 'next', 'accesskey' => 'z']; } return $out; } static public function input(array $params) { static $params_list = ['value', 'default', 'type', 'help', 'label', 'name', 'options', 'source', 'no_size_limit', 'copy']; // 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); $tparams['type'] = 'time'; $tparams['name'] = sprintf('%s_time', $name); unset($tparams['label']); $suffix = self::formInput($tparams); } $current_value = null; $current_value_from_user = false; if (isset($_POST[$name])) { $current_value = $_POST[$name]; |
︙ | ︙ | |||
382 383 384 385 386 387 388 | $attributes['disabled'] = 'disabled'; unset($attributes['required']); } else { unset($attributes['disabled']); } | > > > > > > > | < < < < | | | 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 | $attributes['disabled'] = 'disabled'; unset($attributes['required']); } else { unset($attributes['disabled']); } if (!empty($attributes['readonly'])) { $attributes['readonly'] = 'readonly'; } else { unset($attributes['readonly']); } if (array_key_exists('required', $attributes)) { $required_label = ' <b title="Champ obligatoire">(obligatoire)</b>'; } else { $required_label = ' <i>(facultatif)</i>'; } $attributes_string = $attributes; array_walk($attributes_string, function (&$v, $k) { $v = sprintf('%s="%s"', $k, $v); }); $attributes_string = implode(' ', $attributes_string); if ($type == 'radio-btn') { $radio = self::formInput(array_merge($params, ['type' => 'radio', 'label' => null, 'help' => null])); $out = sprintf('<dd class="radio-btn">%s <label for="f_%s_%s"><div><h3>%s</h3>%s</div></label> </dd>', $radio, htmlspecialchars($name), htmlspecialchars($value), htmlspecialchars($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($_value)); } $input .= '</select>'; |
︙ | ︙ | |||
433 434 435 436 437 438 439 | $input .= '</optgroup>'; } $input .= '</select>'; } elseif ($type == 'textarea') { | | < > > > > > > | | 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 | $input .= '</optgroup>'; } $input .= '</select>'; } elseif ($type == 'textarea') { $input = sprintf('<textarea %s>%s</textarea>', $attributes_string, htmlspecialchars((string)$current_value)); } elseif ($type == 'list') { $multiple = !empty($attributes['multiple']); $values = ''; $delete_btn = self::button(['shape' => 'delete']); if (null !== $current_value && is_iterable($current_value)) { foreach ($current_value as $v => $l) { $values .= sprintf('<span class="label"><input type="hidden" name="%s[%s]" value="%s" /> %3$s %s</span>', htmlspecialchars($name), htmlspecialchars($v), htmlspecialchars($l), $multiple ? $delete_btn : ''); } } $button = self::button([ 'shape' => $multiple ? 'plus' : 'menu', 'label' => $multiple ? 'Ajouter' : 'Sélectionner', 'required' => $attributes['required'] ?? null, 'value' => Utils::getLocalURL($attributes['target']), 'data-multiple' => $multiple ? '1' : '0', 'data-name' => $name, ]); $input = sprintf('<span id="%s_container" class="input-list">%s%s</span>', htmlspecialchars($attributes['id']), $button, $values); } elseif ($type == 'money') { if (null !== $current_value && !$current_value_from_user) { $current_value = Utils::money_format($current_value, ',', ''); } if ((string) $current_value === '0') { $current_value = ''; } $currency = Config::getInstance()->get('monnaie'); $input = sprintf('<nobr><input type="text" pattern="-?[0-9]*([.,][0-9]{1,2})?" inputmode="decimal" size="8" class="money" %s value="%s" /><b>%s</b></nobr>', $attributes_string, htmlspecialchars($current_value), $currency); } else { $value = isset($attributes['value']) ? '' : sprintf(' value="%s"', htmlspecialchars($current_value)); $input = sprintf('<input type="%s" %s %s />', $type, $attributes_string, $value); } // No label? then we only want the input without the widget |
︙ | ︙ | |||
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 | if (isset($help)) { $out .= sprintf(' <em class="help">(%s)</em>', htmlspecialchars($help)); } $out .= '</dd>'; } else { $out = sprintf('<dt>%s%s</dt><dd>%s</dd>', $label, $required_label, $input); if ($type == 'file' && empty($params['no_size_limit'])) { $out .= sprintf('<dd class="help"><small>Taille maximale : %s</small></dd>', Utils::format_bytes(Utils::getMaxUploadSize())); } if (isset($help)) { $out .= sprintf('<dd class="help">%s</dd>', htmlspecialchars($help)); } } return $out; } | > > > > > | > | > | | 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 | if (isset($help)) { $out .= sprintf(' <em class="help">(%s)</em>', htmlspecialchars($help)); } $out .= '</dd>'; } else { if (!empty($copy)) { $input .= sprintf('<input type="button" onclick="var a = $(\'#f_%s\'); a.focus(); a.select(); document.execCommand(\'copy\'); this.value = \'Copié !\'; this.focus(); return false;" onblur="this.value = \'Copier\';" value="Copier" />', $params['name']); } $out = sprintf('<dt>%s%s</dt><dd>%s</dd>', $label, $required_label, $input); if ($type == 'file' && empty($params['no_size_limit'])) { $out .= sprintf('<dd class="help"><small>Taille maximale : %s</small></dd>', Utils::format_bytes(Utils::getMaxUploadSize())); } if (isset($help)) { $out .= sprintf('<dd class="help">%s</dd>', htmlspecialchars($help)); } } return $out; } static public function icon(array $params): string { if (isset($params['html']) && $params['html'] == false) { return Utils::iconUnicode($params['shape']); } $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('<b class="icn" %s>%s</b>', $attributes, Utils::iconUnicode($params['shape'])); |
︙ | ︙ | |||
572 573 574 575 576 577 578 579 580 581 582 583 584 585 | if (isset($params['name']) && !isset($params['value'])) { $params['value'] = 1; } $params['class'] .= ' icn-btn'; array_walk($params, function (&$v, $k) { $v = sprintf('%s="%s"', $k, htmlspecialchars($v)); }); $params = implode(' ', $params); return sprintf('<button %s data-icon="%s">%s</button>', $params, $icon, $label); | > > > | 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 | 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 data-icon="%s">%s</button>', $params, $icon, $label); |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Functions.php from [3358be7df6] to [afbcd93b42].
︙ | ︙ | |||
26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 'dump', 'error', 'read', 'save', 'admin_header', 'admin_footer', 'signature_url', ]; static public function admin_header(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); return $tpl->fetch('admin/_head.tpl'); | > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 'dump', 'error', 'read', 'save', 'admin_header', 'admin_footer', 'signature_url', 'mail', ]; static public function admin_header(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); return $tpl->fetch('admin/_head.tpl'); |
︙ | ︙ | |||
120 121 122 123 124 125 126 127 128 129 130 131 132 133 | } $params = json_encode($params); $db = DB::getInstance(); $db->preparedQuery('REPLACE INTO documents_data (document, key, value) VALUES (?, ?, ?);', $id, $key, $params); } static public function dump(array $params, Brindille $tpl) { if (!count($params)) { $params = $tpl->getAllVariables(); } | > > > > > > > > > > > > > > > > > | 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 | } $params = json_encode($params); $db = DB::getInstance(); $db->preparedQuery('REPLACE INTO documents_data (document, key, value) VALUES (?, ?, ?);', $id, $key, $params); } 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)); } Utils::sendEmail(Utils::EMAIL_CONTEXT_PRIVATE, $params['to'], $params['subject'], $params['body']); } static public function dump(array $params, Brindille $tpl) { if (!count($params)) { $params = $tpl->getAllVariables(); } |
︙ | ︙ | |||
212 213 214 215 216 217 218 | $params['included_from'] = array_merge($from, [$path]); $include->assignArray($params); $include->display(); } | | > > > > | 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | $params['included_from'] = array_merge($from, [$path]); $include->assignArray($params); $include->display(); } static public function http(array $params, UserTemplate $tpl): void { if (headers_sent()) { return; } if (isset($params['redirect'])) { Utils::redirect($params['redirect']); } if (isset($params['code'])) { static $codes = [ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', |
︙ | ︙ | |||
283 284 285 286 287 288 289 | if (!isset($codes[$params['code']])) { throw new Brindille_Exception('Code HTTP inconnu'); } header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true); } | < < | | > | > | | 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | if (!isset($codes[$params['code']])) { throw new Brindille_Exception('Code HTTP inconnu'); } header(sprintf('HTTP/1.1 %d %s', $params['code'], $codes[$params['code']]), true); } if (isset($params['type'])) { header('Content-Type: ' . $params['type'], true); $tpl->setContentType($params['type']); } if (isset($params['download'])) { header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($params['download'])), true); } } } |
Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [bfad13e405] to [a7ff55e10d].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin\UserTemplate; use Garradin\Utils; use KD2\Brindille_Exception; class Modifiers { const PHP_MODIFIERS_LIST = [ 'strtolower', | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin\UserTemplate; use Garradin\Utils; use Garradin\Entities\Users\Email; use KD2\SMTP; use KD2\Brindille_Exception; class Modifiers { const PHP_MODIFIERS_LIST = [ 'strtolower', |
︙ | ︙ | |||
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 | 'wordwrap', 'strip_tags', 'strlen', 'boolval', 'intval', 'floatval', 'substr', ]; const MODIFIERS_LIST = [ 'truncate', 'excerpt', 'protect_contact', 'atom_date', 'xml_escape', 'replace', 'regexp_replace', 'remove_leading_number', 'get_leading_number', 'spell_out_number', 'parse_date', 'math', 'money_int' => [Utils::class, 'moneyToInteger'], ]; const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/'; static public function replace($str, $find, $replace): string { return str_replace($find, $replace, $str); } static public function regexp_replace($str, $pattern, $replace) { return preg_replace($pattern, $replace, $str); } /** * UTF-8 aware intelligent substr * @param string $str UTF-8 string * @param integer $length Maximum string length * @param string $placeholder Placeholder text to append at the string if it has been cut * @param boolean $strict_cut If true then will cut in the middle of words | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | 'wordwrap', 'strip_tags', 'strlen', 'boolval', 'intval', 'floatval', 'substr', 'abs', ]; const MODIFIERS_LIST = [ 'truncate', 'excerpt', 'protect_contact', 'atom_date', 'xml_escape', 'replace', 'regexp_replace', 'regexp_match', 'match', 'remove_leading_number', 'get_leading_number', 'spell_out_number', 'parse_date', 'math', 'money_int' => [Utils::class, 'moneyToInteger'], 'check_email', ]; const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/'; static public function replace($str, $find, $replace): string { return str_replace($find, $replace, $str); } static public function regexp_replace($str, $pattern, $replace) { return preg_replace($pattern, $replace, $str); } static public function regexp_match($str, $pattern) { return (int) preg_match($pattern, $str); } static public function match($str, $pattern) { return (int) (stripos($str, $pattern) !== false); } static public function check_email($str) { if (!trim((string)$str)) { return false; } try { Email::validateAddress((string)$str); } catch (UserException $e) { return false; } return true; } /** * UTF-8 aware intelligent substr * @param string $str UTF-8 string * @param integer $length Maximum string length * @param string $placeholder Placeholder text to append at the string if it has been cut * @param boolean $strict_cut If true then will cut in the middle of words |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Sections.php from [7814929b5b] to [ff6689c43c].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin\UserTemplate; use KD2\Brindille_Exception; use Garradin\Config; use Garradin\DB; use Garradin\Utils; use Garradin\Entities\Web\Page; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use const Garradin\WWW_URL; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin\UserTemplate; use KD2\Brindille_Exception; use Garradin\Config; use Garradin\DB; use Garradin\Utils; use Garradin\Membres\Session; use Garradin\Entities\Web\Page; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; use const Garradin\WWW_URL; |
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 'documents', 'files', 'users', 'transactions', 'transaction_users', 'accounts_sums', 'sql', ]; static protected $_cache = []; static protected function cache(string $id, callable $callback) { if (!array_key_exists($id, self::$_cache)) { | > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 'documents', 'files', 'users', 'transactions', 'transaction_users', 'accounts_sums', 'sql', 'restrict', ]; static protected $_cache = []; static protected function cache(string $id, callable $callback) { if (!array_key_exists($id, self::$_cache)) { |
︙ | ︙ | |||
192 193 194 195 196 197 198 199 200 201 202 203 204 205 | $params['select'] = sprintf('tu.*, u.%s AS name, u.*', $config->champ_identite); $params['tables'] = 'acc_transactions_users tu INNER JOIN membres u ON u.id = tu.id_user'; return self::sql($params, $tpl, $line); } static public function breadcrumbs(array $params, UserTemplate $tpl, int $line): \Generator { if (!isset($params['path'])) { throw new Brindille_Exception('"path" parameter is mandatory and is missing'); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | $params['select'] = sprintf('tu.*, u.%s AS name, u.*', $config->champ_identite); $params['tables'] = 'acc_transactions_users tu INNER JOIN membres u ON u.id = tu.id_user'; return self::sql($params, $tpl, $line); } static public function restrict(array $params, UserTemplate $tpl, int $line): ?\Generator { $session = Session::getInstance(); if (!$session->isLogged()) { return null; } if (empty($params['level']) && empty($params['section'])) { yield []; return null; } $convert = [ 'read' => $session::ACCESS_READ, 'write' => $session::ACCESS_WRITE, 'admin' => $session::ACCESS_ADMIN, ]; if (empty($params['level']) || !isset($convert[$params['level']])) { throw new Brindille_Exception(sprintf("Ligne %d: 'restrict' niveau d'accès inconnu : %s", $line, $params['level'] ?? '')); } $ok = $session->canAccess($params['section'] ?? '', $convert[$params['level']]); if ($ok) { yield []; } return null; } static public function breadcrumbs(array $params, UserTemplate $tpl, int $line): \Generator { if (!isset($params['path'])) { throw new Brindille_Exception('"path" parameter is mandatory and is missing'); } |
︙ | ︙ | |||
251 252 253 254 255 256 257 | $params['select'] = 'w.*'; $params['tables'] = 'web_pages w'; $params['where'] .= ' AND status = :status'; $params[':status'] = Page::STATUS_ONLINE; if (array_key_exists('search', $params)) { | | | 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | $params['select'] = 'w.*'; $params['tables'] = 'web_pages w'; $params['where'] .= ' AND status = :status'; $params[':status'] = Page::STATUS_ONLINE; if (array_key_exists('search', $params)) { if (trim((string) $params['search']) === '') { return; } $params[':search'] = substr(trim($params['search']), 0, 100); unset($params['search']); $params['tables'] .= ' INNER JOIN files_search ON files_search.path = w.file_path'; |
︙ | ︙ | |||
282 283 284 285 286 287 288 | $params['limit'] = 1; $params[':path'] = $params['path']; unset($params['path']); } if (array_key_exists('parent', $params)) { $params['where'] .= ' AND w.parent = :parent'; | | | 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | $params['limit'] = 1; $params[':path'] = $params['path']; unset($params['path']); } if (array_key_exists('parent', $params)) { $params['where'] .= ' AND w.parent = :parent'; $params[':parent'] = trim((string) $params['parent']); unset($params['parent']); } if (isset($params['future'])) { $params['where'] .= sprintf(' AND w.published %s datetime(\'now\', \'localtime\')', $params['future'] ? '>' : '<='); unset($params['future']); |
︙ | ︙ | |||
355 356 357 358 359 360 361 | if (!$page) { return null; } // Store attachments in temp table $db = DB::getInstance(); $db->begin(); | | | | | 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | if (!$page) { return null; } // Store attachments in temp table $db = DB::getInstance(); $db->begin(); $db->exec('CREATE TEMP TABLE IF NOT EXISTS web_pages_attachments (page_id, uri, path, name, modified, image);'); $page_file_name = Utils::basename($page->file_path); foreach ($page->listAttachments() as $file) { if ($file->name == $page_file_name || $file->type != File::TYPE_FILE) { continue; } $db->preparedQuery('INSERT OR REPLACE INTO web_pages_attachments VALUES (?, ?, ?, ?, ?, ?);', $page->id(), $file->uri(), $file->path, $file->name, $file->modified, $file->image); } $db->commit(); return $page; }); |
︙ | ︙ | |||
391 392 393 394 395 396 397 | // Don't regenerate that table for each section called in the page, // we assume the content and list of files will not change between sections self::cache('page_files_text_' . $parent, function () use ($page) { $db = DB::getInstance(); $db->begin(); // Put files mentioned in the text in a temporary table | | | | | | | 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 | // Don't regenerate that table for each section called in the page, // we assume the content and list of files will not change between sections self::cache('page_files_text_' . $parent, function () use ($page) { $db = DB::getInstance(); $db->begin(); // Put files mentioned in the text in a temporary table $db->exec('CREATE TEMP TABLE IF NOT EXISTS files_tmp_in_text (page_id, uri);'); foreach ($page->listTaggedAttachments() as $uri) { $db->insert('files_tmp_in_text', ['page_id' => $page->id(), 'uri' => $uri]); } $db->commit(); }); $params['where'] .= sprintf(' AND uri NOT IN (SELECT uri FROM files_tmp_in_text WHERE page_id = %d)', $page->id()); } if (empty($params['order'])) { $params['order'] = 'name'; } if ($params['order'] == 'name') { $params['order'] .= ' COLLATE U_NOCASE'; } foreach (self::sql($params, $tpl, $line) as $row) { $file = Files::get($row['path']); if (null === $file) { continue; |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/UserTemplate.php from [c6a815babb] to [388344ba13].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use Garradin\Config; use Garradin\Plugin; use Garradin\Utils; use Garradin\Membres\Session; use Garradin\Web\Skeleton; use Garradin\Entities\Files\File; use Garradin\Files\Files; use Garradin\UserTemplate\Modifiers; use Garradin\UserTemplate\Functions; use Garradin\UserTemplate\Sections; use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT, ROOT}; class UserTemplate extends \KD2\Brindille { public $_tpl_path; protected $modified; | > | | > > > > > < < < < < < < < < < < < < < < < < < < > > > | > | < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\Translate; use Garradin\Config; use Garradin\Plugin; use Garradin\Utils; use Garradin\Membres\Session; use Garradin\Web\Skeleton; use Garradin\Entities\Files\File; use Garradin\Files\Files; use Garradin\UserTemplate\Modifiers; use Garradin\UserTemplate\Functions; use Garradin\UserTemplate\Sections; use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT, ROOT}; class UserTemplate extends \KD2\Brindille { public $_tpl_path; protected $modified; protected $file = null; protected $code = null; protected $cache_path = USER_TEMPLATES_CACHE_ROOT; protected $escape_default = 'html'; static protected $root_variables; protected $content_type = null; static public function getRootVariables() { if (null !== self::$root_variables) { return self::$root_variables; } static $keys = ['adresse_asso', 'champ_identifiant', 'champ_identite', 'couleur1', 'couleur2', 'email_asso', 'monnaie', 'nom_asso', 'pays', 'site_asso', 'telephone_asso', 'files']; $config = Config::getInstance(); $files = $config::FILES; // Put URL in files array array_walk($files, function (&$v, $k) use ($config) { $v = $config->fileURL($k); }); $config = array_intersect_key($config->asArray(), array_flip($keys)); $config['files'] = $files; $session = Session::getInstance(); $is_logged = $session->isLogged(); self::$root_variables = [ 'root_url' => WWW_URL, 'request_url' => Utils::getRequestURI(), 'admin_url' => ADMIN_URL, '_GET' => &$_GET, '_POST' => &$_POST, 'visitor_lang' => Translate::getHttpLang(), 'config' => $config, 'now' => new \DateTime, 'is_logged' => $is_logged, 'logged_user' => $is_logged ? $session->getUser() : null, ]; return self::$root_variables; } public function __construct(string $path) { |
︙ | ︙ | |||
107 108 109 110 111 112 113 114 115 116 117 118 119 120 | $this->assignArray(self::getRootVariables()); $this->registerAll(); Plugin::fireSignal('usertemplate.init', ['template' => $this]); } public function registerAll() { // Register default Brindille modifiers $this->registerDefaults(); // Common modifiers | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | $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. * Useful for templates where you don't want the user to be able to do SQL queries etc. * * @param bool $enable * @return void */ public function toggleSafeMode(bool $safe_mode): void { if ($safe_mode) { $this->_functions = []; $this->_sections = []; // Register default Brindille modifiers $this->registerDefaults(); } else { $this->registerAll(); } } public function setEscapeDefault(?string $default): void { $this->escape_default = $default; if (null === $default) { $this->registerModifier('escape', fn($str) => $str); } else { $this->registerModifier('escape', fn ($str) => htmlspecialchars((string)$str) ); } } public function registerAll() { // Register default Brindille modifiers $this->registerDefaults(); // Common modifiers |
︙ | ︙ | |||
141 142 143 144 145 146 147 | $this->registerFunction($name, [Functions::class, $name]); } // Local sections foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } | | > > > | | > | > | > > > | > > | > > | > > > > > > > > > > > > > > > > | > > > > > > > | | > > > > > > > > > > > > > > | > > > > | | 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 | $this->registerFunction($name, [Functions::class, $name]); } // Local sections foreach (Sections::SECTIONS_LIST as $name) { $this->registerSection($name, [Sections::class, $name]); } $this->registerModifier('money', function ($number, bool $hide_empty = true, bool $force_sign = false): string { if ($hide_empty && !$number) { return ''; } $sign = ($force_sign && $number > 0) ? '+' : ''; $out = $sign . Utils::money_format($number, ',', '.', $hide_empty); if (!$this->escape_default) { return $out; } return sprintf('<b class="money">%s</b>', str_replace('.', ' ', $out)); }); $this->registerModifier('money_currency', function ($number, bool $hide_empty = true): string { $out = $this->_modifiers['money']($number, $hide_empty); if ($out !== '') { $out .= $this->escape_default == 'html' ? ' ' : ' '; $out .= Config::getInstance()->get('monnaie'); } return $out; }); } public function setSource(string $path) { $this->file = null; $this->path = $path; $this->modified = filemtime($path); // Use shared cache for default templates $this->cache_path = SHARED_USER_TEMPLATES_CACHE_ROOT; } public function setCode(string $code) { $this->code = $code; $this->file = null; $this->path = null; $this->modified = time(); // Use custom cache for user templates $this->cache_path = USER_TEMPLATES_CACHE_ROOT; } protected function _getCachePath() { $hash = sha1($this->file ? $this->file->path : ($this->code ?: $this->path)); return sprintf('%s/%s.php', $this->cache_path, $hash); } public function display(): void { $compiled_path = $this->_getCachePath(true); if (!is_dir(dirname($compiled_path))) { // Force cache directory mkdir Utils::safe_mkdir(dirname($compiled_path), 0777, true); } if (file_exists($compiled_path) && filemtime($compiled_path) >= $this->modified) { require $compiled_path; return; } $tmp_path = $compiled_path . '.tmp'; if ($this->code) { $source = $this->code; } elseif ($this->file) { $source = $this->file->fetch(); } else { $source = file_get_contents($this->path); } try { $code = $this->compile($source); file_put_contents($tmp_path, $code); require $tmp_path; } catch (Brindille_Exception $e) { throw new Brindille_Exception(sprintf("Erreur de syntaxe dans '%s' : %s", $this->file ? $this->file->name : ($this->code ? 'code' : Utils::basename($this->path)), $e->getMessage()), 0, $e); } catch (\Throwable $e) { // Don't delete temporary file as it can be used to debug throw $e; } |
︙ | ︙ | |||
217 218 219 220 221 222 223 | if ($filename) { header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($filename))); } header('Content-type: application/pdf'); Utils::streamPDF($html); } | | > > > > > > > > > > > > > > > > > > > > | 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 | if ($filename) { header(sprintf('Content-Disposition: attachment; filename="%s"', Utils::safeFileName($filename))); } header('Content-type: application/pdf'); Utils::streamPDF($html); } public function setContentType(string $type): void { $this->content_type = $type; } public function displayWeb(): void { $content = $this->fetch(); $type = $this->content_type ?: 'text/html'; header(sprintf('Content-Type: %s;charset=utf-8', $type), true); if ($type == 'application/pdf') { Utils::streamPDF($content); } else { echo $content; } } } |
Modified src/include/lib/Garradin/Users/Categories.php from [b3ce602bbd] to [9baeb0d492].
︙ | ︙ | |||
11 12 13 14 15 16 17 | static public function get(int $id): ?Category { return EM::findOneById(Category::class, $id); } static public function listSimple(): array { | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | static public function get(int $id): ?Category { return EM::findOneById(Category::class, $id); } static public function listSimple(): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s ORDER BY name COLLATE U_NOCASE;', Category::TABLE)); } static public function listWithStats(): array { return DB::getInstance()->getGrouped(sprintf('SELECT c.id, c.*, (SELECT COUNT(*) FROM membres WHERE id_category = c.id) AS count FROM %s c ORDER BY c.name COLLATE U_NOCASE;', Category::TABLE)); } static public function listHidden(): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s WHERE hidden = 1 ORDER BY name COLLATE U_NOCASE;', Category::TABLE)); } static public function listNotHidden(): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s WHERE hidden = 0 ORDER BY name COLLATE U_NOCASE;', Category::TABLE)); } } |
Added src/include/lib/Garradin/Users/Emails.php version [8018f4f5dd].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin\Users; use Garradin\Config; use Garradin\CSV; use Garradin\DB; use Garradin\DynamicList; use Garradin\Plugin; use Garradin\UserException; use Garradin\Entities\Users\Email; 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\Mail_Message; use KD2\DB\EntityManager as EM; class Emails { const RENDER_FORMATS = [ null => 'Texte brut', Render::FORMAT_SKRIV => 'SkrivML', Render::FORMAT_MARKDOWN => 'MarkDown', ]; /** * Email sending contexts */ const CONTEXT_BULK = 1; const CONTEXT_PRIVATE = 2; const CONTEXT_SYSTEM = 0; /** * When we reach that number of fails, the address is treated as permanently invalid, unless reset by a verification. */ const FAIL_LIMIT = 5; /** * Add a message to the sending queue using templates * @param int $context * @param array $recipients List of recipients, 'From' email address as the key, and an array as a value, that contains variables to be used in the email template * @param string $sender * @param string $subject * @param UserTemplate|string $content * @return void */ static public function queue(int $context, array $recipients, ?string $sender, string $subject, $content, ?string $render = null): void { // Remove duplicates due to case changes $recipients = array_change_key_case($recipients, CASE_LOWER); 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; if ($template) { $template->toggleSafeMode(true); } $db = DB::getInstance(); $db->begin(); $st = $db->prepare('INSERT INTO emails_queue (sender, subject, recipient, recipient_hash, content, content_html, context) VALUES (:sender, :subject, :recipient, :recipient_hash, :content, :content_html, :context);'); if ($render) { $skel = new Skeleton('email.html'); } foreach ($recipients as $to => $variables) { // Ignore invalid addresses if (!preg_match('/.+@.+\..+$/', $to)) { continue; } // We won't try to reject invalid/optout recipients here, // it's done in the queue clearing (more efficient) $hash = Email::getHash($to); $content_html = null; if ($template) { $template->assignArray((array) $variables); // Disable HTML escaping for plaintext emails $template->setEscapeDefault(null); $content = $template->fetch(); if ($render) { $content_html = $template->fetch(); } } if ($render) { $content_html = Render::render($render, null, $content_html ?? $content); } if ($content_html) { // Wrap HTML content in the email skeleton $content_html = $skel->fetch([ 'html' => $content_html, 'recipient' => $to, 'data' => $variables, 'context' => $context, 'from' => $sender, ]); } if (Plugin::fireSignal('email.queue.insert', compact('context', 'to', 'sender', 'subject', 'content', 'render', 'hash', 'content_html'))) { // 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_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(); } // Always send system emails right away elseif ($context == self::CONTEXT_SYSTEM) { self::runQueue(self::CONTEXT_SYSTEM); } } /** * Return an Email entity from the optout code */ static public function getEmailFromOptout(string $code): ?Email { $hash = base64_decode(str_pad(strtr($code, '-_', '+/'), strlen($code) % 4, '=', STR_PAD_RIGHT)); if (!$hash) { return null; } $hash = bin2hex($hash); return EM::findOne(Email::class, 'SELECT * FROM @TABLE WHERE hash = ?;', $hash); } /** * Sets the address as invalid (no email can be sent to this address ever) */ static public function markAddressAsInvalid(string $address): void { $e = self::getEmail($address); if (!$e) { return; } $e->set('invalid', true); $e->set('optout', false); $e->set('verified', false); $e->save(); } /** * Return an Email entity from an email address */ static public function getEmail(string $address): ?Email { return EM::findOne(Email::class, 'SELECT * FROM @TABLE WHERE hash = ?;', Email::getHash(strtolower($address))); } /** * Return or create a new email entity */ static public function getOrCreateEmail(string $address): Email { $address = strtolower($address); $e = self::getEmail($address); if (!$e) { $e = new Email; $e->added = new \DateTime; $e->hash = $e::getHash($address); $e->validate($address); $e->save(); } return $e; } /** * Run the queue of emails that are waiting to be sent */ static public function runQueue(?int $context = null): void { $db = DB::getInstance(); $queue = self::listQueueAndMarkAsSending($context); $ids = []; // listQueue nettoie déjà la queue foreach ($queue as $row) { // Don't send emails to opt-out address, unless it's a password reminder // Invalid and failed-too-many addresses are purged from the queue before processing, no need to handle them here // We still allow emails to be sent to failed or optout addresses if it's a system email if ($row->context != self::CONTEXT_SYSTEM && $row->optout) { self::deleteFromQueue($row->id); continue; } // Create email address in database if (!$row->email_hash) { $email = self::getOrCreateEmail($row->recipient); if (!$email->canSend()) { // Email address is invalid, skip self::deleteFromQueue($row->id); continue; } } $headers = [ 'From' => $row->sender, 'To' => $row->recipient, 'Subject' => $row->subject, ]; self::send($row->context, $row->recipient_hash, $headers, $row->content, $row->content_html); $ids[] = $row->id; } // Update emails list and send count // then delete messages from queue $db->exec(sprintf(' BEGIN; UPDATE emails_queue SET sending = 2 WHERE %s; INSERT OR IGNORE INTO %s (hash) SELECT recipient_hash FROM emails_queue WHERE sending = 2; UPDATE %2$s SET sent_count = sent_count + 1, last_sent = datetime() WHERE hash IN (SELECT recipient_hash FROM emails_queue WHERE sending = 2); DELETE FROM emails_queue WHERE sending = 2; END;', $db->where('id', $ids), Email::TABLE)); } /** * Lists the queue, marks listed elements as "sending" * @return array */ static protected function listQueueAndMarkAsSending(?int $context = null): array { $queue = self::listQueue($context); if (!count($queue)) { return $queue; } $ids = []; foreach ($queue as $row) { $ids[] = $row->id; } $db = DB::getInstance(); $db->update('emails_queue', ['sending' => 1, 'sending_started' => new \DateTime], $db->where('id', $ids)); return $queue; } /** * Returns the lits of emails waiting to be sent, except invalid ones and emails that haved failed too much * * DO NOT USE for sending, use listQueueAndMarkAsSending instead, or there might be multiple processes sending * the same email over and over. * * @param int|null $context Context to list, leave NULL to have all contexts * @return array */ static protected function listQueue(?int $context = null): array { // Clean-up the queue from reject emails self::purgeQueueFromRejected(); // Reset messages that failed during the queue run self::resetFailed(); $condition = null === $context ? '' : sprintf(' AND context = %d', $context); return DB::getInstance()->get(sprintf('SELECT q.*, e.optout, e.verified, e.hash AS email_hash FROM emails_queue q LEFT JOIN emails e ON e.hash = q.recipient_hash WHERE q.sending = 0 %s;', $condition)); } static public function countQueue(): int { return DB::getInstance()->count('emails_queue'); } /** * Supprime de la queue les messages liés à des adresses invalides * ou qui ne souhaitent plus recevoir de message * @return boolean */ static protected function purgeQueueFromRejected(): void { DB::getInstance()->delete('emails_queue', 'recipient_hash IN (SELECT hash FROM emails WHERE invalid = 1 OR fail_count >= ?)', self::FAIL_LIMIT); } /** * If emails have been marked as sending but sending failed, mark them for resend after a while */ static protected function resetFailed(): void { $sql = 'UPDATE emails_queue SET sending = 0, sending_started = NULL WHERE sending = 1 AND sending_started < datetime(\'now\', \'-3 hours\');'; DB::getInstance()->exec($sql); } /** * Supprime un message de la queue d'envoi * @param integer $id * @return boolean */ static protected function deleteFromQueue($id) { return DB::getInstance()->delete('emails_queue', 'id = ?', (int)$id); } static public function listRejectedUsers(): DynamicList { $db = DB::getInstance(); $columns = [ 'identity' => [ 'label' => 'Membre', 'select' => 'u.' . $db->quoteIdentifier(Config::getInstance()->champ_identite), ], 'email' => [ 'label' => 'Adresse', 'select' => 'u.email', ], 'user_id' => [ 'select' => 'u.id', ], 'hash' => [ ], 'status' => [ 'label' => 'Statut', 'select' => sprintf('CASE WHEN e.optout = 1 THEN \'Désinscription\' WHEN e.invalid = 1 THEN \'Invalide\' WHEN e.fail_count >= %d THEN \'Trop d\'\'erreurs\' WHEN e.verified = 1 THEN \'Vérifiée\' ELSE \'\' END', self::FAIL_LIMIT), ], 'sent_count' => [ 'label' => 'Messages envoyés', ], 'fail_log' => [ 'label' => 'Journal d\'erreurs', ], 'last_sent' => [ 'label' => 'Dernière tentative d\'envoi', ], 'optout' => [], 'fail_count' => [], ]; $tables = 'emails e INNER JOIN membres u ON u.email IS NOT NULL AND u.email != \'\' AND e.hash = email_hash(u.email)'; $conditions = sprintf('e.optout = 1 OR e.invalid = 1 OR e.fail_count >= %d', self::FAIL_LIMIT); $list = new DynamicList($columns, $tables, $conditions); $list->orderBy('last_sent', true); return $list; } static protected function send(int $context, string $recipient_hash, array $headers, string $content, ?string $content_html): void { $message = new Mail_Message; $message->setHeaders($headers); if (!$message->getFrom()) { $message->setHeader('From', self::getFromHeader()); } $message->setMessageId(); // Append unsubscribe, except for password reminders if ($context != self::CONTEXT_SYSTEM) { $url = Email::getOptoutURL($recipient_hash); // RFC 8058 $message->setHeader('List-Unsubscribe', sprintf('<%s>', $url)); $message->setHeader('List-Unsubscribe-Post', 'Unsubscribe=Yes'); $optout_text = "Vous recevez ce message car vous êtes dans nos contacts.\n" . "Pour ne plus jamais recevoir de message de notre part cliquez ici :\n"; $content .= "\n\n-- \n" . $optout_text . $url; if (null !== $content_html) { $optout_text = '<hr style="border-top: 2px solid #999; background: none;" /><p style="color: #000; background: #fff; padding: 10px; text-align: center; font-size: 9pt">' . nl2br(htmlspecialchars($optout_text)); $optout_text.= sprintf('<br /><a href="%s" style="color: blue; text-decoration: underline; padding: 5px; border-radius: 5px; background: #ddd;">Me désinscrire</a></p>', $url); if (stripos($content_html, '</body>') !== false) { $content_html = str_ireplace('</body>', $optout_text . '</body>', $content_html); } else { $content_html .= $optout_text; } } } $message->setBody($content); if (null !== $content_html) { $message->addPart('text/html', $content_html); } $config = Config::getInstance(); $message->setHeader('Return-Path', MAIL_RETURN_PATH ?? $config->email_asso); $message->setHeader('X-Auto-Response-Suppress', 'All'); // This is to avoid getting auto-replies from Exchange servers self::sendMessage($context, $message); } static public function sendMessage(int $context, Mail_Message $message) { $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 (!$return) { return null; } if ($return['type'] == 'autoreply') { // Ignore auto-responders return $return; } elseif ($return['type'] == 'genuine') { // Forward emails that are not automatic to the organization email $config = Config::getInstance(); $new = new Mail_Message; $new->setHeaders([ 'To' => $config->email_asso, 'Subject' => 'Réponse à un message que vous avez envoyé', ]); $new->setBody('Veuillez trouver ci-joint une réponse à un message que vous avez envoyé à un de vos membre.'); $new->attachMessage($message->output()); self::sendMessage(self::CONTEXT_SYSTEM, $new); return $return; } return self::handleManualBounce($return['recipient'], $return['type'], $return['message']); } static public function handleManualBounce(string $recipient, string $type, ?string $message): ?array { $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; } /** * Create a mass mailing */ static public function createMailing(array $recipients, string $subject, string $message, bool $send_copy, ?string $render): \stdClass { $list = []; foreach ($recipients as $recipient) { if (empty($recipient->email)) { continue; } $list[$recipient->email] = $recipient; } if (!count($list)) { throw new UserException('Aucun destinataire de la liste ne possède d\'adresse email.'); } $html = null; $tpl = null; $random = array_rand($list); if (false !== strpos($message, '{{')) { $tpl = new UserTemplate; $tpl->setCode($message); $tpl->toggleSafeMode(true); $tpl->assignArray((array)$list[$random]); $tpl->setEscapeDefault(null); try { if (!$render) { // Disable HTML escaping for plaintext emails $message = $tpl->fetch(); } else { $html = $tpl->fetch(); } } catch (\KD2\Brindille_Exception $e) { throw new UserException('Erreur de syntaxe dans le corps du message :' . PHP_EOL . $e->getPrevious()->getMessage(), 0, $e); } } if ($render) { $html = Render::render($render, null, $html ?? $message); } elseif (null !== $html) { $html = '<pre>' . $html . '</pre>'; } else { $html = '<pre>' . htmlspecialchars(wordwrap($message)) . '</pre>'; } $recipients = $list; $config = Config::getInstance(); $sender = sprintf('"%s" <%s>', $config->nom_asso, $config->email_asso); $message = (object) compact('recipients', 'subject', 'message', 'sender', 'tpl', 'send_copy', 'render'); $message->preview = (object) [ 'to' => $random, // Not required to be a valid From header, this is just a preview 'from' => $sender, 'subject' => $subject, 'html' => $html, ]; return $message; } static public function getFromHeader(string $name = null, string $email = null): string { $config = Config::getInstance(); if (null === $name) { $name = $config->nom_asso; } if (null === $email) { $email = $config->email_asso; } $name = str_replace('"', '\\"', $name); $name = str_replace(',', '', $name); // Remove commas return sprintf('"%s" <%s>', $name, $email); } /** * Send a mass mailing */ static public function sendMailing(\stdClass $mailing): void { if (!isset($mailing->recipients, $mailing->subject, $mailing->message, $mailing->send_copy)) { throw new \InvalidArgumentException('Invalid $mailing object'); } if (!count($mailing->recipients)) { throw new UserException('Aucun destinataire de la liste ne possède d\'adresse email.'); } Emails::queue(Emails::CONTEXT_BULK, $mailing->recipients, null, // Default sender $mailing->subject, $mailing->tpl ?? $mailing->message, $mailing->render ?? null ); if ($mailing->send_copy) { $config = Config::getInstance(); Emails::queue(Emails::CONTEXT_BULK, [$config->get('email_asso') => null], null, $mailing->subject, $mailing->message); } } static public function exportMailing(string $format, \stdClass $mailing): void { $rows = $mailing->recipients; $id_field = Config::getInstance()->get('champ_identite'); foreach ($rows as $key => &$row) { $row = [$key, $row->$id_field ?? '']; } unset($row); CSV::export($format, 'Destinataires message collectif', $rows, ['Adresse e-mail', 'Identité']); } } |
Modified src/include/lib/Garradin/Utils.php from [1fb8515d7b] to [2fbb9eecb4].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use KD2\Security; use KD2\Form; use KD2\HTTP; use KD2\Translate; use KD2\SMTP; class Utils { | < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?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' => '↑', 'down' => '↓', 'export' => '↷', 'reset' => '↺', 'upload' => '⇑', 'download' => '⇓', 'home' => '⌂', 'print' => '⎙', 'star' => '★', 'check' => '☑', 'settings' => '☸', 'alert' => '⚠', 'mail' => '✉', 'edit' => '✎', 'delete' => '✘', 'help' => '❓', 'plus' => '➕', 'minus' => '➖', 'logout' => '⤝', 'eye-off' => '⤫', 'menu' => '𝍢', 'eye' => '👁', 'user' => '👤', 'users' => '👪', 'calendar' => '📅', 'attach' => '📎', 'search' => '🔍', 'lock' => '🔒', 'unlock' => '🔓', 'folder' => '🗀', 'document' => '🗅', 'bold' => 'B', 'italic' => 'I', 'header' => 'H', 'paragraph' => '§', 'list-ol' => '1', 'list-ul' => '•', 'table' => '◫', 'radio-unchecked' => '◯', 'uncheck' => '☐', 'radio-checked' => '⬤', 'image' => '🖻', 'left' => '←', 'right' => '→', 'column' => '▚', 'del-column' => '🮔', 'reload' => '🗘', 'gallery' => '🖼', 'code' => '<', 'markdown' => 'M', 'skriv' => 'S', 'globe' => '🌍', 'video' => '▶', 'quote' => '«', 'money' => '€', ]; const FRENCH_DATE_NAMES = [ 'January' => 'janvier', 'February' => 'février', 'March' => 'mars', 'April' => 'avril', 'May' => 'mai', |
︙ | ︙ | |||
89 90 91 92 93 94 95 | { $ts = self::get_datetime($ts); if (null === $ts) { return $ts; } | | < < | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | { $ts = self::get_datetime($ts); if (null === $ts) { return $ts; } $date = Translate::strftime($format, $ts, 'fr_FR'); return $date; } static public function date_fr($ts, $format = null) { $ts = self::get_datetime($ts); |
︙ | ︙ | |||
157 158 159 160 161 162 163 | static public function moneyToInteger($value) { if (trim($value) === '') { return 0; } | | | | 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | static public function moneyToInteger($value) { if (trim($value) === '') { return 0; } if (!preg_match('/^(-?)(\d+)(?:[,.](\d{1,2}))?$/', $value, $match)) { throw new UserException(sprintf('Le montant est invalide : %s. Exemple de format accepté : 142,02', $value)); } $value = $match[1] . $match[2] . str_pad($match[3] ?? '', 2, '0', STR_PAD_RIGHT); $value = (int) $value; return $value; } static public function money_format($number, string $dec_point = ',', string $thousands_sep = ' ', $zero_if_empty = true): string { if ($number == 0) { return $zero_if_empty ? '0' : '0,00'; |
︙ | ︙ | |||
337 338 339 340 341 342 343 344 345 346 347 348 349 350 | static public function getCountryList() { return Translate::getCountriesList('fr'); } static public function getCountryName($code) { $list = self::getCountryList(); if (!isset($list[$code])) return false; return $list[$code]; } | > > | 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 | static public function getCountryList() { return Translate::getCountriesList('fr'); } static public function getCountryName($code) { $code = strtoupper($code); $list = self::getCountryList(); if (!isset($list[$code])) return false; return $list[$code]; } |
︙ | ︙ | |||
637 638 639 640 641 642 643 | if (!empty($params['file'])) $url .= $params['file']; if (!empty($params['query'])) { $url .= '?'; | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > > | 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 | if (!empty($params['file'])) $url .= $params['file']; if (!empty($params['query'])) { $url .= '?'; if (!(is_numeric($params['query']) && (int)$params['query'] === 1) && $params['query'] !== true) $url .= $params['query']; } return $url; } static public function iconUnicode(string $shape): string { if (!isset(self::ICONS[$shape])) { throw new \InvalidArgumentException('Unknown icon shape: ' . $shape); } return self::ICONS[$shape]; } static public function array_transpose(array $array): array { $out = []; $max = 0; |
︙ | ︙ | |||
864 865 866 867 868 869 870 | } $h /= 6; } return array($h * 360, $s, $l); } | | | | < < | > > > | | > | | < > > > | > | 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 | } $h /= 6; } return array($h * 360, $s, $l); } static public function HTTPCache(?string $hash, ?int $last_change, int $max_age = 3600): bool { $etag = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH'], '"\' ') : null; $last_modified = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : null; $etag = $etag ? str_replace('-gzip', '', $etag) : null; header(sprintf('Cache-Control: private, max-age=%d', $max_age), true); header_remove('Expires'); if ($last_change) { header(sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $last_change)), true); } if ($hash) { header(sprintf('Etag: "%s"', $hash), true); } if (($etag && $etag === $hash) || ($last_modified && $last_modified >= $last_change)) { http_response_code(304); exit; } return false; } static public function transformTitleToURI($str) { $str = Utils::transliterateToAscii($str); |
︙ | ︙ | |||
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 | self::$collator = \Collator::create('fr_FR'); // This is what makes the comparison case insensitive // https://www.php.net/manual/en/collator.setstrength.php self::$collator->setAttribute(\Collator::STRENGTH, \Collator::SECONDARY); // Don't use \Collator::NUMERIC_COLLATION here as it goes against what would feel logic // with NUMERIC_COLLATION: 1, 2, 10, 11, 101 // without: 1, 10, 101, 11, 2 } if (isset(self::$collator)) { return (int) self::$collator->compare($a, $b); } $a = strtoupper(self::transliterateToAscii($a)); $b = strtoupper(self::transliterateToAscii($b)); return strcmp($a, $b); } /** * Transforms a unicode string to lowercase AND removes all diacritics * * @see https://www.matthecat.com/supprimer-les-accents-d-une-chaine-avec-php.html */ static public function unicodeCaseFold(?string $str): string { if (null === $str || trim($str) === '') { return ''; } if (!isset(self::$transliterator) && function_exists('transliterator_create')) { self::$transliterator = \Transliterator::create('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; Lower();'); } if (isset(self::$transliterator)) { return self::$transliterator->transliterate($str); | > > > > > > > > > > > > > > > > > > > | 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 | self::$collator = \Collator::create('fr_FR'); // This is what makes the comparison case insensitive // https://www.php.net/manual/en/collator.setstrength.php self::$collator->setAttribute(\Collator::STRENGTH, \Collator::SECONDARY); // Don't use \Collator::NUMERIC_COLLATION here as it goes against what would feel logic // for account ordering // with NUMERIC_COLLATION: 1, 2, 10, 11, 101 // without: 1, 10, 101, 11, 2 } // Make sure we have UTF-8 // If we don't, we may end up with malformed database, eg. "row X missing from index" errors // when doing an integrity check $a = self::utf8_encode($a); $b = self::utf8_encode($b); if (isset(self::$collator)) { return (int) self::$collator->compare($a, $b); } $a = strtoupper(self::transliterateToAscii($a)); $b = strtoupper(self::transliterateToAscii($b)); return strcmp($a, $b); } static public function utf8_encode(?string $str): ?string { if (null === $str) { return null; } // Check if string is already UTF-8 encoded or not return !preg_match('//u', $str) ? utf8_encode($str) : $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 */ static public function unicodeCaseFold(?string $str): string { if (null === $str || trim($str) === '') { return ''; } $str = str_replace('’', '\'', $str); // Normalize French apostrophe if (!isset(self::$transliterator) && function_exists('transliterator_create')) { self::$transliterator = \Transliterator::create('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; Lower();'); } if (isset(self::$transliterator)) { return self::$transliterator->transliterate($str); |
︙ | ︙ | |||
1081 1082 1083 1084 1085 1086 1087 | 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': | | > > | > | | | 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 | 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 %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); } return $target; } /** * Integer to A-Z, AA-ZZ, AAA-ZZZ, etc. * @see https://www.php.net/manual/fr/function.base-convert.php#94874 */ static public function num2alpha(int $n): string { $r = ''; for ($i = 1; $n >= 0 && $i < 10; $i++) { $r = chr(0x41 + intval($n % pow(26, $i) / pow(26, $i - 1))) . $r; $n -= pow(26, $i); } return $r; } static public function uuid(): string { |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/AbstractRender.php from [5c2f7cfe96] to [a7823fc02a].
︙ | ︙ | |||
26 27 28 29 30 31 32 | } $this->user_prefix = $user_prefix; } abstract public function render(?string $content = null): string; | > > > > > | > < > | | | > > > > > | | 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 | } $this->user_prefix = $user_prefix; } abstract public function render(?string $content = null): string; public function registerAttachment(string $uri) { Render::registerAttachment($this->file, $uri); } protected function resolveAttachment(string $uri) { $prefix = $this->current_path; $pos = strpos($uri, '/'); if ($pos === 0) { // Absolute URL: treat it as absolute! $uri = ltrim($uri, '/'); } else { // Handle relative URIs $uri = $prefix . '/' . $uri; } $this->registerAttachment($uri); return WWW_URL . $uri; } protected function resolveLink(string $uri) { $first = substr($uri, 0, 1); if ($first == '/' || $first == '!') { return Utils::getLocalURL($uri); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/Blocks.php from [7866908ee6] to [1286f52e69].
︙ | ︙ | |||
11 12 13 14 15 16 17 | use Garradin\UserTemplate\CommonModifiers; use Parsedown; use Parsedown_Extra; use const Garradin\{ADMIN_URL, WWW_URL}; | < < < < | < | < > | > > > > > | > > > > | | | > < < | < < < < < < < > > | > | > > | > > | > > | > > | | | > > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | use Garradin\UserTemplate\CommonModifiers; use Parsedown; use Parsedown_Extra; use const Garradin\{ADMIN_URL, WWW_URL}; class Blocks extends AbstractRender { const SEPARATOR = "\n\n====\n\n"; static protected $parsedown; protected $_stack = []; // Grid columns templates // CSS grid template => Number of columns const COLUMNS_TEMPLATES = [ 'none' => 1, // No columns 'none / 1fr 1fr' => 2, 'none / 1fr 1fr 1fr' => 3, 'none / 1fr 1fr 1fr 1fr' => 4, 'none / .5fr 1fr' => 2, 'none / 1fr .5fr' => 2, ]; public function render(?string $content = null): string { $content = preg_replace("/\r\n?/", "\n", $content); $out = '<div class="web-blocks">'; foreach (explode(self::SEPARATOR, $content) as $block) { $out .= $this->block($block); } foreach ($this->_stack as $type) { $out .= '</article></section>'; } $out .= '</div>'; return $out; } protected function block(string $block): string { @list($header, $content) = explode("\n\n", $block, 2); $out = ''; $meta = []; foreach (explode("\n", $header) as $line) { $key = strtolower(trim(strtok($line, ':'))); $value = trim(strtok('')); $meta[$key] = $value; } if (empty($meta['type'])) { throw new \InvalidArgumentException('No type specified in block'); } $type = $meta['type']; $content = trim($content ?? '', "\n"); $class = sprintf('web-block-%s', $type); switch ($type) { case 'grid': if (array_pop($this->_stack)) { $out .= '</article></section>'; } $out .= sprintf('<section class="web-grid" %s>', isset($meta['grid-template']) ? sprintf('style="--grid-template: %s"', htmlspecialchars($meta['grid-template'])) : ''); return $out; case 'column': if (array_pop($this->_stack)) { $out .= '</article>'; } $out .= '<article class="web-column">'; $this->_stack[] = 'column'; return $out; case 'code': return sprintf('<pre class="%s">%s</pre>', $class, htmlspecialchars($content)); case 'markdown': $md = new Markdown; return sprintf('<div class="web-content %s">%s</div>', $class, $md->render($content)); case 'skriv': $skriv = new Skriv; return sprintf('<div class="web-content %s">%s</div>', $class, $skriv->render($content)); case 'image': return sprintf('<div class="%s">%s</div>', $class, $this->image($content, $meta)); case 'gallery': return sprintf('<div class="%s">%s</div>', $class, $this->gallery($content, $meta)); case 'heading': return sprintf('<h2 class="%s">%s</h2>', $class, htmlspecialchars($content)); case 'quote': return sprintf('<blockquote class="%s"><p>%s</p></blockquote>', $class, nl2br(htmlspecialchars($content))); case 'video': return sprintf('<iframe class="%s" src="%s" frameborder="0" allow="accelerometer; encrypted-media; gyroscope" allowfullscreen></iframe>', $class, htmlspecialchars($content)); default: throw new \LogicException('Unknown type: ' . $type); } } public function image(string $content, array $meta): string { $content = explode('|', $content); $url = $this->resolveAttachment(trim($content[0] ?? '')); $size = intval($meta['size'] ?? 0); $caption = htmlspecialchars(trim($content[1] ?? '')); $figcaption = $caption ? sprintf('<figcaption>%s</figcaption>', $caption) : ''; if (!empty($meta['size'])) { return sprintf( '<figure><a href="%s"><img src="%s" alt="%s" /></a>%s</figure>', htmlspecialchars($url), htmlspecialchars(sprintf('%s?%dpx', $url, $size)), $caption, $figcaption ); } else { return sprintf( '<figure><img src="%s" alt="%s" />%s</figure>', $url, $caption, $figcaption ); } } public function gallery(string $content, array $meta): string { $images = explode("\n", trim($content)); $out = ''; foreach ($images as $image) { $out .= $this->image($image, ['size' => 200]); } return $out; } } |
Modified src/include/lib/Garradin/Web/Render/Parsedown.php from [8582efeb8b] to [4ec75d240a].
︙ | ︙ | |||
93 94 95 96 97 98 99 | public function buildTOC(): string { if (!count($this->toc)) { return ''; } | | | | > | | > | > > | > > | > | | 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 | public function buildTOC(): string { if (!count($this->toc)) { return ''; } $out = '<div class="toc">' . PHP_EOL; $level = 0; foreach ($this->toc as $k => $h) { if ($h['level'] < $level) { $out .= "\n" . str_repeat("\t", $level); $out .= str_repeat("</ol></li>\n", $level - $h['level']); $level = $h['level']; } elseif ($h['level'] > $level) { $out .= "\n" . str_repeat("\t", $h['level']); $out .= str_repeat("<ol>\n", $h['level'] - $level); $level = $h['level']; } elseif ($k) { $out .= "</li>\n"; } $out .= str_repeat("\t", $level + 1); $out .= sprintf('<li><a href="#%s">%s</a>', $h['id'], $h['label']); } if ($level > 0) { $out .= "\n"; $out .= str_repeat('</li></ol>', $level); } $out .= '</div>'; return $out; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/Render.php from [baee19544e] to [dac2d2f09d].
1 2 3 4 5 6 7 8 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; class Render { | < > > > | > > > > > | | | > > > | > > > > | | | > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; class Render { const FORMAT_SKRIV = 'skriv'; const FORMAT_ENCRYPTED = 'skriv/encrypted'; const FORMAT_MARKDOWN = 'markdown'; const FORMAT_BLOCKS = 'blocks'; static protected $attachments = []; static public function render(string $format, ?File $file, string $content = null, string $link_prefix = null) { return self::getRenderer($format, $file, $link_prefix)->render($content); } static public function getRenderer(string $format, ?File $file, string $link_prefix = null) { if ($format == self::FORMAT_SKRIV) { return new Skriv($file, $link_prefix); } else if ($format == self::FORMAT_ENCRYPTED) { return new EncryptedSkriv($file, $link_prefix); } else if ($format == self::FORMAT_MARKDOWN) { return new Markdown($file, $link_prefix); } else if ($format == self::FORMAT_BLOCKS) { return new Blocks($file, $link_prefix); } else { throw new \LogicException('Invalid format: ' . $format); } } static public function registerAttachment(?File $file, string $uri): void { if (null === $file) { return; } $hash = $file->pathHash(); if (!array_key_exists($hash, self::$attachments)) { self::$attachments[$hash] = []; } self::$attachments[$hash][$uri] = true; } static public function listAttachments(File $file) { return array_keys(self::$attachments[$file->pathHash()] ?? []); } } |
Modified src/include/lib/Garradin/Web/Render/Skriv.php from [02e93f8ff3] to [d47507aaf3].
1 2 3 4 5 6 7 8 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\Web\Render; use Garradin\Entities\Files\File; use Garradin\Plugin; use Garradin\UserTemplate\CommonModifiers; use KD2\SkrivLite; use const Garradin\{ADMIN_URL, WWW_URL}; class Skriv extends AbstractRender { protected $skriv; public function __construct(?File $file = null, ?string $user_prefix = null) { parent::__construct($file, $user_prefix); $this->skriv = new SkrivLite; $this->skriv->registerExtension('file', [$this, 'SkrivFile']); $this->skriv->registerExtension('fichier', [$this, 'SkrivFile']); $this->skriv->registerExtension('image', [$this, 'SkrivImage']); // Enregistrer d'autres extensions éventuellement Plugin::fireSignal('skriv.init', ['skriv' => $this->skriv]); } public function render(?string $content = null): string { | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Plugin; use Garradin\UserTemplate\CommonModifiers; use KD2\SkrivLite; use KD2\Garbage2xhtml; use const Garradin\{ADMIN_URL, WWW_URL}; class Skriv extends AbstractRender { protected $skriv; protected $g2x; public function __construct(?File $file = null, ?string $user_prefix = null) { parent::__construct($file, $user_prefix); $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 { |
︙ | ︙ | |||
121 122 123 124 125 126 127 128 129 130 | if ($caption) { $caption = sprintf('<figcaption>%s</figcaption>', htmlspecialchars($caption)); } $out = sprintf('<figure class="image img-%s">%s%s</figure>', $align, $out, $caption); } return $out; } } | > > > > > > > > > > > > > > > > > > | 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | if ($caption) { $caption = sprintf('<figcaption>%s</figcaption>', htmlspecialchars($caption)); } $out = sprintf('<figure class="image img-%s">%s%s</figure>', $align, $out, $caption); } return $out; } /** * Callback utilisé pour l'extension <<html>>: permet d'insérer du code HTML protégé contre le XSS * (enfin, au max de ce qui est possible…) */ public function SkrivHTML(array $args, ?string $content, SkrivLite $skriv): string { if (null == $this->g2x) { $this->g2x = new Garbage2xhtml; $this->g2x->secure = true; $this->g2x->enclose_text = false; $this->g2x->auto_br = false; } $out = $this->g2x->process($content); return $out; } } |
Modified src/include/lib/Garradin/Web/Skeleton.php from [4e20c725d4] to [054c4e8297].
︙ | ︙ | |||
93 94 95 96 97 98 99 | try { $ut = new UserTemplate('web/' . $this->path); } catch (\InvalidArgumentException $e) { header('HTTP/1.1 404 Not Found', true); // Fallback to 404 | | | > > > > | 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 | try { $ut = new UserTemplate('web/' . $this->path); } catch (\InvalidArgumentException $e) { header('HTTP/1.1 404 Not Found', true); // Fallback to 404 $ut = new UserTemplate('web/404.html'); $ut->assignArray($params); $ut->display(); } try { $ut->assignArray($params); $ut->displayWeb(); } catch (Brindille_Exception $e) { if (!headers_sent()) { header('Content-Type: text/html; charset=utf-8', true); } printf('<div style="border: 5px solid orange; padding: 10px; background: yellow;"><h2>Erreur dans le squelette</h2><p>%s</p></div>', nl2br(htmlspecialchars($e->getMessage()))); } } elseif ($file = $this->file()) { $file->serve(); } else { |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Web.php from [0974a8f35f] to [147f3963f3].
︙ | ︙ | |||
109 110 111 112 113 114 115 | $page->save(); } */ } static public function listCategories(string $parent): array { | | | | | 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 | $page->save(); } */ } static public function listCategories(string $parent): array { $sql = 'SELECT * FROM @TABLE WHERE parent = ? AND type = ? ORDER BY title COLLATE U_NOCASE;'; return EM::getInstance(Page::class)->all($sql, $parent, Page::TYPE_CATEGORY); } static public function listPages(string $parent, bool $order_by_date = true): array { $order = $order_by_date ? 'published DESC' : 'title COLLATE U_NOCASE'; $sql = sprintf('SELECT * FROM @TABLE WHERE parent = ? AND type = %d ORDER BY %s;', Page::TYPE_PAGE, $order); return EM::getInstance(Page::class)->all($sql, $parent); } static public function listAll(string $parent): array { $sql = 'SELECT * FROM @TABLE WHERE parent = ? ORDER BY title COLLATE U_NOCASE;'; return EM::getInstance(Page::class)->all($sql, $parent); } static public function get(string $path): ?Page { $page = EM::findOne(Page::class, 'SELECT * FROM @TABLE WHERE path = ?;', $path); |
︙ | ︙ |
Modified src/include/lib/dependencies.list from [aa4290a8f6] to [fea70107ab].
1 2 3 4 5 6 7 | KD2/data/ KD2/DB/AbstractEntity.php KD2/DB/DB.php KD2/DB/EntityManager.php KD2/DB/SQLite3.php KD2/Brindille.php KD2/ErrorManager.php | < | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | KD2/data/ KD2/DB/AbstractEntity.php KD2/DB/DB.php KD2/DB/EntityManager.php KD2/DB/SQLite3.php KD2/Brindille.php KD2/ErrorManager.php KD2/Form.php KD2/FossilInstaller.php KD2/Garbage2xhtml.php KD2/Graphics/Image.php KD2/Graphics/QRCode.php KD2/Graphics/SVG/Pie.php KD2/Graphics/SVG/Plot.php KD2/Graphics/SVG/Bar.php KD2/JSONSchema.php KD2/HTTP.php KD2/Mail_Message.php KD2/Office/Calc/Writer.php KD2/Security.php KD2/Security_OTP.php KD2/SimpleDiff.php KD2/SkrivLite.php KD2/Smartyer.php KD2/SMTP.php KD2/Translate.php KD2/UserSession.php KD2/ZipWriter.php Parsedown.php |
Added src/scripts/change_user_password.php version [782584c2b0].
> > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?php namespace Garradin; use Garradin\Membres\Session; if (PHP_SAPI != 'cli') { die("Wrong call"); } require_once __DIR__ . '/../include/init.php'; $id = readline('User ID: '); $pw = readline('Password: '); if (!$id || !$pw) { exit; } $id = (int) $id; $pw = trim($pw); $pw = Session::hashPassword($pw); DB::getInstance()->preparedQuery('UPDATE membres SET passe = ? WHERE id = ?;', $pw, $id); echo "OK\n"; |
Modified src/scripts/cron.php from [f852009f8f] to [11940cccfa].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use Garradin\Services\Reminders; require_once __DIR__ . '/../include/init.php'; // Exécution des tâches automatiques $config = Config::getInstance(); | > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php namespace Garradin; use Garradin\Services\Reminders; if (PHP_SAPI != 'cli' && !defined('\Garradin\ROOT')) { die("Wrong call"); } require_once __DIR__ . '/../include/init.php'; // Exécution des tâches automatiques $config = Config::getInstance(); if ($config->get('frequence_sauvegardes') && $config->get('nombre_sauvegardes')) { $s = new Sauvegarde; $s->auto(); } // Exécution des rappels automatiques Reminders::sendPending(); Plugin::fireSignal('cron'); |
Added src/scripts/emails.php version [29d8d5abd0].
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin; use Garradin\Users\Emails; if (PHP_SAPI != 'cli') { die("Wrong call"); } require_once __DIR__ . '/../include/init.php'; // Send messages in queue Emails::runQueue(); |
Added src/scripts/handle_bounce.php version [5eda369c22].
> > > > > > > > > > > > > > > > > > > > > | 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\Users\Emails; require_once __DIR__ . '/../include/init.php'; if (PHP_SAPI != 'cli') { echo "This command can only be called from the command-line.\n"; exit(1); } $message = file_get_contents('php://stdin'); if (empty($message)) { echo "No STDIN content was provided.\nPlease provide the email message on STDIN.\n"; exit(2); } Emails::handleBounce($message); |
Modified src/scripts/upgrade.php from [63ad14ab52] to [f8b2f46a4c].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; const UPGRADE_PROCESS = true; require_once __DIR__ . '/../include/init.php'; try { if (Upgrade::preCheck()) { Upgrade::upgrade(); exit(2); | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; const UPGRADE_PROCESS = true; if (PHP_SAPI != 'cli') { die("Wrong call"); } require_once __DIR__ . '/../include/init.php'; try { if (Upgrade::preCheck()) { Upgrade::upgrade(); exit(2); |
︙ | ︙ |
Modified src/skel-dist/transaction/bilan2020/index.html from [a691584593] to [576a913405].
︙ | ︙ | |||
561 562 563 564 565 566 567 568 569 570 | <tr class="total"> <th>TOTAL GÉNÉRAL (I + II + III + IV + V)</th> <td>{{$t11.balance|math:'+':$t21.balance:'+':$t31.balance:'+':$t41.balance|raw|money}}</td> <td>{{$t12.balance|math:'+':$t22.balance:'+':$t32.balance:'+':$t42.balance|raw|money}}</td> </tr> </tbody> </table> </body> </html> | > > > > > > > > > > | 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 | <tr class="total"> <th>TOTAL GÉNÉRAL (I + II + III + IV + V)</th> <td>{{$t11.balance|math:'+':$t21.balance:'+':$t31.balance:'+':$t41.balance|raw|money}}</td> <td>{{$t12.balance|math:'+':$t22.balance:'+':$t32.balance:'+':$t42.balance|raw|money}}</td> </tr> </tbody> </table> {{#foreach from=$csv}} <tr> <th>{{$value.0}}</th> <td>{{#accounts_sums codes=$value.1 year=$n}}{{$balance|raw|money}}{{/accounts_sums}}</td> <td>{{#accounts_sums codes=$value.2 year=$n}}{{$balance|raw|money}}{{/accounts_sums}}</td> <td>{{#accounts_sums codes=$value.3 year=$n}}{{$balance|raw|money}}{{/accounts_sums}}</td> <td>{{#accounts_sums codes=$value.4 year=$n1}}{{$balance|raw|money}}{{/accounts_sums}}</td> </tr> {{/foreach}} </body> </html> |
Modified src/skel-dist/web/content.css from [36b56b4140] to [ce4188bfc6].
1 2 3 4 5 6 7 8 9 | /** * Ce fichier contient les styles CSS qui s'appliquent au contenu des articles et catégorie, * que ce soit sur le site public ou dans la prévisualisation de l'administration. * * Généralement il n'est pas nécessaire de le modifier. */ .web-content p, .web-content h1, .web-content h2, .web-content h3, .web-content h4, .web-content h5, .web-content h6, .web-content ul, .web-content ol, .web-content table, .web-content blockquote, .web-content pre { | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * Ce fichier contient les styles CSS qui s'appliquent au contenu des articles et catégorie, * que ce soit sur le site public ou dans la prévisualisation de l'administration. * * Généralement il n'est pas nécessaire de le modifier. */ .web-content p, .web-content h1, .web-content h2, .web-content h3, .web-content h4, .web-content h5, .web-content h6, .web-content ul, .web-content ol, .web-content table, .web-content blockquote, .web-content pre { margin: 0; margin-bottom: .8em; } .web-content ul, .web-content ol, .web-content dd { margin-left: 2em; } .web-content ul { |
︙ | ︙ | |||
212 213 214 215 216 217 218 219 220 221 222 223 224 225 | background: #000; max-width: 100%; max-height: 100%; padding: .5rem; border-radius: .5em; cursor: pointer; } @media handheld, screen and (max-width: 980px) { .imageBrowser figure { max-width: 100%; max-height: 100%; } } | > > > > > > > > > > > > > > > > > > > | 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 | background: #000; max-width: 100%; max-height: 100%; padding: .5rem; border-radius: .5em; cursor: pointer; } .web-grid { /* Default grid template: just auto sized columns */; --grid-template: none / repeat(auto-fit, minmax(100px, 1fr)); grid-gap: 1rem; display: grid; grid-template: var(--grid-template); } .web-column { margin-bottom: 1rem; } @media handheld, screen and (max-width: 980px) { .imageBrowser figure { max-width: 100%; max-height: 100%; } } @media screen and (min-width: 60rem) { .web-columns { /* Get template from variable, which is defined style attribute */ grid-template: var(--grid-template); } } |
Added src/templates/acc/accounts/_nav.tpl version [faec48c433].
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton shape="edit" href="!acc/charts/accounts/?id=%d"|args:$current_year.id_chart label="Modifier les comptes"} {/if} {linkbutton shape="search" href="!acc/search.php?year=%d"|args:$current_year.id label="Recherche"} </aside> <ul> <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}acc/accounts/">Comptes usuels</a></li> <li{if $current == 'all'} class="current"{/if}><a href="{$admin_url}acc/accounts/all.php?year={$current_year.id}">Tous les comptes</a></li> <li{if $current == 'users'} class="current"{/if}><a href="{$admin_url}acc/accounts/users.php">Comptes de membres</a></li> <li><a href="{$admin_url}acc/reports/statement.php?year={$current_year.id}"><em>Compte de résultat</em></a></li> <li><a href="{$admin_url}acc/reports/balance_sheet.php?year={$current_year.id}"><em>Bilan</em></a></li> </ul> </nav> |
Modified src/templates/acc/accounts/all.tpl from [ff6fa9acdc] to [3d70275b38].
|
| > > > | | > | < < > > | | < | | | > > | < < | | | > | < | < | < < < < > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <?php use Garradin\Entities\Accounting\Account; ?> {include file="admin/_head.tpl" title="Tous les comptes" current="acc/accounts"} {include file="acc/_year_select.tpl"} {include file="acc/accounts/_nav.tpl" current="all"} {include file="acc/_simple_help.tpl" link="../reports/trial_balance.php?year=%d"|args:$current_year.id type=null} {if !empty($balance)} <table class="list"> <thead> <tr> <td>Numéro</td> <th>Compte</th> <td class="money">Total des débits</td> <td class="money">Total des crédits</td> <td class="money">Solde</td> </tr> </thead> <tbody> {foreach from=$balance item="account"} <tr class="{if $account.balance === 0}disabled{/if}"> <td class="num"> <a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$current_year.id}">{$account.code}</a> </td> <th>{$account.label}</th> <td class="money{if !$account.debit} disabled{/if}">{$account.debit|raw|money:false}</td> <td class="money{if !$account.credit} disabled{/if}">{$account.credit|raw|money:false}</td> <td class="money">{if $account.balance !== null}<b>{$account.balance|escape|money:false}</b>{/if}</td> </tr> {/foreach} </tbody> </table> {else} <div class="alert block"> <p>Aucun compte ne comporte d'écriture sur cet exercice.</p> <p> {linkbutton href="!acc/transactions/new.php" label="Saisir une écriture" shape="plus"} </p> </div> {/if} <p class="help"> Note : n'apparaissent ici que les comptes qui ont été utilisés dans cet exercice (au moins une écriture).<br /> Les lignes grisées correspondent aux comptes soldés.<br /> Pour voir la liste complète des comptes, même ceux qui n'ont pas été utilisés, se référer au <a href="{$admin_url}acc/charts/accounts/?id={$current_year.id_chart}">plan comptable</a>. </p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/accounts/deposit.tpl from [42c0a571f4] to [0883138caa].
︙ | ︙ | |||
54 55 56 57 58 59 60 | <fieldset> <legend>Détails de l'écriture de dépôt</legend> <dl> {input type="text" name="label" label="Libellé" required=1 default="Dépôt en banque"} {input type="date" name="date" default=$date label="Date" required=1} {input type="money" name="amount" label="Montant" required=1} | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | <fieldset> <legend>Détails de l'écriture de dépôt</legend> <dl> {input type="text" name="label" label="Libellé" required=1 default="Dépôt en banque"} {input type="date" name="date" default=$date label="Date" required=1} {input type="money" name="amount" label="Montant" required=1} {input type="list" target="!acc/charts/accounts/selector.php?chart=%d&targets=%d"|args:$account.id_chart,$target name="account_transfer" label="Compte de dépôt" required=1} {input type="text" name="reference" label="Numéro de pièce comptable"} {input type="textarea" name="notes" label="Remarques" rows=4 cols=30} </dl> </fieldset> <p class="submit"> {csrf_field key="acc_deposit_%s"|args:$account.id} |
︙ | ︙ |
Modified src/templates/acc/accounts/index.tpl from [46d037e135] to [33e3d48eca].
|
| > > > | < < < < < | | < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php use Garradin\Entities\Accounting\Account; ?> {include file="admin/_head.tpl" title="Comptes usuels" current="acc/accounts"} {include file="acc/_year_select.tpl"} {include file="acc/accounts/_nav.tpl" current="index"} {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} |
︙ | ︙ | |||
42 43 44 45 46 47 48 | <td colspan="5"><h2 class="ruler">{$group.label}</h2></td> </tr> {foreach from=$group.accounts item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$current_year.id}">{$account.code}</a></td> <th><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$current_year.id}">{$account.label}</a></th> <td class="money"> | | > > > | | | | < | > > > | | | | | | | 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 | <td colspan="5"><h2 class="ruler">{$group.label}</h2></td> </tr> {foreach from=$group.accounts item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$current_year.id}">{$account.code}</a></td> <th><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$current_year.id}">{$account.label}</a></th> <td class="money"> {if $account.balance < 0 || ($account.balance > 0 && $account.position == Account::LIABILITY && ($account.type == Account::TYPE_BANK || $account.type == Account::TYPE_THIRD_PARTY || $account.type == Account::TYPE_CASH))} <strong class="error">-{$account.balance|raw|abs|money_currency:false}</strong> {else} {$account.balance|raw|money_currency:false} {/if} </td> <td> {if $account.type == Account::TYPE_THIRD_PARTY && $account.balance > 0} {if $account.position == Account::LIABILITY}<em class="alert">(Dette)</em> {elseif $account.position == Account::ASSET}<em class="alert">(Créance)</em> {/if} {elseif $account.type == Account::TYPE_BANK && $account.balance > 0 && $account.position == Account::LIABILITY} <em class="alert">(Découvert)</em> {elseif $account.type == Account::TYPE_CASH && $account.balance > 0 && $account.position == Account::LIABILITY} <em class="alert">(Anomalie)</em> {/if} </td> <td class="actions"> {linkbutton label="Journal" shape="menu" href="journal.php?id=%d&year=%d"|args:$account.id,$current_year.id} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {if $account.type == Entities\Accounting\Account::TYPE_BANK && ($account.debit || $account.credit)} {linkbutton label="Rapprochement" shape="check" href="reconcile.php?id=%d"|args:$account.id} {elseif $account.type == Entities\Accounting\Account::TYPE_OUTSTANDING && $account.debit} {linkbutton label="Dépôt en banque" shape="check" href="deposit.php?id=%d"|args:$account.id} {/if} {/if} </td> </tr> {/foreach} </tbody> <?php $has_accounts = true; ?> {/foreach} </table> {if !$has_accounts} <div class="alert block"> <p>Aucun compte usuel ne comporte d'écriture sur cet exercice.</p> <p> {linkbutton href="!acc/transactions/new.php" label="Saisir une écriture" shape="plus"} </p> </div> {/if} {/if} <p class="help"> Note : n'apparaissent ici que les comptes <strong>usuels</strong> qui ont été utilisés dans cet exercice (au moins une écriture).<br /> Pour voir le solde de tous les comptes, se référer à la <a href="all.php">liste de tous comptes de l'exercice</a>.<br /> Pour voir la liste complète des comptes, même ceux qui n'ont pas été utilisés, se référer au <a href="{$admin_url}acc/charts/accounts/?id={$current_year.id_chart}">plan comptable</a>. </p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/accounts/journal.tpl from [f4432495e8] to [a4a61762be].
1 2 3 4 5 6 7 8 9 10 11 12 13 | {include file="admin/_head.tpl" title="Journal : %s - %s"|args:$account.code:$account.label current="acc/accounts" body_id="rapport"} {if empty($year)} {include file="acc/_year_select.tpl"} {else} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> {/if} {if $account.type} | | | < < | | | | | | | | < < | > > | | | | | | | | > > > | | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | {include file="admin/_head.tpl" title="Journal : %s - %s"|args:$account.code:$account.label current="acc/accounts" body_id="rapport"} {if empty($year)} {include file="acc/_year_select.tpl"} {else} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> {/if} {if $account.type} {if $simple && !$account->isReversed($simple, $year.id)} {include file="acc/_simple_help.tpl" link="?id=%d&simple=0&year=%d"|args:$account.id,$year.id type=$account.type} {/if} {if $simple} {if $account.type == $account::TYPE_THIRD_PARTY} {if $account->getPosition($year->id) == $account::ASSET && $sum.balance > 0} <p class="alert block">Ce tiers vous doit <strong>{$sum.balance|abs|raw|money_currency}</strong>.</p> {elseif $account->getPosition($year->id) == $account::LIABILITY && $sum.balance > 0} <p class="alert block">Vous devez <strong>{$sum.balance|abs|raw|money_currency}</strong> à ce tiers.</p> {/if} {elseif $account.type == $account::TYPE_BANK} {if $account->getPosition($year->id) == $account::ASSET && $sum.balance > 0} <p class="confirm block">Ce compte est créditeur de <strong>{$sum.balance|abs|raw|money_currency}</strong> à la banque.</p> {elseif $account->getPosition($year->id) == $account::LIABILITY && $sum.balance > 0} <p class="error block">Ce compte est à découvert de <strong>{$sum.balance|abs|raw|money_currency}</strong> à la banque.</p> {/if} {elseif $account.type == $account::TYPE_CASH} {if $account->getPosition($year->id) == $account::ASSET && $sum.balance > 0} <p class="confirm block">Cette caisse est créditrice de <strong>{$sum.balance|abs|raw|money_currency}</strong>.</p> {elseif $account->getPosition($year->id) == $account::LIABILITY && $sum.balance > 0} <p class="error block">Cette caisse est débiteur de <strong>{$sum.balance|abs|raw|money_currency}</strong>. Est-ce normal ? Une vérification est peut-être nécessaire ?</p> {/if} {elseif $account.type == $account::TYPE_OUTSTANDING} {if $sum.balance < 0} <p class="error block">Ce compte est débiteur <strong>{$sum.balance|abs|raw|money_currency}</strong>. Est-ce normal ? Une vérification est peut-être nécessaire ?</p> {elseif $sum.balance > 0} <p class="confirm block">Ce compte d'attente est créditeur de <strong>{$sum.balance|abs|raw|money_currency}</strong>. {if $sum.balance > 200}Un dépôt à la banque serait peut-être une bonne idée ?{/if}</p> {/if} {elseif $account.type == $account::TYPE_REVENUE && $sum.balance < 0} <p class="alert block">Ce compte présente un solde négatif de <strong>{$sum.balance|raw|money_currency}</strong>. Est-ce normal ? Cette situation ne devrait se produire que si vous avez dû procéder à des remboursements par exemple, et que ceux-ci couvrent des recettes perçues sur un exercice précédent.</p> {elseif $account.type == $account::TYPE_EXPENSE && $sum.balance < 0} <p class="alert block">Ce compte présente un solde négatif de <strong>{$sum.balance|raw|money_currency}</strong>. Est-ce normal ? Cette situation ne devrait se produire que si vous avez reçu des remboursements par exemple, et que ceux-ci couvrent des dépenses réglées sur un exercice précédent.</p> {/if} {/if} <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <nav class="menu"> <b data-icon="↷" class="btn">Export</b> <span> {linkbutton href="%s&export=csv"|args:$self_url label="Export CSV" shape="export"} {linkbutton href="%s&export=ods"|args:$self_url label="Export LibreOffice" shape="export"} {if CALC_CONVERT_COMMAND} {linkbutton href="%s&export=xlsx"|args:$self_url label="Export Excel" shape="export"} {/if} </span> </nav> {/if} {linkbutton shape="search" href="!acc/search.php?year=%d&account=%s"|args:$year.id,$account.code label="Recherche"} {if $year.id == CURRENT_YEAR_ID} {linkbutton href="!acc/transactions/new.php?account=%d"|args:$account.id label="Saisir une écriture dans ce compte" shape="plus"} {/if} </aside> <ul> |
︙ | ︙ | |||
135 136 137 138 139 140 141 | <tfoot> <tr> {if $can_edit} <td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td> {/if} {if !$simple}<td></td>{/if} {if null !== $sum} | > > > > > > > | | > | 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | <tfoot> <tr> {if $can_edit} <td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td> {/if} {if !$simple}<td></td>{/if} {if null !== $sum} {if !$simple} <td><b>Total</b></td> <td class="money">{$sum.debit|raw|money:false}</td> <td class="money">{$sum.credit|raw|money:false}</td> <td class="money">{$line.sum|raw|money:false}</td> {else} <td></td> <td colspan="2"><b>Total</b></td> <td class="money">{$line.sum|raw|money:false}</td> {/if} {else} <td colspan="4"></td> {/if} {if !$simple}<td></td>{/if} <td class="actions" colspan="5"> {if $can_edit} <em>Pour les écritures cochées :</em> |
︙ | ︙ |
Modified src/templates/acc/accounts/reconcile_assist.tpl from [d7fc6d0db8] to [283d27b0f7].
︙ | ︙ | |||
13 14 15 16 17 18 19 | <form method="post" action="{$self_url}" enctype="multipart/form-data"> {if !$csv->loaded()} <fieldset> <legend>Relevé de compte</legend> <p class="help block"> Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br /> | | > > > > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <form method="post" action="{$self_url}" enctype="multipart/form-data"> {if !$csv->loaded()} <fieldset> <legend>Relevé de compte</legend> <p class="help block"> Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br /> <a href="https://garradin.eu/rapprochement_assiste" target="_blank">Aide détaillée</a> </p> <dl> {include file="common/_csv_help.tpl"} <dd class="help"> Le fichier doit également disposer soit d'une colonne <strong>Montant</strong>, soit de deux colonnes <strong>Débit</strong> et <strong>Crédit</strong>. </dd> {input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1} </dl> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="upload" label="Envoyer le fichier" class="main" shape="upload"} </p> </fieldset> |
︙ | ︙ | |||
114 115 116 117 118 119 120 | {$line.journal.debit|raw|money} {/if} </td> <td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|money:false}</td> <th style="text-align: right">{$line.journal.label}</th> {else} <td colspan="5"></td> | | < | < | | 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 | {$line.journal.debit|raw|money} {/if} </td> <td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|money:false}</td> <th style="text-align: right">{$line.journal.label}</th> {else} <td colspan="5"></td> <td class="actions"> {if $line.add} {linkbutton label="Saisir cette écriture" target="_dialog" href="!acc/transactions/new.php?%s"|args:$line.csv.new_params shape="plus"} {/if} </td> {/if} <td class="separator"> {if $line->journal && $line->csv} == {else} <b class="icn">⚠</b> {/if} </td> {if isset($line->csv)} <th class="separator">{$line.csv.label}</th> <td class="money"> {$line.csv.amount|raw|money} </td> <td class="money">{if $line.csv.balance}{$line.csv.balance|raw|money}{else}{$line.csv.running_sum|raw|money}{/if}</td> <td>{$line.csv.date|date_short}</td> {else} <td colspan="4" class="separator"></td> {/if} </tr> {/if} {/foreach} |
︙ | ︙ |
Modified src/templates/acc/accounts/simple.tpl from [2dd6bed10c] to [d86d68ee5b].
1 2 3 4 5 6 7 | {include file="admin/_head.tpl" title="Suivi : %s"|args:$types[$type] current="acc/simple"} {include file="acc/_year_select.tpl"} <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} | > > > | | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | {include file="admin/_head.tpl" title="Suivi : %s"|args:$types[$type] current="acc/simple"} {include file="acc/_year_select.tpl"} <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <nav class="menu"> <b data-icon="↷" class="btn">Export</b> <span> {linkbutton href="?type=%d&export=csv"|args:$type label="Export CSV" shape="export"} {linkbutton href="?type=%d&export=ods"|args:$type label="Export LibreOffice" shape="export"} {if CALC_CONVERT_COMMAND} {linkbutton href="?type=%d&export=xlsx"|args:$type label="Export Excel" shape="export"} {/if} </span> </nav> {/if} {linkbutton shape="search" href="!acc/search.php?year=%d&type=%d"|args:$year.id,$type label="Recherche"} </aside> <ul> {foreach from=$types key="key" item="label"} <li{if $type == $key} class="current"{/if}><a href="?type={$key}">{$label}</a></li> {/foreach} |
︙ | ︙ | |||
29 30 31 32 33 34 35 36 37 38 39 40 41 42 | {foreach from=$list->iterate() item="line"} <tr> {if $can_edit} <td class="check"> {input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0} </td> {/if} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td class="money">{$line.change|abs|raw|money}</td> <td>{$line.reference}</td> <th>{$line.label}</th> <td>{$line.line_reference}</td> <td class="num">{foreach from=$line.code_analytical item="code" key="id"}<a href="{$admin_url}acc/reports/statement.php?analytical={$id}">{$code}</a> {/foreach}</td> | > > > | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | {foreach from=$list->iterate() item="line"} <tr> {if $can_edit} <td class="check"> {input type="checkbox" name="check[%s]"|args:$line.id_line value=$line.id default=0} </td> {/if} {if $line.type_label} <td>{$line.type_label}</td> {/if} <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td class="money">{$line.change|abs|raw|money}</td> <td>{$line.reference}</td> <th>{$line.label}</th> <td>{$line.line_reference}</td> <td class="num">{foreach from=$line.code_analytical item="code" key="id"}<a href="{$admin_url}acc/reports/statement.php?analytical={$id}">{$code}</a> {/foreach}</td> |
︙ | ︙ |
Added src/templates/acc/accounts/users.tpl version [0c99a5ec23].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | {include file="admin/_head.tpl" title="Comptes de membres" current="acc/accounts"} {include file="acc/_year_select.tpl"} {include file="acc/accounts/_nav.tpl" current="users"} <p class="help"> Ce tableau présente une liste de comptes « virtuels » représentant les membres liés aux écritures. Seules les écritures liées à des comptes de tiers (marqués comme usuels) sont comptabilisées.<br /> Les membres qui n'ont aucune écriture associée n'apparaissent pas dans ce tableau. </p> {if !$list->count()} <p class="alert block">Aucune écriture liée à un membre n'existe sur cet exercice.</p> {else} {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="row"} <tr> <td class="num"><a href="{$admin_url}acc/transactions/user.php?id={$row.id}&year={$current_year.id}">{$row.user_number}</a></td> <th><a href="{$admin_url}acc/transactions/user.php?id={$row.id}&year={$current_year.id}">{$row.user_identity}</a></th> <td class="money"> {if $row.balance < 0}<strong class="error">{/if} {$row.balance|raw|money_currency:false} {if $row.balance < 0}</strong>{/if} </td> <td> <em class="alert"> {if $row.balance < 0}Dette {elseif $row.balance > 0}Créance {/if} </em> </td> <td class="actions"> {linkbutton label="Journal" shape="menu" href="!acc/transactions/user.php?id=%d&year=%d"|args:$row.id,$current_year.id} </td> </tr> {/foreach} </tbody> </table> {/if} <p class="help"> Dette = l'association doit de l'argent à ce membre<br /> Créance = le membre doit de l'argent à l'association </p> {include file="admin/_foot.tpl"} |
Added src/templates/acc/charts/_nav.tpl version [c5ebfce017].
> > > > > > > | 1 2 3 4 5 6 7 | <nav class="tabs"> <ul> <li{if $current == 'charts'} class="current"{/if}><a href="{$admin_url}acc/charts/">Plans comptables</a></li> <li{if $current == 'install'} class="current"{/if}><a href="{$admin_url}acc/charts/install.php">Installer un plan comptable</a></li> <li{if $current == 'import'} class="current"{/if}><a href="{$admin_url}acc/charts/import.php">Importer un plan comptable personnel</a></li> </ul> </nav> |
Modified src/templates/acc/charts/accounts/_account_form.tpl from [67f92e389c] to [adc16430f8].
1 | <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 | <dl> {if !$account.type || !$create} {input type="select" label="Type de compte usuel" name="type" source=$account required=true options=$types} <dd class="help">Le statut de compte usuel est utilisé pour les écritures <em>« simplifiées »</em> (recettes, dépenses, dettes, créances, virements), pour la liste des comptes, et également pour proposer certaines fonctionnalités (rapprochement pour les comptes bancaires, règlement rapide de dette et créance, dépôt de chèques).</dd> <dd class="help">Un compte qui n'a pas de type usuel ne pourra être utilisé que dans une saisie avancée, et ne sera visible que dans les rapports de l'exercice.</dd> {else} <dt>Type de compte</dt> <dd><?php $t = $types[$account->type]; ?> {$t}</dd> {/if} {if !$account.type || $account.type == $account::TYPE_VOLUNTEERING} <dt><label for="f_position_0">Position au bilan ou résultat</label>{if !$edit_disabled} <b>(obligatoire)</b>{/if}</dt> <dd class="help">La position permet d'indiquer dans quelle partie du bilan ou du résultat doit figurer le compte.</dd> <dd class="help">Les comptes inscrits en actif ou passif figureront dans le bilan, alors que ceux inscrits en produit ou charge figureront au compte de résultat.</dd> {input type="radio" label="Ne pas utiliser ce compte au bilan ni au résultat" name="position" value=0 source=$account disabled=$edit_disabled} {if $account.type != $account::TYPE_VOLUNTEERING} {input type="radio" label="Bilan : actif" name="position" value=Entities\Accounting\Account::ASSET source=$account help="ce que possède l'association : stocks, locaux, soldes bancaires, etc." disabled=$edit_disabled} {input type="radio" label="Bilan : passif" name="position" value=Entities\Accounting\Account::LIABILITY source=$account help="ce que l'association doit : dettes, provisions, réserves, etc." disabled=$edit_disabled} {input type="radio" label="Bilan : actif ou passif" name="position" value=Entities\Accounting\Account::ASSET_OR_LIABILITY source=$account help="le compte sera placé à l'actif si son solde est débiteur, ou au passif s'il est créditeur" disabled=$edit_disabled} {/if} {input type="radio" label="Résultat : charge" name="position" value=Entities\Accounting\Account::EXPENSE source=$account help="dépenses" disabled=$edit_disabled} {input type="radio" label="Résultat : produit" name="position" value=Entities\Accounting\Account::REVENUE source=$account help="recettes" disabled=$edit_disabled} {/if} {input type="text" label="Code" maxlength="10" name="code" source=$account required=true help="Le code du compte sert à trier le compte dans le plan comptable, attention à choisir un code qui correspond au plan comptable." disabled=$edit_disabled} {input type="text" label="Libellé" name="label" source=$account required=true disabled=$edit_disabled} {input type="textarea" label="Description" name="description" source=$account} {if $create && in_array($account.type, [$account::TYPE_BANK, $account::TYPE_CASH, $account::TYPE_OUTSTANDING, $account::TYPE_THIRD_PARTY]) && !empty($current_year)} {input type="money" name="opening_amount" label="Solde d'ouverture" help="Si renseigné, ce solde sera inscrit dans l'exercice « %s »."|args:$current_year.label} {/if} </dl> |
Modified src/templates/acc/charts/accounts/_nav.tpl from [9ccd9b3c13] to [26818190bb].
︙ | ︙ | |||
11 12 13 14 15 16 17 | <li>{link href="!acc/charts/import.php" label="Importer un plan comptable"}</li> {/if} </ul> <ul class="sub"> <li class="title">{$chart.label}</li> {/if} | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <li>{link href="!acc/charts/import.php" label="Importer un plan comptable"}</li> {/if} </ul> <ul class="sub"> <li class="title">{$chart.label}</li> {/if} <li{if $current == 'favorites'} class="current"{/if}>{link href="!acc/charts/accounts/?id=%d"|args:$chart.id label="Comptes usuels"}</li> <li{if $current == 'all'} class="current"{/if}>{link href="!acc/charts/accounts/all.php?id=%d"|args:$chart.id label="Tous les comptes"}</li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <li{if $current == 'new'} class="current"{/if}><strong>{link href="!acc/charts/accounts/new.php?id=%d"|args:$chart.id label="Ajouter un compte"}</strong></li> {/if} </ul> </nav> |
Modified src/templates/acc/charts/accounts/edit.tpl from [0283435372] to [e354d8006c].
︙ | ︙ | |||
9 10 11 12 13 14 15 | Il n'est pas possible de modifier le libellé, le code ou la position de ce compte car il {if $account.user}est utilisé par des écritures liées à des exercices clôturés{else}fait partie du plan comptable officiel{/if}.<br /> Pour pouvoir modifier ce compte pour l'exercice courant, il est conseillé de <a href="{$admin_url}acc/charts/?from={$account.id_chart}">créer un nouveau plan comptable</a> en y recopiant l'ancien plan comptable. </p> {/if} <fieldset> <legend>Modifier un compte</legend> | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | Il n'est pas possible de modifier le libellé, le code ou la position de ce compte car il {if $account.user}est utilisé par des écritures liées à des exercices clôturés{else}fait partie du plan comptable officiel{/if}.<br /> Pour pouvoir modifier ce compte pour l'exercice courant, il est conseillé de <a href="{$admin_url}acc/charts/?from={$account.id_chart}">créer un nouveau plan comptable</a> en y recopiant l'ancien plan comptable. </p> {/if} <fieldset> <legend>Modifier un compte</legend> {include file="acc/charts/accounts/_account_form.tpl" create=false} </fieldset> <p class="submit"> {csrf_field key="acc_accounts_edit_%s"|args:$account.id} {button type="submit" name="edit" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/charts/accounts/index.tpl from [1453ceb936] to [410caa915d].
|
| | | 1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Comptes usuels" current="acc/charts"} {include file="acc/charts/accounts/_nav.tpl" current="favorites"} <table class="list"> {foreach from=$accounts_grouped item="group"} <tbody> <tr> |
︙ | ︙ |
Modified src/templates/acc/charts/accounts/new.tpl from [4b1c300015] to [da7b7cc99e].
1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="Nouveau compte" current="acc/charts"} {include file="acc/charts/accounts/_nav.tpl" current="new"} {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>Créer un nouveau compte</legend> | > > > > > > > > > > > > > > > > > > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | {include file="admin/_head.tpl" title="Nouveau compte" current="acc/charts"} {include file="acc/charts/accounts/_nav.tpl" current="new"} {form_errors} {if null === $type} <form method="get" action="{$self_url}" data-focus="1"> <fieldset> <legend>Créer un nouveau compte</legend> <dl><label for="f_type">Type de compte</label></dl> {foreach from=$types_create item="t" key="v"} {input type="radio-btn" name="type" value=$v label=$t.label help=$t.help} {/foreach} </fieldset> <p class="submit"> <input type="hidden" name="id" value="{$chart.id}" /> {button type="submit" label="Continuer" shape="right" class="main"} </p> </form> {else} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>Créer un nouveau compte</legend> {include file="acc/charts/accounts/_account_form.tpl" edit_disabled=false create=true} </fieldset> <p class="submit"> {csrf_field key="acc_accounts_new"} {button type="submit" name="save" label="Créer" shape="right" class="main"} </p> </form> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/charts/accounts/selector.tpl from [be1d21d8dd] to [f3ae24f199].
1 2 | {include file="admin/_head.tpl" title="Sélectionner un compte"} | < < < < < > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {include file="admin/_head.tpl" title="Sélectionner un compte"} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} <?php $page = isset($grouped_accounts) ? '' : 'all.php'; ?> <nav class="tabs"> <aside>{linkbutton label="Modifier les comptes" href="!acc/charts/accounts/%s?id=%d"|args:$page,$chart.id shape="edit"}</aside> </nav> {/if} {if empty($grouped_accounts) && empty($accounts)} <p class="block alert">Le plan comptable ne comporte aucun compte de ce type.<br /> {linkbutton href="!acc/charts/accounts/new.php?id=%s&type=%s"|args:$chart.id,$targets[0] label="Créer un compte" shape="plus"} </p> {else} <h2 class="ruler"> <input type="text" placeholder="Recherche rapide" id="lookup" /> {if !isset($grouped_accounts)} <label>{input type="checkbox" name="typed_only" value=0 default=0 default=$all} N'afficher que les comptes usuels</label> {/if} </h2> {if isset($grouped_accounts)} <?php $index = 1; ?> {foreach from=$grouped_accounts item="group"} |
︙ | ︙ |
Modified src/templates/acc/charts/import.tpl from [8e50447ace] to [0fbc722b84].
1 2 | {include file="admin/_head.tpl" title="Importer un nouveau plan comptable" current="acc/charts"} | < < < < < | | 1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="Importer un nouveau plan comptable" current="acc/charts"} {include file="./_nav.tpl" current="import"} {form_errors} <form method="post" action="{$self_url}" enctype="multipart/form-data" data-focus="1"> <fieldset> <legend>Importer un plan comptable</legend> <dl> |
︙ | ︙ |
Modified src/templates/acc/charts/index.tpl from [0e0aa6ffba] to [85fb8f8a5e].
1 2 | {include file="admin/_head.tpl" title="Gestion des plans comptables" current="acc/charts"} | < < < | < > | < < | 1 2 3 4 5 6 7 8 9 10 11 12 | {include file="admin/_head.tpl" title="Gestion des plans comptables" current="acc/charts"} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {include file="./_nav.tpl" current="charts"} {/if} {if $_GET.msg == 'OPEN'} <p class="block alert"> Il n'existe aucun exercice ouvert. {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures. {/if} |
︙ | ︙ | |||
31 32 33 34 35 36 37 | {foreach from=$list item="item"} <tr{if $item.archived} class="disabled"{/if}> <td>{$item.country|get_country_name}</td> <th><a href="{$admin_url}acc/charts/accounts/?id={$item.id}">{$item.label}</a></th> <td>{if $item.code}Officiel{else}Personnel{/if}</td> <td>{if $item.archived}<em>Archivé</em>{/if}</td> <td class="actions"> | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | {foreach from=$list item="item"} <tr{if $item.archived} class="disabled"{/if}> <td>{$item.country|get_country_name}</td> <th><a href="{$admin_url}acc/charts/accounts/?id={$item.id}">{$item.label}</a></th> <td>{if $item.code}Officiel{else}Personnel{/if}</td> <td>{if $item.archived}<em>Archivé</em>{/if}</td> <td class="actions"> {linkbutton shape="star" label="Comptes usuels" href="!acc/charts/accounts/?id=%d"|args:$item.id} {linkbutton shape="menu" label="Tous les comptes" href="!acc/charts/accounts/all.php?id=%d"|args:$item.id} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton shape="edit" label="Modifier" href="!acc/charts/edit.php?id=%d"|args:$item.id} {linkbutton shape="export" label="Export CSV" href="!acc/charts/export.php?id=%d"|args:$item.id} {linkbutton shape="export" label="Export tableur" href="!acc/charts/export.php?id=%d&ods"|args:$item.id} {if !$item.code && !$item.archived} {linkbutton shape="delete" label="Supprimer" href="!acc/charts/delete.php?id=%d"|args:$item.id} |
︙ | ︙ |
Added src/templates/acc/charts/install.tpl version [9f5b758fee].
> > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {include file="admin/_head.tpl" title="Importer un nouveau plan comptable" current="acc/charts"} {include file="./_nav.tpl" current="install"} {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>Installer un plan comptable</legend> <dl> {input type="select" name="code" label="Plan comptable" required=true options=$list} </dl> </fieldset> <p class="submit"> {csrf_field key="acc_charts_install"} {button type="submit" name="install" label="Installer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/index.tpl from [63dacb5997] to [70f092a209].
1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="Comptabilité" current="acc"} {foreach from=$years item="year"} <section class="year-infos"> <h2 class="ruler">{$year.label} — Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2> <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} | > > > > > > > > > > > > > > > > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | {include file="admin/_head.tpl" title="Comptabilité" current="acc"} {if !empty($all_years)} <form method="get" action="{$admin_url}acc/search.php" class="shortForm"> <fieldset> <legend>Recherche rapide</legend> <p> <input type="search" name="qt" value="" /> {input type="select" name="year" options=$all_years default=$first_year} {button type="submit" shape="search" label="Chercher"} </p> <p class="help"> Indiquer un numéro de compte, un numéro d'écriture précédé par le signe hash (<code>#1234</code>), un montant précédé par le signe égal (<code>=62,41</code>) ou une date (<code>JJ/MM/AAAA</code>), sinon la recherche sera effectuée sur le libellé ou la pièce comptable. </p> </fieldset> </form> {/if} {foreach from=$years item="year"} <section class="year-infos"> <h2 class="ruler">{$year.label} — Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2> <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton shape="upload" href="!acc/years/import.php?year=%d"|args:$year.id label="Import & export"} {/if} {linkbutton shape="search" href="!acc/search.php?year=%d"|args:$year.id label="Recherche"} </aside> <ul> <li><a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a></li> <li><a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a></li> <li><a href="{$admin_url}acc/reports/journal.php?year={$year.id}">Journal général</a></li> <li><a href="{$admin_url}acc/reports/ledger.php?year={$year.id}">Grand livre</a></li> <li><a href="{$admin_url}acc/reports/statement.php?year={$year.id}">Compte de résultat</a></li> <li><a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a></li> </ul> </nav> {if $year.nb_transactions > 3} <section class="graphs small"> {foreach from=$graphs key="url" item="label"} <figure> <img src="{$url|args:'year='|cat:$year.id}" alt="" /> <figcaption>{$label}</figcaption> </figure> {/foreach} </section> {else} <p class="help block">Il n'y a pas encore suffisamment d'écritures dans cet exercice pour pouvoir afficher les statistiques.</p> <p>{linkbutton label="Saisir une nouvelle écriture" shape="plus" href="transactions/new.php"}</p> {/if} {if $year.nb_transactions} <?php $list = $last_transactions[$year->id]; ?> <table class="list"> <caption>Dernières écritures</caption> <thead> <tr> {foreach from=$list->getHeaderColumns() item="column"} <td>{$column.label}</td> {/foreach} <td></td> </tr> </thead> <tbody> {foreach from=$list->iterate() item="line"} <tr> <td class="num"><a href="{$admin_url}acc/transactions/details.php?id={$line.id}">#{$line.id}</a></td> <td>{$line.date|date_short}</td> <td class="money">{$line.change|abs|raw|money}</td> <td>{$line.reference}</td> <th>{$line.label}</th> <td>{$line.line_reference}</td> <td class="num">{foreach from=$line.code_analytical item="code" key="id"}<a href="{$admin_url}acc/reports/statement.php?analytical={$id}">{$code}</a> {/foreach}</td> <td class="actions"> {linkbutton href="!acc/transactions/details.php?id=%d"|args:$line.id label="Détails" shape="search"} </td> </tr> {/foreach} </tbody> </table> {/if} </section> {foreachelse} <p class="block alert"> Il n'y a aucun exercice ouvert en cours.<br /> {linkbutton label="Ouvrir un nouvel exercice" shape="plus" href="!acc/years/new.php"} </p> {/foreach} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/reports/_header.tpl from [2032e3497e] to [ecdb90ea8a].
1 2 3 4 5 6 7 8 | <div class="year-header"> <nav class="tabs noprint"> <ul> {if isset($analytical) || $current == 'analytical_ledger'} <li><strong><a href="{$admin_url}acc/reports/projects.php">Projets</a></strong></li> {/if} {if $current == 'analytical_ledger'} | | | | | | > > > > > > > | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <div class="year-header"> <nav class="tabs noprint"> <ul> {if isset($analytical) || $current == 'analytical_ledger'} <li><strong><a href="{$admin_url}acc/reports/projects.php">Projets</a></strong></li> {/if} {if $current == 'analytical_ledger'} <li class="current"><a href="{$admin_url}acc/reports/ledger.php?{$criterias_query_no_compare}">Grand livre analytique</a></li> {else} <li{if $current == "graphs"} class="current"{/if}><a href="{$admin_url}acc/reports/graphs.php?{$criterias_query_no_compare}">Graphiques</a></li> <li{if $current == "trial_balance"} class="current"{/if}><a href="{$admin_url}acc/reports/trial_balance.php?{$criterias_query_no_compare}">Balance générale</a></li> <li{if $current == "journal"} class="current"{/if}><a href="{$admin_url}acc/reports/journal.php?{$criterias_query_no_compare}">Journal général</a></li> <li{if $current == "ledger"} class="current"{/if}><a href="{$admin_url}acc/reports/ledger.php?{$criterias_query_no_compare}">Grand livre</a></li> <li{if $current == "statement"} class="current"{/if}><a href="{$admin_url}acc/reports/statement.php?{$criterias_query}">Compte de résultat</a></li> <li{if $current == "balance_sheet"} class="current"{/if}><a href="{$admin_url}acc/reports/balance_sheet.php?{$criterias_query}">Bilan</a></li> {/if} </ul> {if $current == 'trial_balance'} <ul class="sub"> <li{if $sub_current == 'simple'} class="current"{/if}>{link href="?%s"|args:$criterias_query_no_compare label="Vue simplifiée"}</li> <li{if $sub_current != 'simple'} class="current"{/if}>{link href="?%s&simple=0"|args:$criterias_query_no_compare label="Vue comptable"}</li> </ul> {/if} </nav> <h2>{$config.nom_asso} — {$title}</h2> {if isset($analytical)} <h3>Projet : {$analytical.label}</h3> {/if} {if isset($year)} <p>Exercice : {$year.label} ({if $year.closed}clôturé{else}en cours{/if}, du {$year.start_date|date_short} au {$year.end_date|date_short}, généré le {$close_date|date_short})</p> {/if} {if !empty($allow_compare) && !empty($other_years) && empty($criterias['analytical'])} <form method="get" action="" class="noprint"> <fieldset> <legend>Comparer avec un autre exercice</legend> <p> {input type="select" name="compare_year" options=$other_years default=$criterias.compare_year} {button type="submit" label="Comparer" shape="right"} </p> <input type="hidden" name="year" value="{$year.id}" /> {if isset($analytical)} <input type="hidden" name="analytical" value="{$analytical.id}" /> {/if} </fieldset> </form> {/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> |
Modified src/templates/acc/reports/_statement.tpl from [9f0303b5cc] to [a5af3db236].
1 2 3 4 5 6 7 8 | <table class="statement"> <colgroup> <col width="50%" /> <col width="50%" /> </colgroup> <tbody> <tr> <td> | | | > | | | | | > < < < < < < < < < < < < | | < < < < < | | | | | < < | < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <table class="statement"> <colgroup> <col width="50%" /> <col width="50%" /> </colgroup> <tbody> <tr> <td> {include file="acc/reports/_statement_table.tpl" accounts=$statement.body_left caption=$statement.caption_left} </td> <td> {include file="acc/reports/_statement_table.tpl" accounts=$statement.body_right caption=$statement.caption_right} </td> </tr> </tbody> <tfoot> <tr> <td> <table> <tfoot> {foreach from=$statement.foot_left item="row"} <tr> <th>{$row.label}</th> <td class="money" width="10%">{$row.balance|raw|money:false}</td> {if $row.balance2 || $row.change} <td class="money" width="10%">{$row.balance2|raw|money:false}</td> <td class="money" width="10%">{$row.change|raw|money:false:true}</td> {/if} </tr> {/foreach} </tfoot> </table> </td> <td> <table> <tfoot> {foreach from=$statement.foot_right item="row"} <tr> <th>{$row.label}</th> <td class="money" width="10%">{$row.balance|raw|money:false}</td> {if $row.balance2 || $row.change} <td class="money" width="10%">{$row.balance2|raw|money:false}</td> <td class="money" width="10%">{$row.change|raw|money:false:true}</td> {/if} </tr> {/foreach} </tfoot> </table> </td> </tr> </tfoot> </table> |
Modified src/templates/acc/reports/_statement_table.tpl from [29eca66656] to [d1a6bebbcc].
︙ | ︙ | |||
11 12 13 14 15 16 17 | <td class="money" width="10%">{$year2->label_years()}</td> <td class="money" width="10%">Écart</td> </tr> </thead> {/if} <tbody> {foreach from=$accounts item="account"} | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <td class="money" width="10%">{$year2->label_years()}</td> <td class="money" width="10%">Écart</td> </tr> </thead> {/if} <tbody> {foreach from=$accounts item="account"} <tr class="compte{if isset($year2) && !$account.balance} disabled{/if}"> <td class="num"> {if !empty($year) && $account.id}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$year.id}">{$account.code}</a> {else}{$account.code} {/if} </td> <th>{$account.label}</th> <td class="money">{$account.balance|raw|money:false}</td> {if isset($year2)} <td class="money">{$account.balance2|raw|money:false}</td> <td class="money">{$account.change|raw|money:false:true}</td> {/if} </tr> {/foreach} </tbody> </table> |
Modified src/templates/acc/reports/balance_sheet.tpl from [14beea7ea6] to [9e34e6b155].
1 2 3 4 5 6 7 8 9 10 11 | {include file="admin/_head.tpl" title="Bilan" current="acc/years"} {include file="acc/reports/_header.tpl" current="balance_sheet" title="Bilan" allow_compare=true} {if $balance.sums.asset != $balance.sums.liability} <p class="alert block"> <strong>Le bilan n'est pas équilibré !</strong><br /> Vérifiez que vous n'avez pas oublié de reporter des soldes depuis le précédent exercice. </p> {/if} | > > < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {include file="admin/_head.tpl" title="Bilan" current="acc/years"} {include file="acc/reports/_header.tpl" current="balance_sheet" title="Bilan" allow_compare=true} <p class="help noprint">Le bilan représente une image de votre organisation : <strong>l'actif</strong> étant ce que l'organisation possède comme ressources (immeubles, comptes en banque, outillage, etc.), et <strong>le passif</strong> représente comment l'organisation a obtenu ces ressources (dettes, fonds de réserve, résultat…). En gros : à gauche = ce qu'on a, à droite = comment on l'a obtenu.</p> {if $balance.sums.asset != $balance.sums.liability} <p class="alert block"> <strong>Le bilan n'est pas équilibré !</strong><br /> Vérifiez que vous n'avez pas oublié de reporter des soldes depuis le précédent exercice. </p> {/if} {include file="acc/reports/_statement.tpl" statement=$balance} <p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/reports/ledger.tpl from [5c2ec98ec8] to [d881a4ac4b].
︙ | ︙ | |||
14 15 16 17 18 19 20 | {foreach from=$ledger item="account"} <details open="open"> <summary><h2 class="ruler"> {if !empty($criterias.analytical_only)} <?php $link = sprintf('%sacc/reports/trial_balance.php?analytical=%d&year=%d', $admin_url, $account->id, $account->id_year); ?> {else} | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | {foreach from=$ledger item="account"} <details open="open"> <summary><h2 class="ruler"> {if !empty($criterias.analytical_only)} <?php $link = sprintf('%sacc/reports/trial_balance.php?analytical=%d&year=%d', $admin_url, $account->id, $account->id_year); ?> {else} <?php $link = sprintf('%sacc/accounts/journal.php?id=%d&year=%d', $admin_url, $account->id, $account->id_year); ?> {/if} <a href="{$link}">{$account.code} — {$account.label}</a> </h2></summary> <table class="list"> <thead> <tr> |
︙ | ︙ |
Modified src/templates/acc/reports/statement.tpl from [b37ad29a93] to [ab2ea61147].
1 2 3 4 5 6 | {include file="admin/_head.tpl" title="Compte de résultat" current="acc/years"} {include file="acc/reports/_header.tpl" current="statement" title="Compte de résultat" allow_compare=true} {include file="acc/reports/_statement.tpl" statement=$general caption1="Charges" caption2="Produits"} | > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {include file="admin/_head.tpl" title="Compte de résultat" current="acc/years"} {include file="acc/reports/_header.tpl" current="statement" title="Compte de résultat" allow_compare=true} <p class="help noprint">Le compte de résultat indique les recettes (produits) et dépenses (charges), ainsi que le résultat réalisé.</p> {include file="acc/reports/_statement.tpl" statement=$general caption1="Charges" caption2="Produits"} {if !empty($volunteering.body_left) || !empty($volunteering.body_right)} <h2 class="ruler">Contributions en nature</h2> {include file="acc/reports/_statement.tpl" statement=$volunteering header=false caption1="Emplois des contributions volontaires en nature" caption2="Contributions volontaires en nature"} {/if} <p class="help">Toutes les écritures sont libellées en {$config.monnaie}.</p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/reports/trial_balance.tpl from [14933bb3ab] to [615125a575].
1 2 | {include file="admin/_head.tpl" title="Balance générale" current="acc/years"} | | > > > > > > > > > | < | | | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | {include file="admin/_head.tpl" title="Balance générale" current="acc/years"} {include file="acc/reports/_header.tpl" current="trial_balance" title="Balance générale" sub_current=$simple} <nav class="tabs"> {if !$simple} <p class="help block noprint"> Attention : cette vue présente le solde selon les normes comptables.<br /> Si le montant est <strong>positif</strong> c'est que le compte est <strong>débiteur</strong>.<br />Si le montant est <strong>négatif</strong> c'est que le compte est <strong>créditeur</strong>. </p> {/if} <table class="list"> <thead> <tr> <td>Numéro</td> <th>Compte</th> <td class="money">Total des débits</td> <td class="money">Total des crédits</td> <td class="money">Solde</td> </tr> </thead> <tbody> {foreach from=$balance item="account"} <tr class="{if $account.balance === 0}disabled{/if}"> <td class="num"> {if !empty($year)}<a href="{$admin_url}acc/accounts/journal.php?id={$account.id}&year={$year.id}">{$account.code}</a> {else}{$account.code} {/if} </td> <th>{$account.label}</th> <td class="money{if !$account.debit} disabled{/if}">{$account.debit|raw|money:false}</td> <td class="money{if !$account.credit} disabled{/if}">{$account.credit|raw|money:false}</td> <td class="money">{if $account.balance !== null}<b>{$account.balance|escape|money:false}</b>{/if}</td> </tr> {/foreach} </tbody> </table> <p class="help">Toutes les écritures sont libellées en {$config.monnaie}. Les lignes grisées correspondent aux comptes soldés.</p> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/search.tpl from [7cddfa9ea3] to [d1ddd847c2].
|
| | | 1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Recherche" current="acc" custom_js=['lib/query_builder.min.js']} <nav class="tabs"> <ul> <li class="current"><a href="{$self_url}">Recherche</a></li> <li><a href="saved_searches.php">Recherches enregistrées</a></li> </ul> </nav> |
︙ | ︙ |
Modified src/templates/acc/transactions/_lines_form.tpl from [25878c61c0] to [8a44524a1d].
︙ | ︙ | |||
18 19 20 21 22 23 24 | <td></td> </tr> </thead> <tbody> {foreach from=$lines key="k" item="line"} <tr> <td> | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <td></td> </tr> </thead> <tbody> {foreach from=$lines key="k" item="line"} <tr> <td> {input type="list" target="!acc/charts/accounts/selector.php?chart=%d"|args:$chart_id name="lines[account][]" default=$line.account} </td> <td class="money">{input type="money" name="lines[debit][]" default=$line.debit size=5}</td> <td class="money">{input type="money" name="lines[credit][]" default=$line.credit size=5}</td> <td>{input type="text" name="lines[reference][]" default=$line.reference size=10}</td> <td>{input type="text" name="lines[label][]" default=$line.label}</td> {if count($analytical_accounts) > 1} <td>{input default=$line.id_analytical type="select" name="lines[id_analytical][]" options=$analytical_accounts}</td> |
︙ | ︙ |
Modified src/templates/acc/transactions/details.tpl from [bc964cb764] to [07c0a89355].
︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 | Voir <a href="https://fossil.kd2.org/garradin/wiki?name=Changelog#1_0_1" target="_blank">cette page pour plus d'explications</a></p> <p>Les lignes erronées sont affichées en bas de cette page.</p> <p><em>(Ce message disparaîtra si vous modifiez l'écriture pour la corriger.)</em></p> </div> {/if} <dl class="describe"> {if $transaction.id_related} <dt>Écriture liée à</dt> | > > > > > > > > > > > > > > > > | < < | | < | > > > | | 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 | Voir <a href="https://fossil.kd2.org/garradin/wiki?name=Changelog#1_0_1" target="_blank">cette page pour plus d'explications</a></p> <p>Les lignes erronées sont affichées en bas de cette page.</p> <p><em>(Ce message disparaîtra si vous modifiez l'écriture pour la corriger.)</em></p> </div> {/if} <dl class="describe"> <dt>Libellé</dt> <dd><h2>{$transaction.label|escape|linkify_transactions}</h2></dd> <dt>Type</dt> <dd> {$transaction->getTypeName()} </dd> {if $transaction.type == $transaction::TYPE_DEBT || $transaction.type == $transaction::TYPE_CREDIT} <dt>Statut</dt> <dd> {if $transaction.status & $transaction::STATUS_PAID} <span class="confirm">{icon shape="check"}</span> Réglée {elseif $transaction.status & $transaction::STATUS_WAITING} <span class="alert">{icon shape="alert"}</span> En attente de règlement {/if} </dd> {/if} {if $transaction.id_related} <dt>Écriture liée à</dt> <dd><a class="num" href="?id={$transaction.id_related}">#{$transaction.id_related}</a> {if $transaction.type == $transaction::TYPE_DEBT || $transaction.type == $transaction::TYPE_CREDIT}(en règlement de){/if} </dd> {/if} {if count($related_transactions)} <dt>Écritures liées</dt> {foreach from=$related_transactions item="related"} <dd><a href="?id={$related.id}" class="num">#{$related.id}</a> — {$related.label} — {$related.date|date_short}</dd> {/foreach} {/if} <dt>Date</dt> <dd>{$transaction.date|date:'l j F Y (d/m/Y)'}</dd> <dt>Numéro pièce comptable</dt> <dd>{if $transaction.reference}{$transaction.reference}{else}-{/if}</dd> <dt>Exercice</dt> <dd> <a href="{$admin_url}acc/reports/ledger.php?year={$transaction.id_year}">{$tr_year.label}</a> | Du {$tr_year.start_date|date_short} au {$tr_year.end_date|date_short} | <strong>{if $tr_year.closed}Clôturé{else}En cours{/if}</strong> </dd> |
︙ | ︙ | |||
82 83 84 85 86 87 88 | <a href="{$admin_url}membres/fiche.php?id={$u.id}">{$u.identity}</a> {if $u.id_service_user}— en règlement d'une <a href="{$admin_url}services/user/?id={$u.id}&only={$u.id_service_user}">activité</a>{/if} </dd> {/foreach} {/if} <dt>Remarques</dt> | | | | | 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 | <a href="{$admin_url}membres/fiche.php?id={$u.id}">{$u.identity}</a> {if $u.id_service_user}— en règlement d'une <a href="{$admin_url}services/user/?id={$u.id}&only={$u.id_service_user}">activité</a>{/if} </dd> {/foreach} {/if} <dt>Remarques</dt> <dd>{if $transaction.notes}{$transaction.notes|escape|nl2br|linkify_transactions}{else}-{/if}</dd> </dl> <table class="list"> <thead> <tr> <td class="num">N° compte</td> <th>Compte</th> <td class="money">Débit</td> <td class="money">Crédit</td> <td>Libellé ligne</td> <td>Référence ligne</td> <td>Projet</td> </tr> </thead> <tbody> {foreach from=$transaction->getLinesWithAccounts(false) item="line"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$line.id_account}&year={$transaction.id_year}">{$line.account_code}</a></td> |
︙ | ︙ |
Modified src/templates/acc/transactions/edit.tpl from [e2fa247466] to [a98552bace].
︙ | ︙ | |||
29 30 31 32 33 34 35 | </fieldset> <fieldset> <legend>Informations</legend> <dl> {input type="date" name="date" label="Date" required=1 source=$transaction} {input type="text" name="label" label="Libellé" required=1 source=$transaction} | | | | | > | 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 | </fieldset> <fieldset> <legend>Informations</legend> <dl> {input type="date" name="date" label="Date" required=1 source=$transaction} {input type="text" name="label" label="Libellé" required=1 source=$transaction} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc." source=$transaction} </dl> <dl data-types="all-but-advanced"> {input type="money" name="amount" label="Montant" required=1 default=$amount} </dl> </fieldset> {foreach from=$types_details item="type"} <fieldset data-types="t{$type.id}"> <legend>{$type.label}</legend> {if $type.id == $transaction::TYPE_ADVANCED} {* Saisie avancée *} {include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart} {else} <dl> {foreach from=$type.accounts key="key" item="account"} <?php $selected = $types_accounts[$key] ?? null; ?> {input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1 default=$selected} {/foreach} </dl> {/if} </fieldset> {/foreach} <fieldset> <legend>Détails facultatifs</legend> <dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}"> {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$first_line.reference} </dl> <dl> {input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php" default=$linked_users} {input type="textarea" name="notes" label="Remarques" rows=2 cols=30 source=$transaction} {input type="number" name="id_related" label="Lier à l'écriture numéro" source=$transaction help="Indiquer ici un numéro d'écriture pour faire le lien par exemple avec une dette"} </dl> <dl data-types="all-but-advanced"> {if count($analytical_accounts) > 1} {input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$first_line.id_analytical} {/if} </dl> </fieldset> |
︙ | ︙ |
Modified src/templates/acc/transactions/new.tpl from [757fa62572] to [5d2788881b].
︙ | ︙ | |||
19 20 21 22 23 24 25 | {foreach from=$types_details item="type"} <dd class="radio-btn"> {input type="radio" name="type" value=$type.id source=$transaction label=null} <label for="f_type_{$type.id}"> <div> <h3>{$type.label}</h3> {if !empty($type.help)} | | | | | | > > > | > | > | 19 20 21 22 23 24 25 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 | {foreach from=$types_details item="type"} <dd class="radio-btn"> {input type="radio" name="type" value=$type.id source=$transaction label=null} <label for="f_type_{$type.id}"> <div> <h3>{$type.label}</h3> {if !empty($type.help)} <p class="help">{$type.help}</p> {/if} </div> </label> </dd> {/foreach} </dl> </fieldset> <fieldset> <legend>Informations</legend> <dl> {input type="date" name="date" label="Date" required=1 source=$transaction} {input type="text" name="label" label="Libellé" required=1 source=$transaction} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc." source=$transaction} </dl> <dl data-types="all-but-advanced"> {input type="money" name="amount" label="Montant" required=1 default=$amount} </dl> </fieldset> {foreach from=$types_details item="type"} <fieldset data-types="t{$type.id}"> <legend>{$type.label}</legend> {if $type.id == $transaction::TYPE_ADVANCED} {* Saisie avancée *} {include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart} {else} <dl> {foreach from=$type.accounts key="key" item="account"} <?php $selected = $types_accounts[$key] ?? null; ?> {input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1 default=$selected} {/foreach} </dl> {/if} </fieldset> {/foreach} <fieldset> <legend>Détails facultatifs</legend> <dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}"> {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$transaction->payment_reference()} </dl> <dl> {input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php"} {input type="textarea" name="notes" label="Remarques" rows=4 cols=30} </dl> <dl data-types="t{$transaction::TYPE_ADVANCED}"> {input type="number" name="id_related" label="Lier à l'écriture numéro" source=$transaction help="Indiquer ici un numéro d'écriture pour faire le lien par exemple avec une dette"} </dl> <dl data-types="all-but-advanced"> {if count($analytical_accounts) > 1} {input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$id_analytical} {/if} </dl> </fieldset> <p class="submit"> {csrf_field key="acc_transaction_new"} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> <script type="text/javascript" async="async"> let is_new = {if null !== $transaction->type}false{else}true{/if}; {literal} window.addEventListener('load', () => { g.script('scripts/accounting.js', () => { initTransactionForm(is_new && !$('.block').length); }); }); </script> {/literal} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/transactions/payoff.tpl from [9376e00f53] to [aaa45e359f].
︙ | ︙ | |||
9 10 11 12 13 14 15 | <input type="hidden" name="{$payoff_for.form_account_name}[{$payoff_for.id_account}]" value="-" /> <fieldset> <legend>{if $payoff_for.type == $transaction::TYPE_DEBT}Règlement de dette{else}Règlement de créance{/if}</legend> <dl> <dt>Écriture d'origine</dt> <dd>{link class="num" href="!acc/transactions/details.php?id=%d"|args:$payoff_for.id label="#%d"|args:$payoff_for.id}</dd> {input type="checkbox" name="mark_paid" value="1" default="1" label="Marquer comme payée"} | | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <input type="hidden" name="{$payoff_for.form_account_name}[{$payoff_for.id_account}]" value="-" /> <fieldset> <legend>{if $payoff_for.type == $transaction::TYPE_DEBT}Règlement de dette{else}Règlement de créance{/if}</legend> <dl> <dt>Écriture d'origine</dt> <dd>{link class="num" href="!acc/transactions/details.php?id=%d"|args:$payoff_for.id label="#%d"|args:$payoff_for.id}</dd> {input type="checkbox" name="mark_paid" value="1" default="1" label="Marquer comme payée"} {input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$payoff_targets,$chart_id name=$payoff_for.form_target_name label="Compte de règlement" required=1} </dl> </fieldset> <fieldset> <legend>Informations</legend> <dl> {input type="date" name="date" label="Date" required=1 source=$transaction} {input type="text" name="label" label="Libellé" required=1 source=$transaction} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."} </dl> <dl data-types="all-but-advanced"> {input type="money" name="amount" label="Montant" required=1 default=$amount} </dl> </fieldset> <fieldset> <legend>Détails facultatifs</legend> <dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}"> {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." source=$transaction} </dl> <dl> {input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php"} {input type="textarea" name="notes" label="Remarques" rows=4 cols=30} </dl> <dl data-types="all-but-advanced"> {if count($analytical_accounts) > 1} {input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$id_analytical} {/if} </dl> |
︙ | ︙ |
Modified src/templates/acc/transactions/service_user.tpl from [46d5909f35] to [a9660dff87].
1 2 3 | {include file="admin/_head.tpl" title="Écritures liées à une inscription" current="acc/accounts"} <nav class="tabs"> | | | > > > > > > | | | | | | | | < | | | | | | | | < | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | {include file="admin/_head.tpl" title="Écritures liées à une inscription" current="acc/accounts"} <nav class="tabs"> {linkbutton href="!membres/fiche.php?id=%d"|args:$user_id label="Retour à la fiche membre" shape="left"} {linkbutton href="!services/user/payment.php?id=%d"|args:$service_user_id label="Nouveau règlement" shape="plus" target="_dialog"} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} {linkbutton href="!services/user/link.php?id=%d"|args:$service_user_id label="Lier à une écriture" shape="check" target="_dialog"} {/if} </nav> {if empty($balance)} <p class="alert block">Aucune écriture n'est liée à cette inscription.</p> {else} {include file="acc/reports/_journal.tpl"} <h2 class="ruler">Solde des comptes</h2> <table class="list"> <thead> <tr> <td>Numéro</td> <th>Compte</th> <td class="money">Solde</td> </tr> </thead> <tbody> {foreach from=$balance item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td> <th>{$account.label}</th> <td class="money">{$account.balance|raw|money:false}</td> </tr> {/foreach} </tbody> </table> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/transactions/user.tpl from [4c90bde47d] to [ebafbd05ec].
︙ | ︙ | |||
22 23 24 25 26 27 28 | </form> <p class="block help">Cette liste représente le solde des comptes uniquement pour les écritures liées à ce membre.</p> <table class="list"> <thead> <tr> | | | < | < | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | </form> <p class="block help">Cette liste représente le solde des comptes uniquement pour les écritures liées à ce membre.</p> <table class="list"> <thead> <tr> <td class="num">Numéro</td> <th>Compte</th> <td class="money">Solde</td> </tr> </thead> <tbody> {foreach from=$balance item="account"} <tr> <td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$account.id}">{$account.code}</a></td> <th>{$account.label}</th> <td class="money">{$account.balance|raw|money}</td> </tr> {/foreach} </tbody> </table> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/years/balance.tpl from [512dbaebb8] to [e9fb58eac7].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {include file="admin/_head.tpl" title="Balance d'ouverture" current="acc/years"} {form_errors} {if $year->countTransactions()} <p class="block alert"> <strong>Attention !</strong> Cet exercice a déjà des écritures, peut-être avez-vous déjà renseigné la balance d'ouverture ? </p> {/if} <form method="post" action="{$self_url}"> <fieldset> <legend>Exercice : « {$year.label} » du {$year.start_date|date_short} au {$year.end_date|date_short}</legend> {if !$year_selected} <dl> | > > > > > > | > | > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | {include file="admin/_head.tpl" title="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 !</strong> Cet exercice a déjà des écritures, peut-être avez-vous déjà renseigné la balance d'ouverture ? </p> {/if} <form method="post" action="{$self_url}"> <fieldset> <legend>Exercice : « {$year.label} » 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"} <option value="{$year.id}"{if $year.id == $_GET.from} selected="selected"{/if} data-closed="{$year.closed}">{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short} ({if $year.closed}clôturé{else}en cours{/if})</option> {/foreach} </select> </dd> <dd class="hidden warn-not-closed"> <p class="alert block">Attention l'exercice sélectionné n'est pas clôturé !<br />Si vous modifiez cet exercice après avoir validé cette balance d'ouverture, celle-ci pourrait ne plus correspondre au bilan de l'exercice précédent !</p> </dd> </dl> {literal} <script type="text/javascript" async="async"> let s = document.querySelector('#f_from_year'); const checkOpen = function() { 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> {/if} <table class="list transaction-lines"> <thead> <tr> {if $chart_change} <td>Ancien compte</td> <th>Nouveau compte</th> {else} |
︙ | ︙ | |||
51 52 53 54 55 56 57 | <td> {$line.code} — {$line.label} <input type="hidden" name="lines[code][]" value="{$line.code}" /> <input type="hidden" name="lines[label][]" value="{$line.label}" /> </td> {/if} <th> | | < > > > > > | > > | | 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 | <td> {$line.code} — {$line.label} <input type="hidden" name="lines[code][]" value="{$line.code}" /> <input type="hidden" name="lines[label][]" value="{$line.label}" /> </td> {/if} <th> {input type="list" target="!acc/charts/accounts/selector.php?chart=%d"|args:$year.id_chart name="lines[account][]" default=$line.account} </th> <td>{input type="money" name="lines[debit][]" default=$line.debit size=5}</td> <td>{input type="money" name="lines[credit][]" default=$line.credit size=5}</td> <td>{button label="Enlever la ligne" shape="minus" min="1" name="remove_line"}</td> </tr> {/foreach} </tbody> <tfoot> <tr> <th>Total</th> {if $chart_change} <td></td> {/if} <td>{input type="money" name="debit_total" readonly="readonly" tabindex="-1" }</td> <td>{input type="money" name="credit_total" readonly="readonly" tabindex="-1" }</td> <td>{button label="Ajouter une ligne" shape="plus"}</td> </tr> </tfoot> </table> <dl> {input type="checkbox" name="appropriation" value="1" checked="checked" label="Affecter automatiquement le résultat (conseillé)" help="Si cette case est cochée, le résultat sera automatiquement affecté aux réserves s'il est excédentaire"} {/if} </fieldset> <p class="submit"> {if null === $previous_year} {button type="submit" name="next" label="Continuer" shape="right" class="main"} - ou - {if $_GET.from} {linkbutton shape="reset" href="!acc/years/appropriation.php?id=%d&from=%d"|args:$year.id,$_GET.from label="Passer cet étape"} {else} {linkbutton shape="reset" href="!acc/years/" label="Passer cet étape"} {/if} <i class="help">(Il sera toujours possible de reprendre la balance d'ouverture plus tard.)</i> {else} {csrf_field key=$csrf_key} {if $previous_year} <input type="hidden" name="from_year" value="{$previous_year.id}" /> {else} <input type="hidden" name="from_year" value="" /> {/if} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} |
︙ | ︙ |
Modified src/templates/acc/years/export.tpl from [640274763b] to [46e229f140].
1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="Export d'exercice" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> <nav class="tabs"> <ul> {if !$year.closed} | | | | > > > < > | > > > > > > > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | {include file="admin/_head.tpl" title="Export d'exercice" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</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> <dl> <dt>Format d'export</dt> {input type="radio" name="format" value="ods" default="ods" label="LibreOffice" help="également lisible par Excel, Google Docs, etc."} {input type="radio" name="format" value="csv" label="CSV"} {if CALC_CONVERT_COMMAND} {input type="radio" name="format" value="xlsx" label="Excel"} {/if} <dt>Type d'export</dt> {foreach from=$types key="type" item="info"} {input type="radio-btn" name="type" value=$type label=$info.label help=$info.help default="full"} <dd class="help example"> Exemple : <table class="list auto"> {foreach from=$examples[$type] item="row"} <tr> {foreach from=$row item="v"} <td>{$v}</td> {/foreach} </tr> {/foreach} </table> </dd> {/foreach} </dl> </fieldset> <p class="submit"> <input type="hidden" name="year" value="{$year.id}" /> {button type="submit" name="load" label="Télécharger" shape="download" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/acc/years/import.tpl from [1a8a438acd] to [5d548f284e].
1 2 3 4 5 6 7 8 9 | {include file="admin/_head.tpl" title="Importer des écritures" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> <nav class="tabs"> <ul> | | | < | | > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | > > > > > > > > | < < < < > > > > > > > > > > < > | > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | {include file="admin/_head.tpl" title="Importer des écritures" current="acc/years"} <nav class="acc-year"> <h4>Exercice sélectionné :</h4> <h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3> </nav> <nav class="tabs"> <ul> <li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li> <li><a href="{$admin_url}acc/years/export.php?year={$year.id}">Export</a></li> </ul> </nav> {form_errors} {if $type_name && $csv->loaded()} <form method="post" action="{$self_url}"> {include file="common/_csv_match_columns.tpl"} <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="cancel" value="1" label="Annuler" shape="left"} {button type="submit" name="assign" label="Continuer" class="main" shape="right"} </p> </form> {elseif $type_name} <form method="post" action="{$self_url}" enctype="multipart/form-data"> <fieldset> <legend>Import d'écritures</legend> <dl> <dt> Type d'import </dt> <dd> {$type_name} </dd> {if CALC_CONVERT_COMMAND} {input type="file" name="file" label="Fichier à importer" accept=".ods,application/vnd.oasis.opendocument.spreadsheet,.xls,application/vnd.ms-excel,.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,.csv,text/csv,application/csv" required=true help="Formats acceptés : CSV, LibreOffice Calc, ou Excel"} {else} {input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=true} {/if} {include file="common/_csv_help.tpl" csv=$csv} {input type="checkbox" name="ignore_ids" value="1" label="Ne pas tenir compte des numéros d'écritures" help="Si coché, les écritures importées seront créées, même si un numéro d'écriture est fourni et qu'il existe déjà. Cela peut mener à avoir des écritures en doublon."} </dl> <p class="help block"> - Les lignes comportant un numéro d'écriture existant mettront à jour les écritures correspondant à ces numéros.<br /> - Les lignes comportant un numéro inexistant renverront une erreur.<br /> - Les lignes dont le numéro est vide créeront de nouvelles écritures.<br /> - Si le fichier comporte des écritures dont la date est en dehors de l'exercice courant, elles seront ignorées. </p> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {linkbutton href="?year=%d"|args:$year.id label="Annuler" shape="left"} {button type="submit" name="load" label="Importer" shape="upload" class="main"} </p> </form> {else} <form method="get" action="{$self_url_no_qs}"> <fieldset> <legend>Import d'écritures</legend> <dl> <dt><label for="f_type_garradin">Type de fichier à importer</label></dt> {input type="radio-btn" name="type" value="simple" label="Simplifié (comptabilité de trésorerie)" default="simple" help="Chaque ligne représente une écriture, comme dans un cahier. Les écritures avancées ne peuvent pas être importées dans ce format."} <dd class="help example"> Exemple : <table class="list auto"> {foreach from=$examples.simple item="row"} <tr> {foreach from=$row item="v"} <td>{$v}</td> {/foreach} </tr> {/foreach} </table> </dd> {input type="radio-btn" name="type" value="grouped" label="Complet groupé (comptabilité d'engagement)" help="Permet d'avoir des écritures avancées. Les 7 premières colonnes de chaque ligne sont vides pour indiquer les lignes suivantes de l'écriture."} <dd class="help example"> Exemple : <table class="list auto"> {foreach from=$examples.grouped item="row"} <tr> {foreach from=$row item="v"} <td>{$v}</td> {/foreach} </tr> {/foreach} </table> </dd> </dl> </fieldset> <p class="submit"> <input type="hidden" name="year" value="{$year.id}" /> {button type="submit" label="Continuer" shape="right" class="main"} </p> </form> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/acc/years/index.tpl from [3c1158f29c] to [b2a2e93dd9].
1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Exercices" current="acc/years"} <nav class="tabs"> <aside> {linkbutton shape="search" href="!acc/search.php" label="Recherche"} </aside> <ul> <li class="current"><a href="{$self_url}">Exercices</a></li> | > > > < < < > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {include file="admin/_head.tpl" title="Exercices" current="acc/years"} <nav class="tabs"> <aside> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton shape="plus" href="!acc/years/new.php" label="Nouvel exercice"} {/if} {linkbutton shape="search" href="!acc/search.php" label="Recherche"} </aside> <ul> <li class="current"><a href="{$self_url}">Exercices</a></li> <li><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li> </ul> </nav> {if $_GET.msg == 'IMPORT'} <p class="block confirm"> L'import s'est bien déroulé. </p> {/if} {if $_GET.msg == 'OPEN'} <p class="block error"> Il n'existe aucun exercice ouvert. {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures. {/if} |
︙ | ︙ | |||
63 64 65 66 67 68 69 | | <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)} | | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | | <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> |
︙ | ︙ |
Modified src/templates/acc/years/new.tpl from [c8bd7f28b5] to [53482eebcb].
1 2 | {include file="admin/_head.tpl" title="Commencer un exercice" current="acc/years"} | | | < < | < | > > | | > | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | {include file="admin/_head.tpl" title="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 créer un nouvel exercice ci-dessous.</p> {/if} {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>Commencer un nouvel exercice</legend> <dl> {input type="select_groups" options=$charts name="id_chart" label="Plan comptable" required=true source=$year} <dd class="help"> Il ne sera pas possible de changer le plan comptable une fois l'exercice ouvert.<br /> Il ne sera également pas possible de modifier ou supprimer un compte du plan comptable si le compte est utilisé dans un autre exercice déjà clôturé.<br /> Si vous souhaitez modifier le plan comptable pour ce nouvel exercice, il est recommandé de créer un nouveau plan comptable, recopié à partir de l'ancien plan comptable. Ainsi tous les comptes seront modifiables et supprimables. </dd> <dd class="help">{linkbutton shape="settings" label="Gestion des plans comptables" href="!acc/charts/"}</dd> {input type="text" name="label" label="Libellé" required=true source=$year} {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="admin/_foot.tpl"} |
Modified src/templates/admin/_head.tpl from [73842c1113] to [ebd929ce44].
1 2 3 4 5 6 | <?php if (!isset($current)) { $current = null; } ?> <!DOCTYPE html> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php if (!isset($current)) { $current = null; } ?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr" class="{if $dialog}dialog{/if}" data-version="{$version_hash}" data-url="{$admin_url}"> <head> <meta charset="utf-8" /> <meta name="v" content="{$version_hash}" /> <title>{$title}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" /> <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script> |
︙ | ︙ | |||
54 55 56 57 58 59 60 | </figure> {/if} <ul> {if $session->isLogged()} <?php $current_parent = substr($current, 0, strpos($current, '/')); ?> | | < | | | | | | | | < | | | 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 | </figure> {/if} <ul> {if $session->isLogged()} <?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}"><b data-icn="{icon html=false shape="home"}"></b><span>Accueil</span></a></h3> {if !empty($plugins_menu)} <ul> {foreach from=$plugins_menu key="key" item="html"} <li{if $current == $key} class="current"{/if}>{$html|raw}</li> {/foreach} </ul> {/if} </li> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_READ)} <li class="member list{if $current == 'membres'} current{elseif $current_parent == 'membres'} current_parent{/if}"><h3><a href="{$admin_url}membres/"><b data-icn="{icon html=false shape="users"}"></b></b><span>Membres</span></a></h3> <ul> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} <li class="member new{if $current == 'membres/ajouter'} current{/if}"><a href="{$admin_url}membres/ajouter.php">Ajouter</a></li> {/if} <li class="{if $current == 'membres/services'} current{/if}"><a href="{$admin_url}services/">Activités & cotisations</a></li> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} <li class="member message{if $current == 'membres/message'} current{/if}"><a href="{$admin_url}membres/message_collectif.php">Message collectif</a></li> {/if} </ul> </li> {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)} <li class="{if $current == 'acc'} current{elseif $current_parent == 'acc'} current_parent{/if}"><h3><a href="{$admin_url}acc/"><b data-icn="{icon html=false shape="money"}"></b><span>Comptabilité</span></a></h3> <ul> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} <li class="{if $current == 'acc/new'} current{/if}"><a href="{$admin_url}acc/transactions/new.php">Saisie</a></li> {/if} <li class="{if $current == 'acc/accounts'} current{/if}"><a href="{$admin_url}acc/accounts/">Comptes</a></li> <li class="{if $current == 'acc/simple'} current{/if}"><a href="{$admin_url}acc/accounts/simple.php">Suivi des écritures</a></li> <li class="{if $current == 'acc/years'} current{/if}"><a href="{$admin_url}acc/years/">Exercices & rapports</a></li> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} <li class="{if $current == 'acc/charts'} current{/if}"><a href="{$admin_url}acc/charts/">Plans comptables</a></li> {/if} </ul> </li> {/if} {if $session->canAccess($session::SECTION_DOCUMENTS, $session::ACCESS_READ)} <li class="{if $current == 'docs'} current{elseif $current_parent == 'docs'} current_parent{/if}"><h3><a href="{$admin_url}docs/"><b data-icn="{icon html=false shape="folder"}"></b><span>Documents</span></a></h3> </li> {/if} {if $session->canAccess($session::SECTION_WEB, $session::ACCESS_READ)} <li class="{if $current == 'web'} current{elseif $current_parent == 'web'} current_parent{/if}"><h3><a href="{$admin_url}web/"><b data-icn="{icon html=false shape="globe"}"></b><span>Site web</span></a></h3> </li> {/if} {if $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN)} <li class="main config{if $current == 'config'} current{elseif $current_parent == 'config'} current_parent{/if}"><h3><a href="{$admin_url}config/"><b data-icn="{icon html=false shape="settings"}"></b><span>Configuration</span></a></h3> {/if} <li class="{if $current == 'me'} current{elseif $current_parent == 'me'} current_parent{/if}"><h3><a href="{$admin_url}me/"><b data-icn="{icon html=false shape="user"}"></b><span> Mes infos personnelles</span></a></h3> <ul> <li{if $current == 'me/services'} class="current"{/if}><a href="{$admin_url}me/services.php">Mes activités & cotisations</a></li> </ul> </li> {if !defined('Garradin\LOCAL_LOGIN') || !LOCAL_LOGIN} <li class="logout"><h3><a href="{$admin_url}logout.php"><b data-icn="{icon html=false shape="logout"}"></b><span>Déconnexion</span></a></h3></li> {/if} {if $help_url} <li> <h3><a href="{$help_url}" target="_blank"><b data-icn="{icon html=false shape="help"}"></b><span>Aide</span></a></h3> </li> {/if} {elseif !defined('Garradin\INSTALL_PROCESS')} <li><a href="{if $config.site_asso}{$config.site_asso}{else}{$www_url}{/if}">← Retour au site</a></li> <li><a href="{$admin_url}">Connexion</a> <ul> |
︙ | ︙ |
Modified src/templates/admin/config/_menu.tpl from [3e2e12b62f] to [0f21a24c00].
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <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 == 'advanced'} <ul class="sub"> <li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li> <li{if $sub_current == 'sql'} class="current"{/if}><a href="{$admin_url}config/advanced/sql.php">SQL</a></li> {if ENABLE_TECH_DETAILS} <li{if $sub_current == 'errors'} class="current"{/if}><a href="{$admin_url}config/advanced/errors.php">Journal d'erreurs</a></li> {/if} </ul> {/if} </nav> | > > > > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <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 == 'advanced'} <ul class="sub"> <li{if !$sub_current} class="current"{/if}><a href="{$admin_url}config/advanced/">Fonctions avancées</a></li> <li{if $sub_current == 'api'} class="current"{/if}><a href="{$admin_url}config/advanced/api.php">API</a></li> <li{if $sub_current == 'sql'} class="current"{/if}><a href="{$admin_url}config/advanced/sql.php">SQL</a></li> {if ENABLE_TECH_DETAILS} <li{if $sub_current == 'errors'} class="current"{/if}><a href="{$admin_url}config/advanced/errors.php">Journal d'erreurs</a></li> {if SQL_DEBUG} <li{if $sub_current == 'sql_debug'} class="current"{/if}><a href="{$admin_url}config/advanced/sql_debug.php">Journal SQL</a></li> {/if} {/if} </ul> {/if} </nav> |
Added src/templates/admin/config/advanced/api.tpl version [c8908adec2].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | {include file="admin/_head.tpl" title="API" current="config" custom_css=["config.css"]} {include file="admin/config/_menu.tpl" current="advanced" sub_current="api"} {form_errors} {if count($list)} <form method="post" action=""> <p class="actions"> {csrf_field key=$csrf_key} {button name="delete" value=1 type="submit" label="Supprimer l'identifiant sélectionné" shape="delete"} </p> <table class="list"> <thead> <tr> <td></td> <th>Description</th> <td>Identifiant</td> <td>Accès</td> <td>Création</td> <td>Dernière utilisation</td> </tr> </thead> <tbody> {foreach from=$list item="c"} <tr> <td class="check"> {input type="radio" name="id" value=$c.id} </td> <th>{$c.label}</th> <td>{$c.key}</td> <td class="help">{$access_levels[$c.access_level]}</td> <td>{$c.created|date_short}</td> <td>{if $c.last_use}{$c.last_use|date}{else}-{/if}</td> </tr> {/foreach} </tbody> </table> </form> {/if} <form method="post" action=""> <fieldset> <legend>Créer un nouvel identifiant</legend> <p class="help"> Cet identifiant vous permettra de faire des requêtes vers l'API, pour modifier ou récupérer les informations de votre association.<br /> {linkbutton shape="help" label="Documentation de l'API" href="%swiki?name=API"|args:$website} </p> <dl> {input type="text" name="label" label="Description" required=true} {input type="text" name="key" label="Identifiant" help="Seules les lettres minuscules, chiffres et tirets bas sont acceptés." pattern="[a-z0-9_]+" required=true default=$default_key} {input type="text" label="Mot de passe" default=$secret readonly="readonly" help="Ce mot de passe ne sera plus affiché, il est conseillé de le copier/coller et l'enregistrer de votre côté." name="secret" copy=true} {input type="select" required=true label="Autorisation d'accès" options=$access_levels name="access_level"} </dl> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="add" label="Créer" shape="plus" class="main"} </p> </fieldset> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/advanced/errors.tpl from [39b2ff6b31] to [990ead8274].
︙ | ︙ | |||
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | {if !count($errors)} <p class="block alert">Aucune erreur n'a été trouvée dans le journal error.log</p> {else} <table class="list"> <thead> <tr> <th>Réf.</th> <td>Erreur</td> <td>Occurences</td> <td>Dernière fois</td> <td></td> </tr> </thead> <tbody> {foreach from=$errors item=error key=ref} <tr> <th><a href="?type=errors&id={$ref}">{$ref}</a></th> <td> {$error.message}<br /> <tt>{$error.source}</tt> </td> <td>{$error.count}</td> <td>{$error.last_seen|date}</td> <td class="actions"> | > > | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | {if !count($errors)} <p class="block alert">Aucune erreur n'a été trouvée dans le journal error.log</p> {else} <table class="list"> <thead> <tr> <th>Réf.</th> <td>Site</td> <td>Erreur</td> <td>Occurences</td> <td>Dernière fois</td> <td></td> </tr> </thead> <tbody> {foreach from=$errors item=error key=ref} <tr> <th><a href="?type=errors&id={$ref}">{$ref}</a></th> <td>{$error.hostname}</td> <td> {$error.message}<br /> <tt>{$error.source}</tt> </td> <td>{$error.count}</td> <td>{$error.last_seen|date}</td> <td class="actions"> |
︙ | ︙ |
Added src/templates/admin/config/advanced/sql_debug.tpl version [6b74582aae].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | {include file="admin/_head.tpl" title="Journal SQL" current="config" custom_css=["config.css"]} {include file="admin/config/_menu.tpl" current="advanced" sub_current="sql_debug"} {if isset($debug)} <table class="list multi"> <thead> <tr> <th>T.</th> <td>Durée</td> <td>Trace</td> <td>SQL</td> </tr> </thead> {foreach from=$debug.list item="row"} <tbody> <tr> <th><?=round($row->time / 1000, 2)?></th> <td> <?php $d = round($row->duration / 1000, 2); ?> {if $d > 0.4} <h3 class="error">{$d}</h3> {else} {$d} {/if} </td> <td><pre>{$row.trace}</pre></td> </tr> <tr> <td colspan="3"><h4>Query <a href="sql.php?query={$row.sql|escape:'url'}">[replay]</a></h4><pre>{$row.sql}</pre></td> </tr> <tr> <td colspan="3"><h4>EXPLAIN:</h4><pre>{$row.explain}</pre></td> </tr> </tbody> {/foreach} </table> {elseif isset($list)} <p class="help"> Liste des pages consultées ayant mené à des requêtes SQL. </p> {if !count($list)} <p class="block alert">Aucune requête n'a été trouvée dans le log</p> {else} <table class="list"> <thead> <tr> <th>ID</th> <td>Date</td> <td>Script</td> <td>Membre connecté</td> <td>Durée totale des requêtes</td> <td>Nombre de requêtes</td> </tr> </thead> <tbody> {foreach from=$list item="row"} <tr> <th><a href="?id={$row.id}">{$row.id}</a></th> <td>{$row.date|date_format}</td> <td>{$row.script}</td> <td>{$row.user}</td> <td class="num"><?=$row->duration/1000?> ms</td> <td class="num">{$row.count}</td> </tr> {/foreach} </tbody> </table> {/if} {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/backup/index.tpl from [e84c2fbbb2] to [f2dd4c1906].
1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Sauvegardes" current="config"} {include file="admin/config/_menu.tpl" current="backup"} {include file="admin/config/backup/_menu.tpl" current="index"} <fieldset> <legend>Politique de sauvegardes</legend> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {include file="admin/_head.tpl" title="Sauvegardes" current="config"} {include file="admin/config/_menu.tpl" current="backup"} {include file="admin/config/backup/_menu.tpl" current="index"} <fieldset> <legend>Politique de sauvegardes</legend> {if !$config.frequence_sauvegardes} <p class="help block"> Les <a href="save.php">sauvegardes automatiques</a> sont désactivées. Il est recommandé de les activer pour pouvoir revenir en arrière en cas de problème majeur. Attention, cela ne dispense pas de réaliser des sauvegardes régulières sur votre ordinateur. </p> {/if} <p class="help"> En cas de problème sur le serveur (plantage, dysfonctionnement du disque dur, incendie, etc.) vous pourriez perdre vos données.<br /> |
︙ | ︙ |
Modified src/templates/admin/config/backup/restore.tpl from [cb8b7573a9] to [03715cceb0].
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <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. {/if} {elseif $ok == 'remove'}La sauvegarde a été supprimée. {/if} </p> {/if} <form method="post" action="{$self_url_no_qs}" enctype="multipart/form-data"> <fieldset> <legend><label for="f_file">Restaurer depuis un fichier de sauvegarde</label></legend> <p class="block alert"> Attention, l'intégralité des données courantes seront effacées et remplacées par celles contenues dans le fichier fourni. </p> <p class="help"> Une sauvegarde des données courantes sera effectuée avant le remplacement, en cas de besoin d'annuler cette restauration. </p> <dl> | > > > > > | | 15 16 17 18 19 20 21 22 23 24 25 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 | <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} {elseif $ok == 'remove'}La sauvegarde a été supprimée. {/if} </p> {/if} <form method="post" action="{$self_url_no_qs}" enctype="multipart/form-data"> <fieldset> <legend><label for="f_file">Restaurer depuis un fichier de sauvegarde</label></legend> <p class="block alert"> Attention, l'intégralité des données courantes seront effacées et remplacées par celles contenues dans le fichier fourni. </p> <p class="help"> Une sauvegarde des données courantes sera effectuée avant le remplacement, en cas de besoin d'annuler cette restauration. </p> <dl> {input type="file" name="file" label="Fichier de sauvegarde à restaurer" required=true} </dl> <p class="submit"> {csrf_field key="backup_restore"} {button type="submit" name="restore_file" label="Restaurer depuis le fichier sélectionné" shape="upload" class="main"} </p> {if $code && ($code == Sauvegarde::INTEGRITY_FAIL && ALLOW_MODIFIED_IMPORT)} <p> |
︙ | ︙ | |||
71 72 73 74 75 76 77 | <td>Date</td> <td>Version</td> <td></td> </tr> </thead> {foreach from=$list item="backup"} <tr> | | | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | <td>Date</td> <td>Version</td> <td></td> </tr> </thead> {foreach from=$list item="backup"} <tr> <td class="check">{if $backup.can_restore}{input type="radio" name="selected" value=$backup.filename}{/if}</td> <th><label for="f_selected_{$backup.filename}">{$backup.name}</label></th> <td>{$backup.size|size_in_bytes}</td> <td>{$backup.date|date_short:true}</td> <td>{$backup.version}{if !$backup.can_restore} — <span class="alert">Version trop ancienne pour pouvoir être restaurée</span>{/if}</td> <td class="actions"> {linkbutton href="?download=%s"|args:$backup.filename label="Télécharger" shape="download"} </td> |
︙ | ︙ |
Modified src/templates/admin/config/index.tpl from [6e42e917da] to [265f30cf3e].
︙ | ︙ | |||
12 13 14 15 16 17 18 | <form method="post" action="{$self_url}"> <fieldset> <legend>Garradin</legend> <dl> <dt>Version installée</dt> | > > | > > > > | | 12 13 14 15 16 17 18 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="{$self_url}"> <fieldset> <legend>Garradin</legend> <dl> <dt>Version installée</dt> <dd>{$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 !<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} {linkbutton shape="export" href="upgrade.php" label="Mettre à jour"} {else} {linkbutton shape="export" href=$garradin_website label="Télécharger la mise à jour" target="_blank"} {/if} </p></dd> {/if} {if ENABLE_TECH_DETAILS} <dt>Informations système</dt> <dd class="help"> Version PHP : {$php_version}<br /> |
︙ | ︙ | |||
69 70 71 72 73 74 75 76 77 | <p class="submit"> {csrf_field key="config"} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} | > > > > > > | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | <p class="submit"> {csrf_field key="config"} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {if ENABLE_TECH_DETAILS} <script type="text/javascript" async="async"> fetch(g.admin_url + 'config/?check_version'); </script> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/plugins.tpl from [87c21ad0af] to [c2d1b65842].
︙ | ︙ | |||
50 51 52 53 54 55 56 | <td> <a href="{$plugin.url}" onclick="return !window.open(this.href);">{$plugin.auteur}</a> </td> <td> {$plugin.version} </td> <td class="actions"> | < < < | < > | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | <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="!plugin/%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} |
︙ | ︙ | |||
83 84 85 86 87 88 89 | <dl> {foreach from=$liste_telecharges item="plugin" key="id"} <dt> <input type="radio" name="plugin" value="{$id}" id="f_{$id}" /> <label for="f_{$id}"> {$plugin.nom} </label> | | | 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | <dl> {foreach from=$liste_telecharges item="plugin" key="id"} <dt> <input type="radio" name="plugin" value="{$id}" id="f_{$id}" /> <label for="f_{$id}"> {$plugin.nom} </label> <small>(version {$plugin.version})</small> </dt> <dd>[<a href="{$plugin.url}" onclick="return !window.open(this.href);">{$plugin.auteur}</a>] {$plugin.description}</dd> {/foreach} </dl> </fieldset> <p class="help"> |
︙ | ︙ |
Modified src/templates/admin/config/upgrade.tpl from [f0a3b1ba46] to [876a754ddf].
︙ | ︙ | |||
53 54 55 56 57 58 59 | </dl> </details> <dl class="block error"> {input type="checkbox" name="upgrade" value=$version label="Je confirme vouloir procéder à la mise à jour" help="Cette action peut casser votre installation !"} </dl> </fieldset> | | | | 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 | </dl> </details> <dl class="block error"> {input type="checkbox" name="upgrade" value=$version label="Je confirme vouloir procéder à la mise à jour" help="Cette action peut casser votre installation !"} </dl> </fieldset> <p class="alert block">N'oubliez pas d'aller {link href="%swiki/?name=Changelog"|args:$website target="_blank" label="lire le journal des changements"} avant d'effectuer la mise à jour !</p> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="next" label="Effectuer la mise à jour" shape="right" class="main"} </p> {else} <fieldset> <legend>Mise à jour</legend> <dl> {foreach from=$releases key="version" item="release"} {input type="radio" name="download" value=$version label=$version} {if $version == $latest} <dd class="help"> Dernière version stable, conseillée. </dd> {/if} {/foreach} </dl> </fieldset> <p class="alert block">N'oubliez pas d'aller {link href="%swiki/?name=Changelog"|args:$website target="_blank" label="lire le journal des changements"} avant d'effectuer la mise à jour !</p> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="next" label="Télécharger" shape="right" class="main"} </p> {/if} </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/membres/_details.tpl from [5daf7f5f1c] to [080d8f71af].
︙ | ︙ | |||
32 33 34 35 36 37 38 | {elseif $c == $c_config.champ_identite} <strong>{$value}</strong> {elseif $c_config.type == 'email'} <a href="mailto:{$value|escape:'url'}">{$value}</a> {if $c == 'email' && $show_message_button} {linkbutton href="!membres/message.php?id=%d"|args:$data.id label="Envoyer un message" shape="mail"} {/if} | < < < < < < < < < < | > > > > > > > > > > > > > > > > > > > > | 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 | {elseif $c == $c_config.champ_identite} <strong>{$value}</strong> {elseif $c_config.type == 'email'} <a href="mailto:{$value|escape:'url'}">{$value}</a> {if $c == 'email' && $show_message_button} {linkbutton href="!membres/message.php?id=%d"|args:$data.id label="Envoyer un message" shape="mail"} {/if} {elseif $c_config.type == 'multiple'} <ul> {foreach from=$c_config.options key="b" item="name"} {if $value & (0x01 << $b)} <li>{$name}</li> {/if} {/foreach} </ul> {else} {$value|display_champ_membre:$c_config|raw} {/if} </dd> {if $c_config.type == 'email' && $value && ($email = Users\Emails::getEmail($value))} <dt>Statut e-mail</dt> <dd> {if $email.optout} <b class="alert">{icon shape="alert"}</b> Ne souhaite plus recevoir de messages <br/>{linkbutton target="_dialog" label="Rétablir l'envoi à cette adresse" href="emails.php?verify=%s"|args:$value shape="check"} {elseif $email.invalid} <b class="error">{icon shape="alert"} Adresse invalide</b> {elseif $email->hasReachedFailLimit()} <b class="error">{icon shape="alert"} Trop d'erreurs</b> {elseif $email.verified} <b class="confirm">{icon shape="check" class="confirm"}</b> Adresse vérifiée {else} Adresse non vérifiée {/if} {if $email.fail_log} <br /><span class="help">{$email.fail_log|escape|nl2br}</span> {/if} </dd> {/if} {/foreach} </dl> |
Modified src/templates/admin/membres/_list_actions.tpl from [b514dbed79] to [1111a22fd1].
1 2 | <tfoot> <tr> | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <tfoot> <tr> <td class="check"><input type="checkbox" value="Tout cocher / décocher" id="f_all2" /><label for="f_all2"></label></td> <td class="actions" colspan="{$colspan}"> <em>Pour les membres cochés :</em> {csrf_field key="membres_action"} <select name="action"> <option value="">— Choisir une action à effectuer —</option> <option value="move">Changer de catégorie</option> <option value="template">Générer des documents</option> {if !isset($export) || $export != false} <option value="csv">Exporter en tableau CSV</option> <option value="ods">Exporter en classeur Office</option> {/if} {if empty($hide_delete)} <option value="delete">Supprimer le membre</option> {/if} </select> <noscript> <input type="submit" value="OK" /> </noscript> </td> </tr> </tfoot> |
Added src/templates/admin/membres/emails.tpl version [b2294e48a8].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | {include file="admin/_head.tpl" title="Adresses rejetées" current="membres/message"} <nav class="tabs"> <ul> <li><a href="message_collectif.php">Envoyer</a></li> <li class="current"><a href="emails.php">Adresses rejetées</a></li> </ul> </nav> {if isset($_GET['sent'])} <p class="confirm block"> Un message de demande de confirmation a bien été envoyé. Le destinataire doit désormais cliquer sur le lien dans ce message. </p> {/if} <p class="help"> {if !$queue_count} Il n'y a aucun message en attente d'envoi. {else} Il y a {$queue_count} messages dans la file d'attente, ils seront envoyés dans quelques instants. {/if} </p> {if !$list->count()} <p class="alert block">Aucune adresse e-mail n'a été rejetée pour le moment. Cette page présentera les adresses e-mail invalides ou qui ont demandé à se désinscrire.</p> {else} {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="row"} <tr> <th><a href="fiche.php?id={$row.user_id}">{$row.identity}</a></th> <td>{$row.email}</td> <td>{$row.status}</td> <td class="num">{$row.sent_count}</td> <td>{$row.fail_log|escape|nl2br}</td> <td>{$row.last_sent|date}</td> <td> {if $row.email && $row.optout} {linkbutton target="_dialog" label="Rétablir" href="?verify=%s"|args:$row.email shape="check"} {/if} </td> </tr> {/foreach} </tbody> </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} <div class="block help"> <h3>Statuts possibles d'une adresse e-mail :</h3> <dl class="cotisation"> {* <dt>Vérifiée</dt> <dd>L'adresse a déjà reçu un message et a été vérifiée manuellement par le destinataire.</dd> *} <dt>Désinscription</dt> <dd>Le destinataire a demandé à être désinscrit et ne recevra plus de messages.</dd> <dt>Invalide</dt> <dd>L'adresse n'existe pas ou plus. Il n'est pas possible de lui envoyer des messages.</dd> <dt>Trop d'erreurs</dt> <dd>Le service destinataire a renvoyé une erreur temporaire plus de {$max_fail_count} fois.<br />Cela arrive par exemple si vos messages sont vus comme du spam trop souvent, ou si la boîte mail destinataire est pleine. Cette adresse ne recevra plus de message.</dd> </dl> </div> {/if} {include file="admin/_foot.tpl"} |
Added src/templates/admin/membres/emails_verification.tpl version [1618adab04].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | {include file="admin/_head.tpl" title="Vérification d'adresse" current="membres/message"} <form method="post" action="{$self_url}"> <fieldset> <legend>Demander la vérification de l'adresse</legend> {if $email.optout} <p class="help"> Si le membre a cliqué par erreur sur le lien de désinscription, il est possible de rétablir l'envoi des messages.<br /> Le membre recevra alors un message contenant un lien pour se réinscrire. </p> {elseif $email->hasReachedFailLimit()} <p class="help"> Si l'adresse du membre a rencontré trop d'erreurs (boîte mail pleine par exemple), il est possible de rétablir l'envoi des messages.<br /> Le membre recevra alors un message contenant un lien pour valider son adresse. </p> {/if} <p class="alert block"> Attention, n'utiliser cette procédure qu'à la demande du membre.<br /> En cas d'absence de consentement du membre, les messages aux autres membres pourront être bloqués par les serveurs destinataires. </p> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="send" label="Envoyer un message de vérification" shape="right" class="main"} </p> </fieldset> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/membres/fiche.tpl from [56a79db109] to [5f9a4846f6].
︙ | ︙ | |||
9 10 11 12 13 14 15 | {/if} </ul> </nav> <dl class="cotisation"> <dt>Activités et cotisations</dt> {foreach from=$services item="service"} | | > | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | {/if} </ul> </nav> <dl class="cotisation"> <dt>Activités et cotisations</dt> {foreach from=$services item="service"} <dd{if $service.archived} class="disabled"{/if}> {$service.label} {if $service.archived} <em>(activité passée)</em>{/if} {if $service.status == -1 && $service.end_date} — expirée {elseif $service.status == -1} — <b class="error">en retard</b> {elseif $service.status == 1 && $service.end_date} — <b class="confirm">en cours</b> {elseif $service.status == 1} — <b class="confirm">à jour</b>{/if} {if $service.status.expiry_date} — expire le {$service.expiry_date|date_short}{/if} {if !$service.paid} — <b class="error">À payer !</b>{/if} </dd> {foreachelse} |
︙ | ︙ |
Modified src/templates/admin/membres/import.tpl from [dc72822dd3] to [e618234940].
1 2 3 4 | {include file="admin/_head.tpl" title="Import & export des membres" current="membres"} {include file="admin/membres/_nav.tpl" current="import"} | < | < | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | {include file="admin/_head.tpl" title="Import & export des membres" current="membres"} {include file="admin/membres/_nav.tpl" current="import"} <p> {linkbutton shape="export" href="?export=csv" label="Exporter en CSV"} {linkbutton shape="export" href="?export=ods" label="Exporter en classeur LibreOffice/Office"} </p> {form_errors} {if $ok} <p class="block confirm"> L'import s'est bien déroulé. </p> |
︙ | ︙ |
Modified src/templates/admin/membres/index.tpl from [b40cd2743c] to [99c33965df].
︙ | ︙ | |||
31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <input type="submit" value="Chercher →" /> </fieldset> </form> <form method="post" action="action.php" class="memberList"> {if $list->count()} {include file="common/dynamic_list_head.tpl" check=$can_edit} {foreach from=$list->iterate() item="row"} <tr> {if $can_edit} <td class="check">{input type="checkbox" name="selected[]" value=$row._user_id}</td> {/if} | > > | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <input type="submit" value="Chercher →" /> </fieldset> </form> <form method="post" action="action.php" class="memberList"> {if $list->count()} {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {include file="common/dynamic_list_head.tpl" check=$can_edit} {foreach from=$list->iterate() item="row"} <tr> {if $can_edit} <td class="check">{input type="checkbox" name="selected[]" value=$row._user_id}</td> {/if} |
︙ | ︙ |
Modified src/templates/admin/membres/message.tpl from [4059d1bc38] to [29a83a20df].
1 2 3 4 5 | {include file="admin/_head.tpl" title="Contacter un membre" current="membres"} {form_errors} <form method="post" action="{$self_url}"> | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {include file="admin/_head.tpl" title="Contacter un membre" current="membres"} {form_errors} <form method="post" action="{$self_url}"> <fieldset class="mailing"> <legend>Message</legend> <dl> <dt>Expéditeur</dt> <dd>{$user.identite} <{$user.email}></dd> <dt>Destinataire</dt> <dd>{$membre.identite} ({$categorie.name})</dd> <dt><label for="f_subject">Sujet</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="sujet" id="f_subject" value="{form_field name=sujet}" required="required" /></dd> <dt><label for="f_message">Message</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><textarea name="message" id="f_message" cols="72" rows="25" required="required">{form_field name=message}</textarea></dd> <dd> <input type="checkbox" name="copie" id="f_copie" value="1" /> <label for="f_copie">Recevoir par e-mail une copie du message envoyé</label> </dd> </dl> |
︙ | ︙ |
Modified src/templates/admin/membres/message_collectif.tpl from [621649891a] to [0cc04e5843].
|
| | > > > > > > > > > > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | < < | < | < < < | | < | < > > | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | {include file="admin/_head.tpl" title="Envoyer un message collectif" current="membres/message" custom_css=["!web/css.php"]} <nav class="tabs"> <ul> <li class="current"><a href="{$self_url}">Envoyer</a></li> <li><a href="emails.php">Adresses rejetées</a></li> </ul> </nav> {if $sent} <p class="block confirm">Votre message a été envoyé.</p> {/if} {form_errors} <form method="post" action="{$self_url_no_qs}"> {if $preview} <fieldset class="mailing"> <legend>Prévisualisation du message</legend> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} <nav class="menu"> <b data-icon="↷" class="btn">Exporter la liste des destinataires</b> <span> {button type="submit" name="export" value="csv" shape="export" label="Export CSV"} {button type="submit" name="export" value="ods" shape="export" label="Export LibreOffice"} {if CALC_CONVERT_COMMAND} {button type="submit" name="export" value="xlsx" shape="export" label="Export Excel"} {/if} </span> </nav> {/if} <p class="help"> Ce message sera envoyé à <strong>{$recipients_count}</strong> destinataires.<br /> Voici un exemple du message pour un de ces destinataires. </p> <dl> <dt>Expéditeur</dt> <dd>{$preview.from}</dd> <dt>Destinataire</dt> <dd> {$preview.to} </dd> <dt>Sujet</dt> <dd>{$preview.subject}</dd> <dt>Message</dt> <dd class="preview">{$preview.html|raw}</dd> </dl> </fieldset> <p class="submit"> {input type="hidden" name="subject"} {input type="hidden" name="message"} {input type="hidden" name="target"} {input type="hidden" name="send_copy"} {input type="hidden" name="render"} {csrf_field key=$csrf_key} {button type="submit" name="back" label="Retour à l'édition" shape="left"} {button type="submit" name="send" label="Envoyer" shape="right" class="main"} </p> {else} <fieldset class="mailing"> <legend>Message</legend> <dl> <dt>Expéditeur</dt> <dd>{$config.nom_asso} <{$config.email_asso}></dd> <dt><label for="f_target">Destinataires</label></dt> <dd> <select name="target" id="f_target" required="required"> <option value="all_">Tous les membres (sauf ceux appartenant à une catégorie cachée)</option> <optgroup label="Catégorie de membres"> {foreach from=$categories key="id" item="label"} <option value="category_{$id}" {form_field name="target" selected="category_%d"|args:$id}>{$label}</option> {/foreach} </optgroup> <optgroup label="Recherches enregistrées"> {foreach from=$search_list item="s"} <option value="search_{$s.id}" {form_field name="target" selected="search_%d"|args:$s.id}>{$s.intitule}</option> {/foreach} </optgroup> </select> </dd> <dd class="help"> Vous pouvez cibler précisément des membres en créant une <a href="{$admin_url}membres/recherche.php">recherche enregistrée</a>. Les recherches enregistrées apparaîtront dans ce formulaire. </dd> {input type="text" name="subject" required=true label="Sujet"} {input type="textarea" name="message" cols=35 rows=25 required=true label="Message"} {input type="checkbox" name="send_copy" value=1 label="Recevoir par e-mail une copie du message envoyé"} <dt><label for="f_render">Format de rendu</label></dt> <dd> {input type="select" name="render" options=$render_formats} {linkbutton shape="help" href="!web/_syntax_skriv.html" target="_dialog" label="Aide syntaxe SkrivML"} {linkbutton shape="help" href="!web/_syntax_markdown.html" target="_dialog" label="Aide syntaxe MarkDown"} </dd> </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="preview" label="Prévisualiser" shape="right" class="main"} </p> {/if} </form> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/membres/recherche.tpl from [7ed26a1626] to [27011add8f].
|
| | | 1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Recherche de membre" current="membres" custom_js=['lib/query_builder.min.js']} {include file="admin/membres/_nav.tpl" current="recherche"} {include file="common/search/advanced.tpl" action_url=$self_url} {if !empty($result)} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} |
︙ | ︙ |
Added src/templates/admin/optout.tpl version [c331cc3781].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | {include file="admin/_head.tpl" title="Désinscription" transparent=true} {if $verify === true} <p class="block confirm"> Votre adresse e-mail a bien été vérifiée, merci ! </p> {elseif $verify === false} <p class="block error"> Erreur de vérification de votre adresse e-mail. </p> {elseif $ok} <p class="block confirm"> Vous avez été bien désinscrit, vous ne recevrez plus aucun message de notre part. </p> <p class="help"> Vous pouvez revenir sur cette page pour vous réinscrire à tout moment. </p> {elseif $resub_ok} <p class="block confirm"> Un e-mail vous a été envoyé, merci de cliquer sur le lien dans le message reçu pour confirmer. </p> {elseif $email.optout} <p class="block alert"> Votre adresse e-mail est déjà désinscrite. Pour demander à vous réinscrire, renseignez le formulaire ci-dessous. </p> {form_errors} <form method="post" action="{$self_url}"> <fieldset> <dl> {input type="email" required=true name="email" label="Adresse e-mail"} {input type="checkbox" name="confirm_resub" value="1" required=true label="Oui, je veux à nouveau recevoir les messages de l'association"} </dl> </fieldset> <p class="submit"> {csrf_field key="optout"} {button type="submit" name="resub" label="Réinscrire mon adresse e-mail" shape="right" class="main"} </p> </form> {else} {form_errors} <form method="post" action="{$self_url}"> <p class="help"> En cliquant sur ce bouton vous confirmez ne plus vouloir recevoir de messages de notre part. </p> <p class="submit"> {csrf_field key="optout"} {button type="submit" name="optout" label="Me désinscrire" shape="right" class="main"} </p> </form> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/common/_csv_help.tpl from [109bc3ecbe] to [ea4612c151].
1 2 3 4 5 6 7 | <dd class="help"> Règles à suivre pour créer le fichier CSV : <ul> <li>Il est recommandé d'utiliser LibreOffice pour créer le fichier CSV</li> <li>Le fichier doit être en UTF-8</li> <li>Le séparateur doit être le point-virgule ou la virgule</li> <li>Cocher l'option <em>"Mettre en guillemets toutes les cellules du texte"</em></li> | > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | <dd class="help"> Règles à suivre pour créer le fichier CSV : <ul> {if !CALC_CONVERT_COMMAND} <li>Il est recommandé d'utiliser LibreOffice pour créer le fichier CSV</li> <li>Le fichier doit être en UTF-8</li> <li>Le séparateur doit être le point-virgule ou la virgule</li> <li>Cocher l'option <em>"Mettre en guillemets toutes les cellules du texte"</em></li> {/if} <li>Le fichier peut comporter les colonnes suivantes : <em>{$csv->getColumnsString()}</em></li> {if ($columns = $csv->getMandatoryColumnsString())}<li>Le fichier <strong>doit obligatoirement</strong> comporter les colonnes suivantes : <em>{$columns}</em></li>{/if} </ul> </dd> |
Modified src/templates/common/_csv_match_columns.tpl from [09427edd39] to [a4fa33eaf0].
1 | <fieldset> | | | 1 2 3 4 5 6 7 8 9 | <fieldset> <legend>Importer depuis un fichier CSV</legend> <dl> <dd class="help">{$csv->count()} lignes trouvées dans le fichier</dd> <dt>{input type="checkbox" name="skip_first_line" value="1" label="Ne pas importer la première ligne" help="Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes, mais des données" default=1} <dt><label>Correspondance des colonnes</label></dt> <dd> <table class="list auto"> <thead> |
︙ | ︙ |
Modified src/templates/common/dynamic_list_head.tpl from [16625e076c] to [81d97ad488].
1 2 3 4 5 6 7 8 | <table class="list"> <thead class="userOrder"> <tr> {if !empty($check)} <td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td> {/if} {foreach from=$list->getHeaderColumns() key="key" item="column"} <td class="{if $list->order == $key}cur{/if}"> | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <table class="list"> <thead class="userOrder"> <tr> {if !empty($check)} <td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td> {/if} {foreach from=$list->getHeaderColumns() key="key" item="column"} <td class="{if $list->order == $key}cur{/if}"> {if (!array_key_exists('select', $column) || !is_null($column['select'])) && !(array_key_exists('order', $column) && null === $column['order'])} <a href="{$list->orderURL($key, $list->order == $key ? !$list->desc : $list->desc)}" title="{if $list->order == $key}Cliquer pour inverser l'ordre de tri{else}Cliquer pour trier avec cette colonne{/if}"> {if $list.desc} <span class="icn dn">↓</span> {else} <span class="icn up">↑</span> {/if} {$column.label} </a> |
︙ | ︙ |
Modified src/templates/common/files/_context_list.tpl from [3ece07f3ea] to [d2e743b145].
︙ | ︙ | |||
18 19 20 21 22 23 24 | <p class="actions"> {linkbutton shape="upload" href="!common/files/upload.php?p=%s"|args:$path target="_dialog" label="Ajouter un fichier"} </p> {/if} <div class="files-list"> {foreach from=$files item="file"} | > | | < > > > > > | | | | 18 19 20 21 22 23 24 25 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 | <p class="actions"> {linkbutton shape="upload" href="!common/files/upload.php?p=%s"|args:$path target="_dialog" label="Ajouter un fichier"} </p> {/if} <div class="files-list"> {foreach from=$files item="file"} <?php if (!$file->checkReadAccess($session)) { break; } $preview = $file->canPreview(); $target = $preview ? '_dialog' : '_blank'; $url = $preview ? ADMIN_URL . 'common/files/preview.php?p=' . $file->path : $file->url(); ?> <aside class="file"> {if $file.image} <figure> <a target="{$target}" href="{$url}" data-mime="{$file.mime}"><img src="{$file->thumb_url()}" alt="" /></a> <figcaption> <a target="{$target}" href="{$url}" data-mime="{$file.mime}">{$file.name}</a> <small>({$file.mime}, {$file.size|size_in_bytes})</small> </figcaption> </figure> {else} <a target="{$target}" href="{$url}" data-mime="{$file.mime}">{$file.name}</a> <small>({$file.mime}, {$file.size|size_in_bytes})</small> {/if} {linkbutton shape="download" href=$file->url(true) target="_blank" label="Télécharger"} {if $edit && $file->checkDeleteAccess($session)} {linkbutton shape="delete" target="_dialog" href="!common/files/delete.php?p=%s"|args:$file.path label="Supprimer"} {/if} </aside> {foreachelse} {if !$can_upload} <em>--</em> {/if} {/foreach} </div> |
Modified src/templates/common/files/edit_code.tpl from [7057de99c4] to [e806796639].
︙ | ︙ | |||
10 11 12 13 14 15 16 | {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> | < < | 10 11 12 13 14 15 16 17 | {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="admin/_foot.tpl"} |
Modified src/templates/error.tpl from [6a0842893f] to [67cba6eb16].
︙ | ︙ | |||
30 31 32 33 34 35 36 | </head> <body> <h1>{if empty($title)}Erreur{else}{$title}{/if}</h1> <p class="block error"> | > > > | > | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | </head> <body> <h1>{if empty($title)}Erreur{else}{$title}{/if}</h1> <p class="block error"> {if $html_error} {$html_error|raw} {else} {$error|escape|nl2br} {/if} </p> <p> <a href="{$admin_url}" onclick="history.back(); return false;">← Retour</a> </p> </body> </html> |
Modified src/templates/me/services.tpl from [4bbaabc663] to [ce6b8b28f7].
1 2 3 4 5 | {include file="admin/_head.tpl" title="Mes activités & cotisations" current="me/services"} <dl class="cotisation"> <dt>Mes activités et cotisations</dt> {foreach from=$services item="service"} | > > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php use Garradin\Entities\Accounting\Account; ?> {include file="admin/_head.tpl" title="Mes activités & cotisations" current="me/services"} <dl class="cotisation"> <dt>Mes activités et cotisations</dt> {foreach from=$services item="service"} <dd{if $service.archived} class="disabled"{/if}> {$service.label} {if $service.archived} <em>(activité passée)</em>{/if} {if $service.status == -1 && $service.end_date} — expirée {elseif $service.status == -1} — <b class="error">en retard</b> {elseif $service.status == 1 && $service.end_date} — <b class="confirm">en cours</b> {elseif $service.status == 1} — <b class="confirm">à jour</b>{/if} {if $service.status.expiry_date} — expire le {$service.expiry_date|date_short}{/if} {if !$service.paid} — <b class="error">À payer !</b>{/if} </dd> {foreachelse} <dd> Vous n'êtes inscrit à aucune activité ou cotisation. </dd> {/foreach} </dl> <h2 class="ruler">Dettes et créances</h2> {if !count($accounts)} <p class="help">Aucune dette ou créance n'est associée à votre profil.</p> {else} <table class="list"> <thead> <tr> <td class="money">Montant</td> <th>Compte</th> <td></td> </tr> </thead> <tbody> {foreach from=$accounts item="account"} <tr> <td class="money">{$account.balance|raw|money_currency}</td> <th>{$account.label}</th> <td> {if $account.position == Account::LIABILITY}<em>Nous vous devons {$account.balance|raw|money_currency}.</em> {else}<strong class="error">Vous nous devez {$account.balance|raw|money_currency}.</strong>{/if} </td> </tr> {/foreach} </tbody> </table> {/if} {if $list->count()} <h2 class="ruler">Historique des inscriptions</h2> {include file="common/dynamic_list_head.tpl"} |
︙ | ︙ |
Modified src/templates/services/_nav.tpl from [df8b661101] to [883c7c58b1].
︙ | ︙ | |||
18 19 20 21 22 23 24 | <li{if $show_old_services} class="current"{/if}>{link href="!services/?old=1" label="Activités passées"}</li> </ul> {/if} {if isset($current_service)} <ul class="sub"> <li class="title"> | | < < < < < < < | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <li{if $show_old_services} class="current"{/if}>{link href="!services/?old=1" label="Activités passées"}</li> </ul> {/if} {if isset($current_service)} <ul class="sub"> <li class="title"> {$current_service->long_label()} </li> <li{if $service_page == 'index'} class="current"{/if}><a href="{$admin_url}services/fees/?id={$current_service.id}"><strong>Tarifs</strong></a></li> <li{if $service_page == 'active'} class="current"{/if}><a href="{$admin_url}services/details.php?id={$current_service.id}">À jour</a></li> <li{if $service_page == 'expired'} class="current"{/if}><a href="{$admin_url}services/details.php?id={$current_service.id}&type=expired">Inscription expirée</a></li> <li{if $service_page == 'unpaid'} class="current"{/if}><a href="{$admin_url}services/details.php?id={$current_service.id}&type=unpaid">En attente de règlement</a></li> </ul> {/if} {if isset($current_fee)} <ul class="sub"> <li class="title"> {$current_fee.label} {if $current_fee.amount} — {$current_fee.amount|money_currency|raw}{/if} </li> <li{if $fee_page == 'active'} class="current"{/if}><a href="{$admin_url}services/fees/details.php?id={$current_fee.id}">À jour</a></li> <li{if $fee_page == 'expired'} class="current"{/if}><a href="{$admin_url}services/fees/details.php?id={$current_fee.id}&type=expired">Inscription expirée</a></li> <li{if $fee_page == 'unpaid'} class="current"{/if}><a href="{$admin_url}services/fees/details.php?id={$current_fee.id}&type=unpaid">En attente de règlement</a></li> </ul> {/if} </nav> |
Modified src/templates/services/_service_form.tpl from [00234cd4d6] to [02bf322c18].
1 2 3 4 5 6 7 8 9 10 | {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>{$legend}</legend> <dl> {input name="label" type="text" required=1 label="Libellé" source=$service} {input name="description" type="textarea" label="Description" source=$service} | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>{$legend}</legend> <dl> {input name="label" type="text" required=1 label="Libellé" source=$service} {input name="description" type="textarea" label="Description" source=$service} <dt><label for="f_periodicite_jours">Période de validité</label> <b title="Champ obligatoire">(obligatoire)</b></dt> <dd class="help">Attention, une modification de la période renseignée ici ne modifie pas la date d'expiration des activités déjà enregistrées.</dd> {input name="period" type="radio" value="0" label="Pas de période (cotisation ponctuelle)" default=$period} {input name="period" type="radio" value="1" label="En nombre de jours" default=$period} <dd class="period_1"> <dl> {input name="duration" type="number" step="1" label="Durée de validité" size="5" source=$service} </dl> |
︙ | ︙ |
Modified src/templates/services/details.tpl from [0d42873582] to [96bde1ac4a].
1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="%s — Liste des membres inscrits"|args:$service.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page=$type} <dl class="cotisation"> <dt>Nombre de membres trouvés</dt> <dd> {$list->count()} <em class="help">(N'apparaît ici que l'inscription la plus récente de chaque membre.)</em> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} | > > > | | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {include file="admin/_head.tpl" title="%s — Liste des membres inscrits"|args:$service.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page=$type} <dl class="cotisation"> <dt>Nombre de membres trouvés</dt> <dd> {$list->count()} <em class="help">(N'apparaît ici que l'inscription la plus récente de chaque membre.)</em> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} <nav class="menu"> <b data-icon="↷" class="btn">Export</b> <span> {linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"} {linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export LibreOffice"} {if CALC_CONVERT_COMMAND} {linkbutton href="%s&export=xlsx"|args:$self_url shape="export" label="Export Excel"} {/if} </span> </nav> {/if} </dd> </dl> <?php $can_action = $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN); ?> |
︙ | ︙ | |||
53 54 55 56 57 58 59 | {linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user} </td> </tr> {/foreach} </tbody> {if $can_action} | | | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | {linkbutton shape="alert" label="Rappels envoyés" href="!services/reminders/user.php?id=%d"|args:$row.id_user} </td> </tr> {/foreach} </tbody> {if $can_action} {include file="admin/membres/_list_actions.tpl" colspan=7 export=false hide_delete=true} {/if} </table> {if $can_action} </form> {/if} {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {include file="admin/_foot.tpl"} |
Modified src/templates/services/fees/_fee_form.tpl from [9d5c15a2e4] to [cbf57c15a9].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php assert(isset($legend)); assert(isset($csrf_key)); assert(isset($submit_label)); $targets = Entities\Accounting\Account::TYPE_REVENUE; ?> {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>{$legend}</legend> <dl> | > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 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 assert(isset($legend)); assert(isset($csrf_key)); assert(isset($submit_label)); $targets = Entities\Accounting\Account::TYPE_REVENUE; $analytical_targets = Entities\Accounting\Account::TYPE_ANALYTICAL; ?> {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>{$legend}</legend> <dl> {input name="label" type="text" required=true label="Libellé" source=$fee} {input name="description" type="textarea" label="Description" source=$fee} <dt><label for="f_amount_type">Montant de la cotisation</label></dt> {input name="amount_type" type="radio" value="0" label="Gratuite ou prix libre" default=$amount_type} {input name="amount_type" type="radio" value="1" label="Montant fixe ou prix libre conseillé" default=$amount_type} <dd class="amount_type_1"> <dl> {input name="amount" type="money" label="Montant" source=$fee required=true} </dl> </dd> {input name="amount_type" type="radio" value="2" label="Montant variable" default=$amount_type} <dd class="amount_type_2"> <dl> {input name="formula" type="textarea" label="Formule de calcul" source=$fee required=true} <dd class="help"> <a href="https://garradin.eu/Formule-calcul-activite">Aide sur les formules de calcul</a> </dd> </dl> </dd> <dt><strong>Comptabilité</strong></dt> {input name="accounting" type="checkbox" value="1" label="Enregistrer en comptabilité" default=$accounting_enabled} |
︙ | ︙ | |||
50 51 52 53 54 55 56 | <select id="f_id_year" name="id_year"> <option value="">-- Sélectionner un exercice</option> {foreach from=$years item="year"} <option value="{$year.id}"{if $year.id == $fee.id_year} selected="selected"{/if}>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</option> {/foreach} </select> </dd> | | > | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <select id="f_id_year" name="id_year"> <option value="">-- Sélectionner un exercice</option> {foreach from=$years item="year"} <option value="{$year.id}"{if $year.id == $fee.id_year} selected="selected"{/if}>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</option> {/foreach} </select> </dd> {input type="list" target="!acc/charts/accounts/selector.php?targets=%s&year=%d"|args:$targets,$fee.id_year name="account" label="Compte de recettes à utiliser" default=$account required=true} {input type="list" target="!acc/charts/accounts/selector.php?targets=%s&year=%d"|args:$analytical_targets,$fee.id_year name="analytical" label="Associer les écritures à ce projet" default=$analytical_account required=false} </dl> {/if} </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} |
︙ | ︙ |
Modified src/templates/services/fees/details.tpl from [614b52eea9] to [3a82c9cc09].
1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="Tarif : %s — Liste des membres inscrits"|args:$fee.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page="index" current_fee=$fee fee_page=$type} <dl class="cotisation"> <dt>Nombre de membres trouvés</dt> <dd> {$list->count()} <em class="help">(N'apparaît ici que l'inscription la plus récente de chaque membre.)</em> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} | > > > | | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {include file="admin/_head.tpl" title="Tarif : %s — Liste des membres inscrits"|args:$fee.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page="index" current_fee=$fee fee_page=$type} <dl class="cotisation"> <dt>Nombre de membres trouvés</dt> <dd> {$list->count()} <em class="help">(N'apparaît ici que l'inscription la plus récente de chaque membre.)</em> {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} <nav class="menu"> <b data-icon="↷" class="btn">Export</b> <span> {linkbutton href="%s&export=csv"|args:$self_url shape="export" label="Export CSV"} {linkbutton href="%s&export=ods"|args:$self_url shape="export" label="Export LibreOffice"} {if CALC_CONVERT_COMMAND} {linkbutton href="%s&export=xlsx"|args:$self_url shape="export" label="Export Excel"} {/if} </span> </nav> {/if} </dd> </dl> <?php $can_action = $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN); ?> |
︙ | ︙ | |||
39 40 41 42 43 44 45 | </td> </tr> {/foreach} </tbody> {if $can_action} | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | </td> </tr> {/foreach} </tbody> {if $can_action} {include file="admin/membres/_list_actions.tpl" colspan=5 export=false hide_delete=true} {/if} </table> {if $can_action} </form> {/if} {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {include file="admin/_foot.tpl"} |
Modified src/templates/services/fees/edit.tpl from [5f56ed8c69] to [6cdf0d7768].
|
| | | 1 2 3 4 5 6 7 | {include file="admin/_head.tpl" title="%s — Modifier le tarif"|args:$fee.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page="index"} {include file="services/fees/_fee_form.tpl" legend="Modifier un tarif" submit_label="Enregistrer"} {include file="admin/_foot.tpl"} |
Modified src/templates/services/fees/index.tpl from [829d080cff] to [8a4c3cb8d5].
1 2 3 4 5 6 7 8 9 | {include file="admin/_head.tpl" title="%s — Tarifs"|args:$service.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page="index"} {if count($list)} <table class="list"> <thead> <th>Tarif</th> <td>Montant</td> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | {include file="admin/_head.tpl" title="%s — Tarifs"|args:$service.label current="membres/services"} {include file="services/_nav.tpl" current="index" current_service=$service service_page="index"} {if count($list)} <table class="list"> <thead> <th>Tarif</th> <td>Montant</td> <td>Membres à jour</td> <td>Membres expirés</td> <td>Membres en attente de règlement</td> <td></td> </thead> <tbody> {foreach from=$list item="row"} <tr> |
︙ | ︙ |
Modified src/templates/services/index.tpl from [ac3ba626de] to [7b0953562f].
1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title="Activités et cotisations" current="membres/services"} {include file="services/_nav.tpl" current="index" service=null fee=null} {if isset($_GET['CREATE'])} <p class="block error">Vous devez déjà créer une activité pour pouvoir utiliser cette fonction.</p> {/if} | | < | < < < < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {include file="admin/_head.tpl" title="Activités et cotisations" current="membres/services"} {include file="services/_nav.tpl" current="index" service=null fee=null} {if isset($_GET['CREATE'])} <p class="block error">Vous devez déjà créer une activité pour pouvoir utiliser cette fonction.</p> {/if} {if $list->count()} {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="row"} <tr> <th><a href="fees/?id={$row.id}">{$row.label}</a></th> <td> {if $row.duration} {$row.duration} jours {elseif $row.start_date} du {$row.start_date|date_short} au {$row.end_date|date_short} |
︙ | ︙ |
Modified src/templates/services/user/_service_user_form.tpl from [75a8977fdd] to [018a3a1cfd].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php assert(isset($create) && is_bool($create)); assert(isset($has_past_services) && is_bool($has_past_services)); assert(isset($current_only) && is_bool($current_only)); assert(isset($form_url) && is_string($form_url)); assert(isset($today) && $today instanceof \DateTimeInterface); assert($create === false || isset($account_targets)); assert(isset($grouped_services) && is_array($grouped_services)); ?> <form method="post" action="{$self_url}" data-focus="1" data-create="{$create|escape:json}"> | < < < < < < < < < | > | < > > > | > > | > > > > > > | | | < > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <?php assert(isset($create) && is_bool($create)); assert(isset($has_past_services) && is_bool($has_past_services)); assert(isset($current_only) && is_bool($current_only)); assert(isset($form_url) && is_string($form_url)); assert(isset($today) && $today instanceof \DateTimeInterface); assert($create === false || isset($account_targets)); assert(isset($grouped_services) && is_array($grouped_services)); ?> <form method="post" action="{$self_url}" data-focus="1" data-create="{$create|escape:json}"> <fieldset> <legend>Inscrire à une activité</legend> <dl> {if $create && $users} <dt> Membres à inscrire </dt> <dd> <table> {foreach from=$users key="id" item="name"} <tr> <td> <input type="hidden" name="users[{$id}]" value="{$name}" /> {button shape="delete" onclick="this.parentNode.parentNode.remove();" title="Supprimer de la liste"} </td> <th> {$name} </th> </tr> {/foreach} </table> </dd> {elseif $create && $copy_service} <dt>Recopier depuis l'activité</dt> <dd><strong>{$copy_service.label}</strong><input type="hidden" name="copy_service" value="{$copy_service.id}" /></dd> <dd><em>{if $copy_service_only_paid}(seulement les inscriptions marquées comme payées){else}(toutes les inscriptions){/if}</em><input type="hidden" name="copy_service_only_paid" value="{$copy_service_only_paid}" /></dd> {/if} <dt><label for="f_service_ID">Activité</label> <b>(obligatoire)</b></dt> {if $has_past_services} <dd> {if $current_only} Seules les activités courantes sont affichées. {button name="past_services" value="1" shape="reset" type="submit" label="Inscrire à une activité passée"} {else} Seules les activités passées sont affichées. {button name="past_services" value="0" shape="left" type="submit" label="Inscrire à une activité courante"} {/if} </dd> {/if} {foreach from=$grouped_services item="service"} <dd class="radio-btn"> {input type="radio" name="id_service" value=$service.id data-duration=$service.duration data-expiry=$service.expiry_date|date_short label=null source=$service_user} <label for="f_id_service_{$service.id}"> <div> <h3>{$service.label}</h3> |
︙ | ︙ | |||
117 118 119 120 121 122 123 124 | </dl> </fieldset> {if $create} <fieldset class="accounting"> <legend>{input type="checkbox" name="create_payment" value=1 default=1 label="Enregistrer en comptabilité"}</legend> <dl> | > > > > | | | | 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | </dl> </fieldset> {if $create} <fieldset class="accounting"> <legend>{input type="checkbox" name="create_payment" value=1 default=1 label="Enregistrer en comptabilité"}</legend> {if !empty($users)} <p class="help">Une écriture sera créée pour chaque membre inscrit.</p> {/if} <dl> {input type="money" name="amount" label="Montant réglé par le membre" required=true help="En cas de règlement en plusieurs fois il sera possible d'ajouter des règlements via la page de suivi des activités de ce membre."} {input type="list" target="!acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=true} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."} {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."} {input type="textarea" name="notes" label="Remarques"} </dl> </fieldset> {/if} <p class="submit"> |
︙ | ︙ |
Modified src/templates/services/user/add.tpl from [027f0b9b0f] to [f21f56bf5b].
︙ | ︙ | |||
13 14 15 16 17 18 19 | {input type="radio-btn" name="choice" value="2" label="Recopier depuis une activité" help="Utile si vous avez une cotisation par année civile par exemple : copie les membres inscrits l'année précédente dans la nouvelle année."} </dl> </fieldset> <fieldset class="c1"> <legend>Inscrire des membres</legend> <dl> | | | > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | {input type="radio-btn" name="choice" value="2" label="Recopier depuis une activité" help="Utile si vous avez une cotisation par année civile par exemple : copie les membres inscrits l'année précédente dans la nouvelle année."} </dl> </fieldset> <fieldset class="c1"> <legend>Inscrire des membres</legend> <dl> {input type="list" name="users" required=true label="Membres à inscrire" target="!membres/selector.php" multiple=true} </dl> </fieldset> <fieldset class="c2"> <legend>Recopier depuis une activité</legend> <dl> {input type="select" name="copy_service" label="Activité à recopier" options=$services required=true default=0} {input type="checkbox" name="copy_service_only_paid" value="1" label="Ne recopier que les membres dont l'inscription est payée"} </dl> </fieldset> <p class="submit"> <input type="hidden" name="paid" value="1" /> {button type="submit" name="next" label="Continuer" shape="right" class="main"} </p> </form> <script type="text/javascript"> {literal} function selectChoice() { |
︙ | ︙ |
Modified src/templates/services/user/index.tpl from [43943e4257] to [6870bb4fa4].
1 2 3 4 5 6 7 8 9 10 11 12 | {include file="admin/_head.tpl" title="%s — Inscriptions aux activités et cotisations"|args:$user.identite current="membres/services"} <p> {linkbutton href="!membres/fiche.php?id=%d"|args:$user.id label="Retour à la fiche membre" shape="user"} {linkbutton href="!services/user/subscribe.php?user=%d"|args:$user.id label="Inscrire à une activité" shape="plus"} </p> {form_errors} <dl class="cotisation"> <dt>Statut des inscriptions</dt> {foreach from=$services item="service"} | | > | | > > > | | > > > > > > > > > > > > > > > > > > > < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | {include file="admin/_head.tpl" title="%s — Inscriptions aux activités et cotisations"|args:$user.identite current="membres/services"} <p> {linkbutton href="!membres/fiche.php?id=%d"|args:$user.id label="Retour à la fiche membre" shape="user"} {linkbutton href="!services/user/subscribe.php?user=%d"|args:$user.id label="Inscrire à une activité" shape="plus"} </p> {form_errors} <dl class="cotisation"> <dt>Statut des inscriptions</dt> {foreach from=$services item="service"} <dd{if $service.archived} class="disabled"{/if}> {$service.label} {if $service.archived} <em>(activité passée)</em>{/if} {if $service.status == -1 && $service.end_date} — expirée {elseif $service.status == -1} — <b class="error">en retard</b> {elseif $service.status == 1 && $service.end_date} — <b class="confirm">en cours</b> {elseif $service.status == 1} — <b class="confirm">à jour</b>{/if} {if $service.status.expiry_date} — expire le {$service.expiry_date|date_short}{/if} {if !$service.paid} — <b class="error">À payer !</b>{/if} </dd> {foreachelse} <dd> Ce membre n'est inscrit à aucune activité ou cotisation. </dd> {/foreach} <dt>Nombre d'inscriptions pour ce membre</dt> <dd> {$list->count()} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} <nav class="menu"> <b data-icon="↷" class="btn">Export</b> <span> {linkbutton href="?id=%d&export=csv"|args:$user.id shape="export" label="Export CSV"} {linkbutton href="?id=%d&export=ods"|args:$user.id shape="export" label="Export LibreOffice"} {if CALC_CONVERT_COMMAND} {linkbutton href="?id=%d&export=xlsx"|args:$user.id shape="export" label="Export Excel"} {/if} </span> </nav> {/if} </dd> </dl> {if $only} <p class="alert block">Cette liste ne montre qu'une seule inscription, liée à une écriture. {link href="?id=%d"|args:$user.id label="Voir toutes les inscriptions"}</p> {/if} {include file="common/dynamic_list_head.tpl"} {foreach from=$list->iterate() item="row"} <tr> <th>{$row.label}</th> <td>{$row.date|date_short}</td> <td>{$row.expiry|date_short}</td> <td>{$row.fee}</td> <td>{if $row.paid}<b class="confirm">Oui</b>{else}<b class="error">Non</b>{/if}</td> <td>{$row.amount|raw|money_currency}</td> <td class="actions"> {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE) && $row.id_account} {linkbutton shape="plus" label="Nouveau règlement" href="payment.php?id=%d"|args:$row.id} {/if} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ)} {if $row.has_transactions} {linkbutton shape="menu" label="Liste des écritures" href="!acc/transactions/service_user.php?id=%d&user=%d"|args:$row.id,$user.id} {else} {linkbutton shape="check" label="Lier à une écriture" href="link.php?id=%d"|args:$row.id target="_dialog"} {/if} {/if} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE)} {if $row.paid} {linkbutton shape="reset" label="Marquer comme non payé" href="?id=%d&su_id=%d&paid=0"|args:$user.id,$row.id} {else} {linkbutton shape="check" label="Marquer comme payé" href="?id=%d&su_id=%d&paid=1"|args:$user.id,$row.id} {/if} {linkbutton shape="edit" label="Modifier" href="edit.php?id=%d"|args:$row.id} {linkbutton shape="delete" label="Supprimer" href="delete.php?id=%d"|args:$row.id} {/if} </td> </tr> {/foreach} </tbody> </table> {pagination url=$list->paginationURL() page=$list.page bypage=$list.per_page total=$list->count()} {include file="admin/_foot.tpl"} |
Added src/templates/services/user/link.tpl version [2dca006b28].
> > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {include file="admin/_head.tpl" title="Lier une inscription à une écriture" current="acc/accounts"} {form_errors} <form method="post" action="{$self_url}" data-focus="1"> <fieldset> <legend>Lier à une écriture</legend> <dl> {input type="number" label="Numéro de l'écriture" name="id_transaction" required=true} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/services/user/payment.tpl from [c4cd0c545e] to [d2a8c9e74c].
︙ | ︙ | |||
10 11 12 13 14 15 16 | <dl> <dt>Membre sélectionné</dt> <dd><h3>{$user_name}</h3></dd> <dt><strong>Inscription</strong></dt> {input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"} {input type="date" name="date" label="Date" required=1 source=$su} {input type="money" name="amount" label="Montant réglé par le membre" required=1} | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <dl> <dt>Membre sélectionné</dt> <dd><h3>{$user_name}</h3></dd> <dt><strong>Inscription</strong></dt> {input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"} {input type="date" name="date" label="Date" required=1 source=$su} {input type="money" name="amount" label="Montant réglé par le membre" required=1} {input type="list" target="!acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=1} {input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."} {input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} {button type="submit" name="save_and_add_payment" label="Enregistrer et ajouter un autre règlement" shape="plus"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/web/_attach.tpl from [dbe6b7661f] to [1e5c124ad7].
︙ | ︙ | |||
27 28 29 30 31 32 33 | <dt>Alignement :</dt> <dd class="align"> <input type="button" name="left" value="À gauche" /> <input type="button" name="center" value="Au centre" /> <input type="button" name="right" value="À droite" /> </dd> <dd class="cancel"> | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <dt>Alignement :</dt> <dd class="align"> <input type="button" name="left" value="À gauche" /> <input type="button" name="center" value="Au centre" /> <input type="button" name="right" value="À droite" /> </dd> <dd class="cancel"> <button type="reset">Annuler</button> </dd> </dl> </fieldset> </form> {if !empty($images)} <ul class="gallery"> |
︙ | ︙ |
Modified src/templates/web/_selector.tpl from [f6ba705f24] to [4cf247aa35].
1 2 3 4 5 6 | {include file="admin/_head.tpl" title="Choisir la page parent" current="web"} <table class="tree-selector list"> <tbody> <tr{if !$parent} class="focused"{/if}> <td><input type="button" value="Choisir" data-path="" data-label="Racine du site" /></td> | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {include file="admin/_head.tpl" title="Choisir la page parent" current="web"} <table class="tree-selector list"> <tbody> <tr{if !$parent} class="focused"{/if}> <td><input type="button" value="Choisir" data-path="" data-label="Racine du site" /></td> <th><h3><a href="?current={$selected}&_dialog">Racine du site</a></h3></th> </tr> <?php $last = 1; ?> {foreach from=$breadcrumbs item="_title" key="_path"} <tr{if $_path == $parent} class="focused"{/if}> <td><input type="button" value="Choisir" data-path="{$_path}" data-label="{$_title}" /></td> <th><?=str_repeat('<i> </i>', $iteration)?> <b class="icn">→</b> <a href="?parent={$_path}&current={$selected}&_dialog">{$_title}</a></th> <?php $last = $iteration; ?> </tr> {/foreach} {foreach from=$categories item="cat"} <tr{if $cat.path == $parent} class="focused"{/if}> <td><input type="button" value="Choisir" data-path="{$cat.path}" data-label="{$cat.title}" /></td> <th><?=str_repeat('<i> </i>', $last+1)?> <b class="icn">→</b> <a href="?parent={$cat.path}&current={$selected}&_dialog">{$cat.title}</a></th> </tr> {foreachelse} <tr> <td></td> <th><?=str_repeat('<i> </i>', $last+1)?> <b class="icn">→</b> <em>Pas de sous-catégorie…</em></th> </tr> {/foreach} |
︙ | ︙ |
Modified src/templates/web/edit.tpl from [4b0b9a3fe9] to [ffcfc7ed08].
︙ | ︙ | |||
10 11 12 13 14 15 16 | <form method="post" action="{$self_url}" class="web-edit" data-focus="#f_content"> <fieldset class="wikiMain"> <legend>Informations générales</legend> <dl> {input type="text" name="title" source=$page required=true label="Titre"} {input type="text" name="uri" default=$page.uri required=true label="Adresse unique URI" help="Utilisée pour désigner l'adresse de la page sur le site. Ne peut comporter que des lettres, des chiffres, des tirets et des tirets bas." pattern="[A-Za-z0-9_-]+"} | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <form method="post" action="{$self_url}" class="web-edit" data-focus="#f_content"> <fieldset class="wikiMain"> <legend>Informations générales</legend> <dl> {input type="text" name="title" source=$page required=true label="Titre"} {input type="text" name="uri" default=$page.uri required=true label="Adresse unique URI" help="Utilisée pour désigner l'adresse de la page sur le site. Ne peut comporter que des lettres, des chiffres, des tirets et des tirets bas." pattern="[A-Za-z0-9_-]+"} {input type="list" name="parent" label="Catégorie" default=$parent target="!web/_selector.php?current=%s&parent=%s"|args:$page.path,$page.parent required=true} {input type="datetime" name="date" label="Date" required=true default=$page.published} <dt>Statut</dt> {input type="radio" name="status" value=$page::STATUS_ONLINE label="En ligne" source=$page} {input type="radio" name="status" value=$page::STATUS_DRAFT label="Brouillon" source=$page help="ne sera pas visible sur le site"} {input type="select" name="format" options=$formats source=$page label="Format"} </dl> </fieldset> |
︙ | ︙ |
Modified src/templates/web/page.tpl from [618d6c6d33] to [704d1904e7].
1 2 3 4 5 6 7 8 | {include file="admin/_head.tpl" title=$page.title current="web"} <nav class="tabs"> {if $page.type == $page::TYPE_CATEGORY} <aside> {linkbutton shape="plus" label="Nouvelle page" href="new.php?type=%d&parent=%d"|args:$type_page,$page.path} {linkbutton shape="plus" label="Nouvelle catégorie" href="new.php?type=%d&parent=%d"|args:$type_category,$page.path} </aside> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {include file="admin/_head.tpl" title=$page.title current="web"} <nav class="tabs"> {if $page.type == $page::TYPE_CATEGORY} <aside> {linkbutton shape="plus" label="Nouvelle page" href="new.php?type=%d&parent=%d"|args:$type_page,$page.path} {linkbutton shape="plus" label="Nouvelle catégorie" href="new.php?type=%d&parent=%d"|args:$type_category,$page.path} </aside> {elseif $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)} <aside> {linkbutton href="?p=%s&toggle_type"|args:$page.path label="Transformer en catégorie" shape="reset"} </aside> {/if} <ul> <li><a href="{$admin_url}web/?p={if $page.type == $page::TYPE_CATEGORY}{$page.path}{else}{$page.parent}{/if}">Retour à la liste</a></li> {if $session->canAccess($session::SECTION_WEB, $session::ACCESS_WRITE)} |
︙ | ︙ |
Modified src/www/.htaccess from [477ff985ec] to [3a5902bc51].
︙ | ︙ | |||
13 14 15 16 17 18 19 | # cf. https://bz.apache.org/bugzilla/show_bug.cgi?id=58292 # et https://serverfault.com/questions/559067/apache-hangs-for-five-seconds-with-fallbackresource-when-accessing # Un peu de sécurité <IfModule mod_alias.c> RedirectMatch 404 _inc\.php </IfModule> | > > > > > > | 13 14 15 16 17 18 19 20 21 22 23 24 25 | # cf. https://bz.apache.org/bugzilla/show_bug.cgi?id=58292 # et https://serverfault.com/questions/559067/apache-hangs-for-five-seconds-with-fallbackresource-when-accessing # Un peu de sécurité <IfModule mod_alias.c> RedirectMatch 404 _inc\.php </IfModule> # This is to avoid caching mismatch when using mod_deflate # see https://github.com/symfony/symfony-docs/issues/12644 <IfModule mod_deflate.c> FileETag None </IfModule> |
Modified src/www/admin/acc/_inc.php from [b6e3c3f264] to [b33504a9a2].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; if (!defined('Garradin\ALLOW_ACCOUNTS_ACCESS') || !ALLOW_ACCOUNTS_ACCESS) { $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); } $current_year_id = $session->get('acc_year'); $current_year = null; if ($current_year_id) { // Check that the year is still valid $current_year = Years::get($current_year_id); |
︙ | ︙ |
Modified src/www/admin/acc/accounts/all.php from [f7050bf620] to [7cd9b8ea94].
1 2 3 | <?php namespace Garradin; | | | < < < < < | | < < < < | < > < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin; use Garradin\Accounting\Reports; require_once __DIR__ . '/../_inc.php'; if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $criterias = ['year' => CURRENT_YEAR_ID]; $tpl->assign('balance', Reports::getTrialBalance($criterias, true)); $tpl->display('acc/accounts/all.tpl'); |
Modified src/www/admin/acc/accounts/reconcile.php from [f870477b9e] to [7aab23bf4d].
︙ | ︙ | |||
49 50 51 52 53 54 55 | if ($start > $end) { $end = clone $start; } $journal = $account->getReconcileJournal($current_year->id(), $start, $end, $only); // Enregistrement des cases cochées | | < | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | if ($start > $end) { $end = clone $start; } $journal = $account->getReconcileJournal($current_year->id(), $start, $end, $only); // Enregistrement des cases cochées $form->runIf(f('save') || f('save_next'), function () use ($journal, $start, $account, $only) { Transactions::saveReconciled($journal, f('reconcile')); if (f('save')) { Utils::redirect(Utils::getSelfURI()); } else { $start->modify('+1 month'); $url = sprintf('%sacc/accounts/reconcile.php?id=%s&start=%s&end=%s&only=%d', ADMIN_URL, $account->id(), $start->format('01/m/Y'), $start->format('t/m/Y'), $only); Utils::redirect($url); } }, 'acc_reconcile_' . $account->id()); $prev = clone $start; $next = clone $start; $prev->modify('-1 month'); |
︙ | ︙ |
Modified src/www/admin/acc/accounts/reconcile_assist.php from [b1a074fbc8] to [e310db5f89].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace Garradin; use Garradin\Accounting\Accounts; use Garradin\Accounting\Transactions; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $account = Accounts::get((int)qg('id')); if (!$account) { throw new UserException("Le compte demandé n'existe pas."); } $csrf_key = 'acc_reconcile_assist_' . $account->id(); | > < < < < < < < < < | | | < | | < < < < < < < | > | | < > | | < | | < | | < < | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | <?php namespace Garradin; use Garradin\Accounting\Accounts; use Garradin\Accounting\Transactions; use Garradin\Accounting\AssistedReconciliation; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $account = Accounts::get((int)qg('id')); if (!$account) { throw new UserException("Le compte demandé n'existe pas."); } $csrf_key = 'acc_reconcile_assist_' . $account->id(); $ar = new AssistedReconciliation; $csv = $ar->csv(); $form->runIf('cancel', function () use ($csv) { $csv->clear(); }, $csrf_key, Utils::getSelfURI()); $form->runIf(f('upload') && isset($_FILES['file']['name']), function () use ($csv) { $csv->load($_FILES['file']); }, $csrf_key, Utils::getSelfURI()); $form->runIf('assign', function () use ($ar) { $ar->setSettings(f('translation_table'), (int)f('skip_first_line')); }, $csrf_key, Utils::getSelfURI()); $start = $end = null; try { extract($ar->getStartAndEndDates()); } catch (UserException $e) { $form->addError($e->getMessage()); $csv->clear(); } $journal = null; if ($start && $end) { if ($start < $current_year->start_date || $start > $current_year->end_date) { $start = clone $current_year->start_date; } if ($end < $current_year->start_date || $end > $current_year->end_date) { $end = clone $current_year->end_date; } $journal = $account->getReconcileJournal(CURRENT_YEAR_ID, $start, $end); } // Enregistrement des cases cochées $form->runIf('save', function () use ($journal, $csv) { Transactions::saveReconciled($journal, f('reconcile')); $csv->clear(); }, $csrf_key, Utils::getSelfURI()); $lines = null; if ($journal && $csv->ready()) { try { $lines = $ar->mergeJournal($journal); } catch (UserException $e) { $form->addError($e->getMessage()); } } $tpl->assign(compact( 'account', 'start', 'end', 'lines', 'ar', 'csv', 'csrf_key' )); $tpl->display('acc/accounts/reconcile_assist.tpl'); |
Modified src/www/admin/acc/accounts/simple.php from [e1797be0df] to [83f17410b3].
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $year = $current_year; $types = [ Transaction::TYPE_REVENUE => 'Recettes', Transaction::TYPE_EXPENSE => 'Dépenses', Transaction::TYPE_TRANSFER => 'Virements', Transaction::TYPE_DEBT => 'Dettes', Transaction::TYPE_CREDIT => 'Créances', Transaction::TYPE_ADVANCED => 'Saisies avancées', ]; | > | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $year = $current_year; $types = [ -1 => 'Toutes', Transaction::TYPE_REVENUE => 'Recettes', Transaction::TYPE_EXPENSE => 'Dépenses', Transaction::TYPE_TRANSFER => 'Virements', Transaction::TYPE_DEBT => 'Dettes', Transaction::TYPE_CREDIT => 'Créances', Transaction::TYPE_ADVANCED => 'Saisies avancées', ]; $type = qg('type'); if (!array_key_exists($type, $types)) { $type = key($types); } $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'); |
Added src/www/admin/acc/accounts/users.php version [01b638e737].
> > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace Garradin; use Garradin\Accounting\Accounts; require_once __DIR__ . '/../_inc.php'; if (!CURRENT_YEAR_ID) { throw new UserException('Aucun exercice sélectionné'); } $accounts = new Accounts($current_year->id_chart); $list = $accounts->listUserAccounts($current_year->id); $list->loadFromQueryString(); $tpl->assign('chart_id', $current_year->id_chart); $tpl->assign(compact('list')); $tpl->display('acc/accounts/users.tpl'); |
Modified src/www/admin/acc/charts/accounts/edit.php from [1dfbc47d52] to [4df5ac9aa9].
︙ | ︙ | |||
26 27 28 29 30 31 32 33 34 35 36 37 38 39 | try { if ($edit_disabled) { $account->importLimitedForm(); } else { $account->importForm(); } $account->save(); $page = ''; if (!$account->type) { $page = 'all.php'; | > > > > > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | try { if ($edit_disabled) { $account->importLimitedForm(); } else { $account->importForm(); } // Force account position from type if ($account->isModified('type') && $account->user) { $account->position = Accounts::getPositionFromType($account->type); } $account->save(); $page = ''; if (!$account->type) { $page = 'all.php'; |
︙ | ︙ |
Modified src/www/admin/acc/charts/accounts/new.php from [317fd65486] to [1d828d1672].
1 2 3 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\Entities\Accounting\Account; use Garradin\Accounting\Accounts; use Garradin\Accounting\Charts; require_once __DIR__ . '/../../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $chart = Charts::get((int)qg('id')); if (!$chart) { throw new UserException('Ce plan comptable n\'existe pas'); } if ($chart->archived) { throw new UserException("Il n'est pas possible de modifier un plan comptable archivé."); } $account = new Account; $account->position = Account::ASSET_OR_LIABILITY; $types = $account::TYPES_NAMES; $types[0] = '-- Pas un compte favori'; | > > > > > < < < < < < < < < | | | < | < | < < | < < | < < > | | | | < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Accounting\Line; use Garradin\Accounting\Accounts; use Garradin\Accounting\Charts; use Garradin\Membres\Session; require_once __DIR__ . '/../../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $chart = Charts::get((int)qg('id')); if (!$chart) { throw new UserException('Ce plan comptable n\'existe pas'); } if ($chart->archived) { throw new UserException("Il n'est pas possible de modifier un plan comptable archivé."); } $accounts = $chart->accounts(); $account = new Account; $account->position = Account::ASSET_OR_LIABILITY; $types = $account::TYPES_NAMES; $types[0] = '-- Pas un compte favori'; // Simple creation with pre-determined account type if (qg('type') !== null) { $account->type = (int)qg('type'); $account->position = Accounts::getPositionFromType($account->type); $account->code = $accounts->getNextCodeForType($account->type); } $form->runIf('save', function () use ($account, $accounts, $chart, $current_year) { $db = DB::getInstance(); $db->begin(); $account->importForm(); $account->id_chart = $chart->id(); $account->user = 1; $account->save(); if (!empty(f('opening_amount')) && $current_year) { $t = new Transaction; $t->label = 'Solde d\'ouverture du compte'; $t->id_creator = Session::getInstance()->getUser()->id; $t->date = clone $current_year->start_date; $t->type = $t::TYPE_ADVANCED; $t->notes = 'Créé automatiquement à l\'ajout du compte'; $t->id_year = $current_year->id; $opening_account = $accounts->getOpeningAccountId(); $amount = Utils::moneyToInteger(f('opening_amount')); $a = $amount > 0 ? 0 : abs($amount); $b = $amount < 0 ? 0 : abs($amount); $t->addLine(Line::create($account->id, $a, $b)); $t->addLine(Line::create($opening_account, $b, $a)); $t->save(); } $db->commit(); $page = ''; if (!$account->type) { $page = 'all.php'; } Utils::redirect(sprintf('%sacc/charts/accounts/%s?id=%d', ADMIN_URL, $page, $account->id_chart)); }, 'acc_accounts_new'); $types_create = [ Account::TYPE_BANK => [ 'label' => Account::TYPES_NAMES[Account::TYPE_BANK], 'help' => 'Compte bancaire, livret, ou intermédiaire financier (type HelloAsso, Paypal, Stripe, SumUp, etc.)', ], Account::TYPE_CASH => [ 'label' => Account::TYPES_NAMES[Account::TYPE_CASH], 'help' => 'Caisse qui sert aux espèces, par exemple la caisse de l\'atelier ou de la boutique.', ], Account::TYPE_OUTSTANDING => [ 'label' => Account::TYPES_NAMES[Account::TYPE_OUTSTANDING], 'help' => 'Paiements qui ont été reçus mais qui ne sont pas encore déposés sur un compte bancaire (typiquement les chèques reçus, qui seront déposés en banque plus tard).', ], Account::TYPE_THIRD_PARTY => [ 'label' => Account::TYPES_NAMES[Account::TYPE_THIRD_PARTY], 'help' => 'Fournisseur, membres de l\'association, collectivités ou services de l\'État par exemple.', ], Account::TYPE_EXPENSE => [ 'label' => Account::TYPES_NAMES[Account::TYPE_EXPENSE], 'help' => 'Compte destiné à recevoir les dépenses (charges)', ], Account::TYPE_REVENUE => [ 'label' => Account::TYPES_NAMES[Account::TYPE_REVENUE], 'help' => 'Compte destiné à recevoir les recettes (produits)', ], Account::TYPE_ANALYTICAL => [ 'label' => Account::TYPES_NAMES[Account::TYPE_ANALYTICAL], 'help' => 'Permet de suivre un budget spécifique, un projet, par exemple : bourse aux vélos, séjour au ski, etc.', ], Account::TYPE_VOLUNTEERING => [ 'label' => Account::TYPES_NAMES[Account::TYPE_VOLUNTEERING], 'help' => 'Pour valoriser le temps de bénévolat, les dons en nature, etc.', ], Account::TYPE_NONE => [ 'label' => 'Autre type de compte', ], ]; $type = $account->type; $tpl->assign(compact('types', 'types_create', 'account', 'chart', 'type')); $tpl->display('acc/charts/accounts/new.tpl'); |
Modified src/www/admin/acc/charts/accounts/selector.php from [f3271645a4] to [0b96176cb9].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Accounting\Charts; use Garradin\Accounting\Years; require_once __DIR__ . '/../../_inc.php'; $targets = qg('targets'); | > > > | > > > < > | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Accounting\Charts; use Garradin\Accounting\Years; const ALLOW_ACCOUNTS_ACCESS = true; require_once __DIR__ . '/../../_inc.php'; $targets = qg('targets'); $targets = $targets ? explode(':', $targets) : []; $chart = (int) qg('chart') ?: null; $targets = array_map('intval', $targets); $targets_str = implode(':', $targets); // Cache the page until the charts have changed $last_change = Config::getInstance()->get('last_chart_change') ?: time(); $hash = sha1($targets_str . $chart . $last_change); // Exit if there's no need to reload Utils::HTTPCache($hash, null, 10); if ($chart) { $chart = Charts::get($chart); } elseif (qg('year')) { $year = Years::get((int)qg('year')); if ($year) { $chart = $year->chart(); } } elseif ($current_year) { $chart = $current_year->chart(); } if (!$chart) { throw new UserException('Aucun exercice ouvert disponible'); } $accounts = $chart->accounts(); $tpl->assign(compact('chart', 'targets', 'targets_str')); $all = qg('all'); if (null !== $all) { $session->set('account_selector_all', (bool) $all); } $all = (bool) $session->get('account_selector_all'); if (!count($targets)) { $tpl->assign('accounts', !$all ? $accounts->listCommonTypes() : $accounts->listAll()); } else { $tpl->assign('grouped_accounts', $accounts->listCommonGrouped($targets)); } $tpl->assign('all', $all); $tpl->display('acc/charts/accounts/selector.tpl'); |
Modified src/www/admin/acc/charts/import.php from [105414a37d] to [a0aa64cc91].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Accounting\Accounts; use Garradin\Accounting\Charts; use Garradin\Entities\Accounting\Chart; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); | | > > | | | | | < | < | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?php namespace Garradin; use Garradin\Accounting\Accounts; use Garradin\Accounting\Charts; use Garradin\Entities\Accounting\Chart; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $form->runIf('import', function () { $db = DB::getInstance(); $db->begin(); $chart = new Chart; $chart->importForm(); $chart->save(); $chart->accounts()->importUpload($_FILES['file']); // This will save everything $db->commit(); }, 'acc_charts_import', '!acc/charts/'); $tpl->assign('columns', implode(', ', Accounts::EXPECTED_CSV_COLUMNS)); $tpl->assign('country_list', Utils::getCountryList()); $tpl->display('acc/charts/import.tpl'); |
Added src/www/admin/acc/charts/install.php version [0b80f2e466].
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Accounting\Charts; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $form->runIf('install', function () { Charts::install(f('code')); }, 'acc_charts_install', '!acc/charts/'); $tpl->assign('list', Charts::listInstallable()); $tpl->display('acc/charts/install.tpl'); |
Modified src/www/admin/acc/index.php from [e3b75e427f] to [0d045132f2].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; use Garradin\Accounting\Years; use Garradin\Accounting\Graph; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); | | > | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Accounting\Years; use Garradin\Accounting\Graph; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); $tpl->assign('graphs', array_slice(Graph::URL_LIST, 0, 3)); $years = Years::listOpen(true); $tpl->assign('years', $years); $tpl->assign('first_year', count($years) ? current($years)->id : null); $tpl->assign('all_years', [null => '-- Tous les exercices'] + Years::listAssoc()); $tpl->assign('last_transactions', Years::listLastTransactions(10, $years)); $tpl->display('acc/index.tpl'); |
Modified src/www/admin/acc/reports/_inc.php from [d05357fbdb] to [e27f27583d].
︙ | ︙ | |||
46 47 48 49 50 51 52 53 54 | } if ($y2 = Years::get((int)qg('compare_year'))) { $tpl->assign('year2', $y2); $criterias['compare_year'] = $y2->id; } $tpl->assign('criterias', $criterias); $tpl->assign('criterias_query', http_build_query($criterias)); | > > > > | 46 47 48 49 50 51 52 53 54 55 56 57 58 | } if ($y2 = Years::get((int)qg('compare_year'))) { $tpl->assign('year2', $y2); $criterias['compare_year'] = $y2->id; } $criterias_query = $criterias; unset($criterias_query['compare_year']); $tpl->assign('criterias', $criterias); $tpl->assign('criterias_query', http_build_query($criterias)); $tpl->assign('criterias_query_no_compare', http_build_query($criterias_query)); |
Modified src/www/admin/acc/reports/graph_pie.php from [f575671169] to [8f8e6c9b90].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/_inc.php'; header('Content-Type: image/svg+xml'); | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/_inc.php'; header('Content-Type: image/svg+xml'); $expiry = time() - 30; $hash = sha1('pie_' . json_encode($criterias)); if (!Utils::HTTPCache($hash, $expiry)) { echo Graph::pie(qg('type'), $criterias); } |
Modified src/www/admin/acc/reports/graph_plot.php from [98d8578b26] to [0af289aaca].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/_inc.php'; header('Content-Type: image/svg+xml'); | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/_inc.php'; header('Content-Type: image/svg+xml'); $expiry = time() - 30; $hash = sha1('plot_' . json_encode($criterias)); if (!Utils::HTTPCache($hash, $expiry)) { echo Graph::plot(qg('type'), $criterias); } |
Modified src/www/admin/acc/reports/graph_plot_all.php from [eb0fafc20b] to [df25f047b8].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/../_inc.php'; qv(['type' => 'string|required']); header('Content-Type: image/svg+xml'); | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/../_inc.php'; qv(['type' => 'string|required']); header('Content-Type: image/svg+xml'); $expiry = time() - 30; $hash = sha1('plot_all'); if (!Utils::HTTPCache($hash, $expiry)) { echo Graph::bar(qg('type'), []); } |
Modified src/www/admin/acc/reports/trial_balance.php from [c1cd5c7e43] to [72f8179dff].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Accounting\Reports; require_once __DIR__ . '/_inc.php'; | > > > > | | 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'; $simple = qg('simple') === null || qg('simple') ? 'simple' : null; $tpl->assign(compact('simple')); $tpl->assign('balance', Reports::getTrialBalance($criterias, (bool) $simple)); $tpl->display('acc/reports/trial_balance.tpl'); |
Modified src/www/admin/acc/transactions/details.php from [b7f3c97ce2] to [2a516510d2].
︙ | ︙ | |||
24 25 26 27 28 29 30 31 32 33 34 | $tpl->assign('files', $transaction->listFiles()); $tpl->assign('tr_year', $transaction->year()); $tpl->assign('creator_name', $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null); $tpl->assign('files_edit', $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)); $tpl->assign('file_parent', $transaction->getAttachementsDirectory()); $tpl->assign('related_users', $transaction->listLinkedUsers()); $tpl->assign('documents', Document::list(Document::CONTEXT_TRANSACTION)); $tpl->assign('doc_params', ['id_transaction' => $transaction->id()]); $tpl->display('acc/transactions/details.tpl'); | > | 24 25 26 27 28 29 30 31 32 33 34 35 | $tpl->assign('files', $transaction->listFiles()); $tpl->assign('tr_year', $transaction->year()); $tpl->assign('creator_name', $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null); $tpl->assign('files_edit', $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)); $tpl->assign('file_parent', $transaction->getAttachementsDirectory()); $tpl->assign('related_users', $transaction->listLinkedUsers()); $tpl->assign('related_transactions', $transaction->listRelatedTransactions()); $tpl->assign('documents', Document::list(Document::CONTEXT_TRANSACTION)); $tpl->assign('doc_params', ['id_transaction' => $transaction->id()]); $tpl->display('acc/transactions/details.tpl'); |
Modified src/www/admin/acc/transactions/new.php from [0cd4c89e75] to [4b7865c3e6].
1 2 3 4 5 6 7 8 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\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Files\File; use Garradin\Accounting\Transactions; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $chart = $current_year->chart(); $accounts = $chart->accounts(); $transaction = new Transaction; $lines = [[], []]; $amount = 0; $types_accounts = null; $id_analytical = null; // Duplicate transaction if (qg('copy')) { $old = Transactions::get((int)qg('copy')); if (!$old) { throw new UserException('Cette écriture n\'existe pas (ou plus).'); | > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Entities\Files\File; use Garradin\Accounting\AssistedReconciliation; use Garradin\Accounting\Transactions; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); if (!CURRENT_YEAR_ID) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN'); } $chart = $current_year->chart(); $accounts = $chart->accounts(); $transaction = new Transaction; $lines = [[], []]; $amount = 0; $types_accounts = null; $id_analytical = null; // Quick-fill transaction from query parameters if (qg('a')) { $amount = Utils::moneyToInteger(qg('a')); } if (qg('l')) { $transaction->label = qg('l'); } if (qg('d')) { $transaction->date = new \DateTime(qg('d')); } if (qg('t')) { $transaction->type = (int) qg('t'); } // Duplicate transaction if (qg('copy')) { $old = Transactions::get((int)qg('copy')); if (!$old) { throw new UserException('Cette écriture n\'existe pas (ou plus).'); |
︙ | ︙ | |||
42 43 44 45 46 47 48 | foreach ($lines as $k => &$line) { $line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_name)]; } unset($line); } | > > | | > | | > | | < < | 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 | foreach ($lines as $k => &$line) { $line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_name)]; } unset($line); } // Set last used date if (empty($transaction->date) && $session->get('acc_last_date') && $date = \DateTime::createFromFormat('!Y-m-d', $session->get('acc_last_date'))) { $transaction->date = $date; } // Set date of the day if no date was set elseif (empty($transaction->date)) { $transaction->date = new \DateTime; } // Make sure the date cannot be outside of the current year if ($transaction->date < $current_year->start_date || $transaction->date > $current_year->end_date) { $transaction->date = $current_year->start_date; } // Quick transaction from an account journal page if ($id = qg('account')) { $account = $accounts::get($id); if (!$account || $account->id_chart != $current_year->id_chart) { throw new UserException('Ce compte ne correspond pas à l\'exercice comptable ou n\'existe pas'); } |
︙ | ︙ | |||
90 91 92 93 94 95 96 | $transaction->save(); // Link members if (null !== f('users') && is_array(f('users'))) { $transaction->updateLinkedUsers(array_keys(f('users'))); } | | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | $transaction->save(); // Link members if (null !== f('users') && is_array(f('users'))) { $transaction->updateLinkedUsers(array_keys(f('users'))); } $session->set('acc_last_date', $transaction->date->format('Y-m-d')); Utils::redirect(Utils::getSelfURI(false) . '?ok=' . $transaction->id()); }, 'acc_transaction_new'); $tpl->assign(compact('transaction', 'amount', 'lines', 'types_accounts', 'id_analytical')); $tpl->assign('ok', (int) qg('ok')); $tpl->assign('types_details', Transaction::getTypesDetails()); $tpl->assign('chart_id', $chart->id()); $tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical()); $tpl->display('acc/transactions/new.tpl'); |
Modified src/www/admin/acc/transactions/service_user.php from [6aaa90de39] to [6ef72d23d2].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; use Garradin\Accounting\Reports; use Garradin\Accounting\Years; require_once __DIR__ . '/../../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin; use Garradin\Accounting\Reports; use Garradin\Accounting\Years; require_once __DIR__ . '/../../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); $criterias = ['subscription' => (int)qg('id')]; $tpl->assign('balance', Reports::getAccountsBalances($criterias)); $tpl->assign('journal', Reports::getJournal($criterias)); $tpl->assign('user_id', qg('user')); $tpl->assign('service_user_id', qg('id')); $tpl->display('acc/transactions/service_user.tpl'); |
Modified src/www/admin/acc/transactions/user.php from [4b78a375e7] to [c553e49b64].
︙ | ︙ | |||
16 17 18 19 20 21 22 | $years = Years::listAssoc(); end($years); $year = (int)qg('year') ?: key($years); $criterias = ['user' => $u->id]; | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 | $years = Years::listAssoc(); end($years); $year = (int)qg('year') ?: key($years); $criterias = ['user' => $u->id]; $tpl->assign('balance', Reports::getAccountsBalances($criterias + ['year' => $year], null, false)); $tpl->assign('journal', Reports::getJournal($criterias)); $tpl->assign(compact('years', 'year')); $tpl->assign('transaction_user', $u); $tpl->display('acc/transactions/user.tpl'); |
Modified src/www/admin/acc/years/balance.php from [3a08646ae0] to [60df387c7b].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Accounting\Reports; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $year = Years::get((int)qg('id')); if (!$year) { throw new UserException('Exercice inconnu.'); } if ($year->closed) { throw new UserException('Impossible de modifier un exercice clôturé.'); } | > | | | | | | | > > > | > > > | < | < > | > > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | <?php namespace Garradin; use Garradin\Entities\Accounting\Account; use Garradin\Entities\Accounting\Transaction; use Garradin\Accounting\Reports; use Garradin\Accounting\Years; use Garradin\Membres\Session; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $year = Years::get((int)qg('id')); if (!$year) { throw new UserException('Exercice inconnu.'); } if ($year->closed) { throw new UserException('Impossible de modifier un exercice clôturé.'); } $csrf_key = 'acc_years_balance_' . $year->id(); $form->runIf('save', function () use ($year) { $transaction = new Transaction; $transaction->id_creator = Session::getInstance()->getUser()->id; $transaction->importFromBalanceForm($year); $transaction->save(); if (f('appropriation')) { // (affectation du résultat) $t2 = Years::makeAppropriation($year); if ($t2) { $t2->id_creator = $transaction->id_creator; $t2->save(); } Utils::redirect('!acc/reports/journal.php?year=' . $year->id()); } Utils::redirect('!acc/transactions/details.php?id=' . $transaction->id()); }, $csrf_key); $previous_year = null; $year_selected = f('from_year') !== null; $chart_change = false; $lines = [[]]; $years = Years::list(true); // Empty balance if (!count($years) || f('from_year') === '') { $previous_year = 0; } elseif (null !== f('from_year')) { $previous_year = (int)f('from_year'); $previous_year = Years::get($previous_year); if (!$previous_year) { throw new UserException('Année précédente invalide'); } } if ($previous_year) { $lines = Reports::getAccountsBalances(['year' => $previous_year->id(), 'exclude_position' => [Account::EXPENSE, Account::REVENUE]]); if ($previous_year->id_chart != $year->id_chart) { $chart_change = true; $codes = []; foreach ($lines as $line) { $codes[] = $line->code; |
︙ | ︙ | |||
84 85 86 87 88 89 90 | 'id' => null, 'code' => null, 'label' => null, ]; } $lines[] = (object) [ | | | | | | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 'id' => null, 'code' => null, 'label' => null, ]; } $lines[] = (object) [ 'balance' => $result, 'id' => $account->id, 'code' => $account->code, 'label' => $account->label, 'is_debt' => $result < 0, ]; foreach ($lines as $k => &$line) { $line->credit = !$line->is_debt ? abs($line->balance) : 0; $line->debit = $line->is_debt ? abs($line->balance) : 0; if ($chart_change) { if (array_key_exists($line->code, $matching_accounts)) { $acc = $matching_accounts[$line->code]; $line->account = [$acc->id => sprintf('%s — %s', $acc->code, $acc->label)]; } } |
︙ | ︙ | |||
120 121 122 123 124 125 126 | foreach ($lines as &$line) { $line['credit'] = Utils::moneyToInteger($line['credit']); $line['debit'] = Utils::moneyToInteger($line['debit']); } } | < | | 129 130 131 132 133 134 135 136 137 138 | foreach ($lines as &$line) { $line['credit'] = Utils::moneyToInteger($line['credit']); $line['debit'] = Utils::moneyToInteger($line['debit']); } } $tpl->assign(compact('lines', 'years', 'chart_change', 'previous_year', 'year_selected', 'year', 'csrf_key')); $tpl->display('acc/years/balance.tpl'); |
Modified src/www/admin/acc/years/export.php from [5fa4e489df] to [2f3ec0ba23].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; use Garradin\Accounting\Transactions; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); | | > > > > > > > > > > > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <?php namespace Garradin; use Garradin\Accounting\Transactions; 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); } if (!$year) { throw new UserException("L'exercice demandé n'existe pas."); } $format = qg('format'); $type = qg('type'); if (null !== $format && null !== $type) { Transactions::export($year, $format, $type); exit; } $examples = Transactions::getExportExamples($year); $types = [ Transactions::EXPORT_FULL => [ 'label' => 'Complet (comptabilité d\'engagement)', 'help' => '(Conseillé pour transfert vers un autre logiciel) Chaque ligne reprend toutes les informations de la ligne et de l\'écriture.', ], Transactions::EXPORT_GROUPED => [ 'label' => 'Complet groupé', 'help' => 'Idem, sauf que les lignes suivantes ne mentionnent pas les informations de l\'écriture.', ], Transactions::EXPORT_SIMPLE => [ 'label' => 'Simplifié (comptabilité de trésorerie)', 'help' => 'Les écritures avancées ne sont pas inclues dans cet export.', ], ]; $tpl->assign(compact('year', 'examples', 'types')); $tpl->display('acc/years/export.tpl'); |
Modified src/www/admin/acc/years/import.php from [2d7d375a16] to [1f7549983a].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; use Garradin\Accounting\Transactions; use Garradin\Accounting\Years; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); | | < < < | < > > > > > | < > > | < > > > > > | | | | | | < < < < < | | > | | < > > | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; use Garradin\Accounting\Transactions; 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); } if (!$year) { throw new UserException("L'exercice demandé n'existe pas."); } if (qg('export')) { Transactions::export($year->id()); exit; } if ($year->closed) { throw new UserException('Impossible de modifier un exercice clôturé.'); } $type = qg('type'); $type_name = Transactions::EXPORT_NAMES[$type] ?? null; $csrf_key = 'acc_years_import_' . $year->id(); $examples = null; $csv = new CSV_Custom($session, 'acc_import_year'); $ignore_ids = (bool) (f('ignore_ids') ?? qg('ignore_ids')); $params = ['year' => $year->id(), 'ignore_ids' => (int) $ignore_ids, 'type' => $type]; if (f('cancel')) { $csv->clear(); unset($params['type']); Utils::redirect(Utils::getSelfURI($params)); } if ($type && $type_name) { $columns = Transactions::EXPORT_COLUMNS[$type]; unset($columns['linked_users']); $csv->setColumns($columns); $csv->setMandatoryColumns(Transactions::MANDATORY_COLUMNS[$type]); $form->runIf(f('assign') && $csv->loaded(), function () use ($type, $csv, $year, $user, $ignore_ids) { $csv->skip((int)f('skip_first_line')); $csv->setTranslationTable(f('translation_table')); Transactions::import($type, $year, $csv, $user->id, (bool) $ignore_ids); $csv->clear(); }, $csrf_key, ADMIN_URL . 'acc/years/?msg=IMPORT'); $form->runIf(f('load') && isset($_FILES['file']['tmp_name']), function () use ($type, $csv, $year, $params) { $csv->load($_FILES['file']); Utils::redirect(Utils::getSelfURI($params)); }, $csrf_key); } else { $csv->clear(); $examples = Transactions::getExportExamples($year); } $tpl->assign(compact('csv', 'year', 'csrf_key', 'examples', 'type', 'type_name', 'ignore_ids')); $tpl->display('acc/years/import.tpl'); |
Modified src/www/admin/acc/years/new.php from [089708c6e5] to [caf2a0665e].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use Garradin\Accounting\Years; use Garradin\Accounting\Charts; use Garradin\Services\Fees; use Garradin\Entities\Accounting\Year; require_once __DIR__ . '/../../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); | < | > > | > > | > > > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?php namespace Garradin; use Garradin\Accounting\Years; use Garradin\Accounting\Charts; use Garradin\Services\Fees; use Garradin\Entities\Accounting\Year; require_once __DIR__ . '/../../_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN); $year = new Year; $form->runIf('new', function () use ($year) { $year->importForm(); $year->save(); $old_id = qg('from'); if ($old_id) { $old = Years::get((int) $old_id); $changed = Fees::updateYear($old, $year); if (!$changed) { Utils::redirect(ADMIN_URL . 'acc/years/?msg=UPDATE_FEES'); } } if (Years::countClosed()) { Utils::redirect(ADMIN_URL . 'acc/years/balance.php?from=' . $old_id . '&id=' . $year->id()); } }, 'acc_years_new', '!acc/years/'); $new_dates = Years::getNewYearDates(); $year->start_date = $new_dates[0]; $year->end_date = $new_dates[1]; $year->label = sprintf('Exercice %s', $year->label_years()); $tpl->assign(compact('year')); $tpl->assign('charts', Charts::listByCountry(true)); $tpl->display('acc/years/new.tpl'); |
Modified src/www/admin/common/files/delete.php from [f68ffe55ca] to [eaeee76565].
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 | $context = $file->context(); if ($context == File::CONTEXT_CONFIG || $context == File::CONTEXT_WEB) { throw new UserException('Vous n\'avez pas le droit de supprimer ce fichier.'); } $csrf_key = 'file_delete_' . $file->pathHash(); $form->runIf('delete', function () use ($file) { $file->delete(); | > | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | $context = $file->context(); if ($context == File::CONTEXT_CONFIG || $context == File::CONTEXT_WEB) { throw new UserException('Vous n\'avez pas le droit de supprimer ce fichier.'); } $csrf_key = 'file_delete_' . $file->pathHash(); $parent = $file->parent; $form->runIf('delete', function () use ($file) { $file->delete(); }, $csrf_key, '!docs/?path=' . $parent); $tpl->assign(compact('file', 'csrf_key')); $tpl->display('common/files/delete.tpl'); |
Modified src/www/admin/common/files/rename.php from [c1d61a3f81] to [9458129839].
︙ | ︙ | |||
18 19 20 21 22 23 24 | $context = $file->context(); $csrf_key = 'file_rename_' . $file->pathHash(); $form->runIf('rename', function () use ($file) { $file->changeFileName(f('new_name')); | | | 18 19 20 21 22 23 24 25 26 27 28 29 | $context = $file->context(); $csrf_key = 'file_rename_' . $file->pathHash(); $form->runIf('rename', function () use ($file) { $file->changeFileName(f('new_name')); }, $csrf_key, '!docs/?path=' . $file->parent); $tpl->assign(compact('file', 'csrf_key')); $tpl->display('common/files/rename.tpl'); |
Modified src/www/admin/common/search.php from [f310a1310c] to [4f62b55a04].
︙ | ︙ | |||
18 19 20 21 22 23 24 | 'order' => f('order') ?: $recherche->getDefaultOrder($target), 'limit' => f('limit') ?: 100, 'desc' => $recherche->getDefaultDesc($target), ]; $query->desc = (bool) f('desc'); | | > > > > > > > | 18 19 20 21 22 23 24 25 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 | 'order' => f('order') ?: $recherche->getDefaultOrder($target), 'limit' => f('limit') ?: 100, 'desc' => $recherche->getDefaultDesc($target), ]; $query->desc = (bool) f('desc'); $text_query = trim((string) qg('qt')); $result = null; $sql_query = null; $search = null; $id = f('id') ?: qg('id'); $is_unprotected = false; // Recherche simple if ($text_query !== '' && $target === 'membres' && empty($query->query)) { $query = $recherche->buildSimpleMemberQuery($text_query); } elseif ($text_query !== '' && $target == 'compta' && empty($query->query)) { $query = $recherche->buildSimpleAccountingQuery($text_query, (int) qg('year')); if (is_string($query)) { Utils::redirect($query); } } // Recherche existante elseif ($id && empty($query->query)) { $search = $recherche->get($id); if (!$search) { |
︙ | ︙ |
Added src/www/admin/config/advanced/api.php version [e6e975d2c5].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 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\API_Credentials; use Garradin\Entities\API_Credentials AS API_Entity; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'api_edit'; $secret = null; $form->runIf('add', function () { API_Credentials::create(); }, $csrf_key, Utils::getSelfURI()); $form->runIf('delete', function () { API_Credentials::delete((int)f('id')); }, $csrf_key, Utils::getSelfURI()); $list = API_Credentials::list(); $default_key = API_Credentials::generateKey(); $secret = API_Credentials::generateSecret(); $access_levels = API_Entity::ACCESS_LEVELS; $tpl->assign('website', WEBSITE); $tpl->assign(compact('list', 'csrf_key', 'default_key', 'secret', 'access_levels')); $tpl->display('admin/config/advanced/api.tpl'); |
Modified src/www/admin/config/advanced/errors.php from [99e2c2d1c3] to [b9d8e0a904].
︙ | ︙ | |||
42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 'message' => $report->errors[0]->message, 'source' => sprintf('%s:%d', $report->errors[0]->backtrace[0]->file, $report->errors[0]->backtrace[0]->line), 'count' => 0, ]; } $errors[$report->context->id]['last_seen'] = $report->context->date; $errors[$report->context->id]['count']++; } $tpl->assign('errors', $errors); } $tpl->display('admin/config/advanced/errors.tpl'); | > | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 'message' => $report->errors[0]->message, 'source' => sprintf('%s:%d', $report->errors[0]->backtrace[0]->file, $report->errors[0]->backtrace[0]->line), 'count' => 0, ]; } $errors[$report->context->id]['last_seen'] = $report->context->date; $errors[$report->context->id]['hostname'] = $report->context->hostname ?? null; $errors[$report->context->id]['count']++; } $tpl->assign('errors', $errors); } $tpl->display('admin/config/advanced/errors.tpl'); |
Modified src/www/admin/config/advanced/index.php from [391415153c] to [a26a70916a].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use Garradin\Accounting\Years; use Garradin\Files\Files; require_once __DIR__ . '/../_inc.php'; $quota_used = Files::getUsedQuota(true); $form->runIf('reset_ok', function () use ($session) { Install::reset($session, f('passe_verif')); | | | 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\Years; use Garradin\Files\Files; require_once __DIR__ . '/../_inc.php'; $quota_used = Files::getUsedQuota(true); $form->runIf('reset_ok', function () use ($session) { Install::reset($session, f('passe_verif')); }, 'reset'); $form->runIf('reopen_ok', function () use ($session) { $year = Years::get((int) f('year')); $year->reopen($session->getUser()->id); }, 'reopen_year', Utils::getSelfURI(['msg' => 'REOPEN'])); if (FILE_STORAGE_BACKEND !== 'SQLite' && ENABLE_TECH_DETAILS) { |
︙ | ︙ |
Modified src/www/admin/config/advanced/sql.php from [33f27e5775] to [094e2d2540].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use KD2\ErrorManager; require_once __DIR__ . '/../_inc.php'; $list = null; $table = qg('table'); | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin; use KD2\ErrorManager; require_once __DIR__ . '/../_inc.php'; $list = null; $table = qg('table'); $query = f('query') ?? qg('query'); $db = DB::getInstance(); $tables_list = $db->getGrouped('SELECT name, sql, NULL AS count FROM sqlite_master WHERE type = \'table\' ORDER BY name;'); $index_list = null; $triggers_list = null; $result = null; $result_header = null; |
︙ | ︙ |
Added src/www/admin/config/advanced/sql_debug.php version [ce67b51f29].
> > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php namespace Garradin; use KD2\ErrorManager; require_once __DIR__ . '/../_inc.php'; if (!ENABLE_TECH_DETAILS || !SQL_DEBUG) { throw new UserException('Détails techniques ou debug SQL désactivés'); } DB::getInstance()->disableLog(); if (qg('id')) { $tpl->assign('debug', DB::getDebugSession(qg('id'))); } else { $tpl->assign('list', DB::getDebugSessionsList()); } $tpl->display('admin/config/advanced/sql_debug.tpl'); |
Modified src/www/admin/config/backup/documents.php from [185acbc75a] to [a7f4d2c329].
︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 41 42 43 44 45 | }, 'files_download'); $ok = qg('ok') !== null; $failed = (int) qg('failed'); if ($ok) { // Reset $config->updateFiles(); $config->save(); $tpl->assign(compact('config')); Web::sync(true); Static_Cache::clean(0); | > | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | }, 'files_download'); $ok = qg('ok') !== null; $failed = (int) qg('failed'); if ($ok) { // Reset $config = Config::getInstance(); $config->updateFiles(); $config->save(); $tpl->assign(compact('config')); Web::sync(true); Static_Cache::clean(0); |
︙ | ︙ |
Modified src/www/admin/config/backup/restore.php from [c5bad54374] to [02133a1ff3].
︙ | ︙ | |||
25 26 27 28 29 30 31 | throw new UserException('Aucune sauvegarde sélectionnée'); } $s->remove(f('selected')); }, 'backup_manage', Utils::getSelfURI(['ok' => 'remove'])); | | | > > | < > | > | 25 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 | throw new UserException('Aucune sauvegarde sélectionnée'); } $s->remove(f('selected')); }, 'backup_manage', Utils::getSelfURI(['ok' => 'remove'])); $form->runIf('restore_file', function () use ($s, &$code, $session, $form) { // Ignorer la vérification d'intégrité si autorisé et demandé $check = (ALLOW_MODIFIED_IMPORT && f('force_import')) ? false : true; try { $r = $s->restoreFromUpload($_FILES['file'], $check); Utils::redirect(Utils::getSelfURI(['ok' => 'restore', 'code' => (int)$r])); } catch (UserException $e) { $code = $e->getCode(); if ($code == 0) { throw $e; } $form->addError($e->getMessage()); } }, 'backup_restore'); $ok_code = qg('code'); // return code $ok = qg('ok'); // return message $list = $s->getList(); $tpl->assign(compact('code', 'list', 'ok', 'ok_code')); $tpl->display('admin/config/backup/restore.tpl'); |
Modified src/www/admin/config/backup/save.php from [9f5c67bf3b] to [c274dbc0c6].
︙ | ︙ | |||
15 16 17 18 19 20 21 | // Create local backup $form->runIf('create', function () use ($s) { $s->create(); }, 'backup_create', Utils::getSelfURI(['ok' => 'create'])); $form->runIf('config', function () { | < < < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Create local backup $form->runIf('create', function () use ($s) { $s->create(); }, 'backup_create', Utils::getSelfURI(['ok' => 'create'])); $form->runIf('config', function () { $frequency = (int) f('frequence_sauvegardes'); if ($frequency < 0 || $frequency > 365) { throw new UserException('Fréquence invalide'); } $number = (int) f('nombre_sauvegardes'); |
︙ | ︙ |
Modified src/www/admin/config/edit_file.php from [15de1c6be2] to [7ce9030a67].
︙ | ︙ | |||
26 27 28 29 30 31 32 | $form->runIf('reset', function () use ($key, $config) { $config->setFile($key, null); $config->save(); }, $csrf_key, Utils::getSelfURI()); $form->runIf('save', function () use ($key, $config) { | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | $form->runIf('reset', function () use ($key, $config) { $config->setFile($key, null); $config->save(); }, $csrf_key, Utils::getSelfURI()); $form->runIf('save', function () use ($key, $config) { $content = trim((string) f('content')); $config->setFile($key, $content === '' ? null : $content); $config->save(); if (qg('js') !== null) { die('{"success":true}'); } |
︙ | ︙ |
Modified src/www/admin/config/index.php from [a6385e7542] to [746271d6a0].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Users\Categories; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $config = Config::getInstance(); $form->runIf('save', function () use ($config) { $config->importForm(); $config->save(); }, 'config', Utils::getSelfURI(['ok' => ''])); | > > > > > | | 1 2 3 4 5 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\Users\Categories; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; if (qg('check_version') !== null) { echo json_encode(Upgrade::fetchLatestVersion()); exit; } $config = Config::getInstance(); $form->runIf('save', function () use ($config) { $config->importForm(); $config->save(); }, 'config', Utils::getSelfURI(['ok' => ''])); $latest = Upgrade::getLatestVersion(); if (null !== $latest) { $latest = $latest->version; } $tpl->assign([ 'garradin_version' => garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']', |
︙ | ︙ |
Modified src/www/admin/config/upgrade.php from [423a4f1508] to [335fdb5c42].
︙ | ︙ | |||
33 34 35 36 37 38 39 | $tpl->assign('downloaded', true); $tpl->assign('verified', $i->verify(f('download'))); $tpl->assign('diff', $i->diff(f('download'))); $tpl->assign('version', f('download')); }, $csrf_key); $form->runIf('upgrade', function () use ($i) { | < > | > > | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | $tpl->assign('downloaded', true); $tpl->assign('verified', $i->verify(f('download'))); $tpl->assign('diff', $i->diff(f('download'))); $tpl->assign('version', f('download')); }, $csrf_key); $form->runIf('upgrade', function () use ($i) { $i->upgrade(f('upgrade')); sleep(2); $url = ADMIN_URL . 'upgrade.php'; printf('<h2>Cliquez ici pour terminer la mise a jour :</h2><form method="get" action="%s"><button type="submit">Continuer</button></form>', $url); exit; }, $csrf_key); $tpl->assign('website', WEBSITE); $tpl->assign(compact('releases', 'latest', 'csrf_key')); $tpl->display('admin/config/upgrade.tpl'); |
Modified src/www/admin/docs/index.php from [7fe6a0182d] to [1b21a8a97c].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $path = qg('path') ?: File::CONTEXT_DOCUMENTS; $context = Files::getContext($path); $context_ref = Files::getContextRef($path); $list = null; // Specific lists for some contexts if ($context == File::CONTEXT_TRANSACTION) { |
︙ | ︙ |
Modified src/www/admin/docs/new_dir.php from [d8847a425e] to [5427985226].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $parent = qg('path'); if (!File::checkCreateAccess($parent, $session)) { throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.'); } $csrf_key = 'create_dir'; $form->runIf('create', function () use ($parent) { $name = trim((string) f('name')); File::validatePath($parent . '/' . $name); $f = File::createDirectory($parent, $name); $url = '!docs/?path=' . $f->path; if (null !== qg('_dialog')) { Utils::reloadParentFrame($url); |
︙ | ︙ |
Modified src/www/admin/docs/new_file.php from [4505ff7d98] to [8c1dc0d614].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $parent = qg('path'); if (!File::checkCreateAccess($parent, $session)) { throw new UserException('Vous n\'avez pas le droit de créer de répertoire ici.'); } $csrf_key = 'create_file'; $form->runIf('create', function () use ($parent) { $name = trim((string) f('name')); if (!strpos($name, '.')) { $name .= '.skriv'; } File::validatePath($parent . '/' . $name); $name = File::filterName($name); |
︙ | ︙ |
Modified src/www/admin/docs/search.php from [553721a33e] to [8ca80671b0].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $q = trim((string) f('q')); $tpl->assign('query', $q); if ($q) { $r = Files::search($q, File::CONTEXT_DOCUMENTS . '%'); $tpl->assign('results', $r); $tpl->assign('results_count', count($r)); |
︙ | ︙ |
Modified src/www/admin/docs/zip.php from [21577262c1] to [9c06488dfb].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Files\Files; use Garradin\Files\Transactions; use Garradin\Files\Users; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $path = qg('path') ?: File::CONTEXT_DOCUMENTS; $name = preg_replace('/[^\p{L}_-]+/i', '_', $path); $name = sprintf('%s - Fichiers - %s.zip', Config::getInstance()->get('nom_asso'), $name); header('Content-type: application/zip'); header(sprintf('Content-Disposition: attachment; filename="%s"', $name)); Files::zip($path, $session); |
Deleted src/www/admin/email.php version [0a473d143d].
|
| < < < < < < < < < < < < < < < < < < |
Added src/www/admin/handle_bounce.php version [250e0cfa82].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <?php namespace Garradin; use Garradin\Users\Emails; require_once __DIR__ . '/../../include/init.php'; function error(int $http_code, string $message) { $http_statuses = [ 202 => 'Accepted', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', ]; header(sprintf('%s %d %s', $_SERVER['SERVER_PROTOCOL'], $http_code, $http_statuses[$http_code]), true, $http_code); echo $message . PHP_EOL; exit; } if (empty(MAIL_BOUNCE_PASSWORD) || empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW']) || $_SERVER['PHP_AUTH_USER'] != 'bounce' || $_SERVER['PHP_AUTH_PW'] != MAIL_BOUNCE_PASSWORD) { error(403, 'Invalid credentials'); } if (empty($_POST['message'])) { error(400, 'Missing or invalid required parameters'); } Emails::handleBounce($_POST['message']); error(202, 'OK'); |
Modified src/www/admin/index.php from [bb797c6a32] to [59ec07f24d].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $banner = null; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Files\Files; use Garradin\Entities\Files\File; require_once __DIR__ . '/_inc.php'; $banner = null; Plugin::fireSignal('home.banner', ['user' => $user, '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 { |
︙ | ︙ |
Modified src/www/admin/install.php from [f605686443] to [76f9b18d41].
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 | if (file_exists(DB_FILE)) { throw new UserException('Garradin est déjà installé'); } try { Install::checkAndCreateDirectories(); } catch (UserException $e) { echo $e->getMessage(); exit; } function f($key) | > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | if (file_exists(DB_FILE)) { throw new UserException('Garradin est déjà installé'); } try { Install::checkAndCreateDirectories(); Install::checkReset(); } catch (UserException $e) { echo $e->getMessage(); exit; } function f($key) |
︙ | ︙ |
Modified src/www/admin/login.php from [052d532ffd] to [0c05de3b09].
︙ | ︙ | |||
26 27 28 29 30 31 32 | } $champs = $config->get('champs_membres'); $id_field = (object) $champs->get($config->get('champ_identifiant')); $id_field_name = $id_field->title; $form->runIf('login', function () use ($id_field_name, $session) { | | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | } $champs = $config->get('champs_membres'); $id_field = (object) $champs->get($config->get('champ_identifiant')); $id_field_name = $id_field->title; $form->runIf('login', function () use ($id_field_name, $session) { if (!trim((string) f('_id'))) { throw new UserException(sprintf('L\'identifiant (%s) n\'a pas été renseigné.', $id_field_name)); } if (!trim((string) f('password'))) { throw new UserException('Le mot de passe n\'a pas été renseigné.'); } if (!$session->login(f('_id'), f('password'), (bool) f('permanent'))) { throw new UserException(sprintf("Connexion impossible.\nVérifiez votre identifiant (%s) et votre mot de passe.", $id_field_name)); } }, 'login', ADMIN_URL); |
︙ | ︙ |
Modified src/www/admin/me/services.php from [29032889e2] to [127612d905].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php namespace Garradin; use Garradin\Services\Services_User; require_once __DIR__ . '/../_inc.php'; $tpl->assign('membre', $user); $list = Services_User::perUserList($user->id); $list->loadFromQueryString(); $tpl->assign(compact('list')); $tpl->assign('services', Services_User::listDistinctForUser($user->id)); $tpl->display('me/services.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\Services\Services_User; use Garradin\Accounting\Reports; use Garradin\Entities\Accounting\Account; require_once __DIR__ . '/../_inc.php'; $tpl->assign('membre', $user); $list = Services_User::perUserList($user->id); $list->loadFromQueryString(); $tpl->assign(compact('list')); $tpl->assign('services', Services_User::listDistinctForUser($user->id)); $tpl->assign('accounts', Reports::getAccountsBalances(['user' => $user->id, 'type' => Account::TYPE_THIRD_PARTY])); $tpl->display('me/services.tpl'); |
Added src/www/admin/membres/emails.php version [68d3590a53].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php namespace Garradin; use Garradin\Users\Emails; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE); if ($address = qg('verify')) { $email = Emails::getEmail($address); if (!$email) { throw new UserException('Adresse invalide'); } $csrf_key = 'send_verification'; $form->runIf('send', function () use ($email, $address) { $email->sendVerification($address); }, $csrf_key, '!membres/emails.php?sent', true); $tpl->assign(compact('csrf_key', 'email')); $tpl->display('admin/membres/emails_verification.tpl'); exit; } $list = Emails::listRejectedUsers(); $list->loadFromQueryString(); $max_fail_count = Emails::FAIL_LIMIT; $queue_count = Emails::countQueue(); $tpl->assign(compact('list', 'max_fail_count', 'queue_count')); $tpl->display('admin/membres/emails.tpl'); |
Modified src/www/admin/membres/message_collectif.php from [7a50fc77e3] to [771f06599a].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Users\Categories; require_once __DIR__ . '/_inc.php'; $recherche = new Recherche; | > > > | < | | | < | < | | > | | < | | < < > | | > | > | < | < < | > > | > > | | < | | | < | | < < < < | | < | < < | > > | | < > > > > > | > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; use Garradin\Users\Categories; use Garradin\Users\Emails; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE); $recherche = new Recherche; $csrf_key = 'send_mailing'; $form->runIf(f('send') || f('subject'), function () use ($membres, &$mailing, $recherche) { if (!trim(f('subject'))) { throw new UserException('Le sujet ne peut rester vide.'); } if (!trim(f('message'))) { throw new UserException('Le message ne peut rester vide.'); } if (!f('target')) { throw new UserException('Aucun destinataire sélectionné.'); } $target = explode('_', f('target')); if (count($target) !== 2) { throw new UserException('Destinataire invalide'); } if ($target[0] == 'all') { $recipients = $membres->listAllButHidden(); } elseif ($target[0] == 'category') { $recipients = $membres->listAllByCategory($target[1], true); } elseif ($target[0] == 'search') { $recipients = $recherche->search($target[1], ['membres.*'], true); } if (empty($recipients)) { throw new UserException('La liste de destinataires sélectionnée ne comporte aucun membre, ou aucun avec une adresse e-mail renseignée.'); } $mailing = Emails::createMailing($recipients, f('subject'), f('message'), (bool) f('send_copy'), f('render') ?: null); }, $csrf_key); $form->runIf('export', function() use ($mailing) { Emails::exportMailing(f('export'), $mailing); exit; }); $form->runIf('send', function () use ($membres, $mailing) { Emails::sendMailing($mailing); }, $csrf_key, '!membres/message_collectif.php?sent'); $tpl->assign('categories', Categories::listNotHidden()); $tpl->assign('preview', f('preview') && $mailing ? $mailing->preview : null); $tpl->assign('recipients_count', $mailing ? count($mailing->recipients) : 0); $tpl->assign('search_list', $recherche->getList($user->id, 'membres')); $tpl->assign('render_formats', Emails::RENDER_FORMATS); $tpl->assign(compact('csrf_key')); $tpl->assign('sent', null !== qg('sent')); $tpl->display('admin/membres/message_collectif.tpl'); |
Modified src/www/admin/membres/selector.php from [325272ec6c] to [64ef2d09da].
1 2 3 4 5 | <?php namespace Garradin; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin; require_once __DIR__ . '/_inc.php'; $text_query = trim((string) (qg('q') ?? f('q'))); $tpl->assign('list', []); // Recherche simple if ($text_query !== '') { $tpl->assign('list', (new Membres)->quickSearch($text_query)); } $tpl->assign('query', $text_query); $tpl->display('admin/membres/selector.tpl'); |
Added src/www/admin/optout.php version [541d80a80f].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <?php namespace Garradin; use Garradin\Users\Emails; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; if (empty($_GET['un'])) { throw new UserException('Demande de désinscription incomplète.'); } $code = $_GET['un']; $email = Emails::getEmailFromOptout($code); $verify = null; if (!$email) { throw new UserException('Adresse email introuvable.'); } if (!empty($_GET['v'])) { if ($email->verify($_GET['v'])) { $email->save(); $verify = true; } else { $verify = false; } } $form->runIf('confirm_resub', function () use ($email) { if (empty($_POST['email'])) { throw new UserException('Merci de renseigner l\'adresse email'); } $email->sendVerification($_POST['email']); }, 'optout', '!optout.php?resub_ok&un=' . $code); $form->runIf('optout', function () use ($email) { $email->setOptout(); $email->save(); }, 'optout', '!optout.php?ok&un=' . $code); $ok = isset($_GET['ok']); $resub_ok = isset($_GET['resub_ok']); $tpl->assign(compact('email', 'ok', 'resub_ok', 'verify')); $tpl->display('admin/optout.tpl'); |
Modified src/www/admin/password.php from [8dbf35c476] to [85043868d0].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; if (qg('c')) { if (!$session->recoverPasswordCheck(qg('c'))) { $form->addError('Le lien que vous avez suivi est invalide ou a expiré.'); } else { |
︙ | ︙ | |||
34 35 36 37 38 39 40 | { $form->check('recoverPassword', [ 'id' => 'required' ]); if (!$form->hasErrors()) { | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | { $form->check('recoverPassword', [ 'id' => 'required' ]); if (!$form->hasErrors()) { if (f('id') && $session->recoverPasswordSend(f('id'))) { Utils::redirect(ADMIN_URL . 'password.php?sent'); } $form->addError('Ce membre n\'a pas d\'adresse email enregistrée ou n\'a pas le droit de se connecter.'); } } |
︙ | ︙ |
Modified src/www/admin/services/details.php from [f51df3fd9b] to [d1a47e7c38].
︙ | ︙ | |||
17 18 19 20 21 22 23 | if ('unpaid' == $type) { $list = $service->unpaidUsersList(); } elseif ('expired' == $type) { $list = $service->expiredUsersList(); } else { | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | if ('unpaid' == $type) { $list = $service->unpaidUsersList(); } elseif ('expired' == $type) { $list = $service->expiredUsersList(); } else { $type = 'active'; $list = $service->activeUsersList(); } $list->loadFromQueryString(); $tpl->assign(compact('list', 'service', 'type')); $tpl->display('services/details.tpl'); |
Modified src/www/admin/services/fees/details.php from [81bc52a6fd] to [48c5e807d0].
︙ | ︙ | |||
17 18 19 20 21 22 23 | if ('unpaid' == $type) { $list = $fee->unpaidUsersList(); } elseif ('expired' == $type) { $list = $fee->expiredUsersList(); } else { | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | if ('unpaid' == $type) { $list = $fee->unpaidUsersList(); } elseif ('expired' == $type) { $list = $fee->expiredUsersList(); } else { $type = 'active'; $list = $fee->activeUsersList(); } $list->loadFromQueryString(); $service = $fee->service(); $tpl->assign(compact('list', 'fee', 'type', 'service')); $tpl->display('services/fees/details.tpl'); |
Modified src/www/admin/services/fees/edit.php from [545ab428a4] to [ea164cf904].
︙ | ︙ | |||
34 35 36 37 38 39 40 41 | } $accounting_enabled = (bool) $fee->id_account; $years = Years::listOpen(); $account = $fee->id_account ? [$fee->id_account => Accounts::getSelectorLabel($fee->id_account)] : null; | > | | 34 35 36 37 38 39 40 41 42 43 44 45 | } $accounting_enabled = (bool) $fee->id_account; $years = Years::listOpen(); $account = $fee->id_account ? [$fee->id_account => Accounts::getSelectorLabel($fee->id_account)] : null; $analytical_account = $fee->id_analytical ? [$fee->id_analytical => Accounts::getSelectorLabel($fee->id_analytical)] : null; $tpl->assign(compact('service', 'amount_type', 'fee', 'csrf_key', 'account', 'accounting_enabled', 'years', 'analytical_account')); $tpl->display('services/fees/edit.tpl'); |
Modified src/www/admin/services/fees/index.php from [853d7be879] to [8cf8f06ec6].
︙ | ︙ | |||
19 20 21 22 23 24 25 | $form->runIf($session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && f('save'), function () use ($service) { $fee = new Fee; $fee->id_service = $service->id(); $fee->importForm(); $fee->save(); }, 'fee_add', ADMIN_URL . 'services/fees/?id=' . $service->id()); | < < > | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | $form->runIf($session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN) && f('save'), function () use ($service) { $fee = new Fee; $fee->id_service = $service->id(); $fee->importForm(); $fee->save(); }, 'fee_add', ADMIN_URL . 'services/fees/?id=' . $service->id()); $accounting_enabled = false; $years = Years::listOpen(); $analytical_account = null; $tpl->assign(compact('service', 'accounting_enabled', 'years', 'analytical_account')); $tpl->assign('list', $fees->listWithStats()); $tpl->display('services/fees/index.tpl'); |
Modified src/www/admin/services/index.php from [62f531a546] to [c5518378ef].
︙ | ︙ | |||
14 15 16 17 18 19 20 | $service->save(); Utils::redirect(ADMIN_URL . 'services/fees/?id=' . $service->id()); }, $csrf_key); $has_old_services = Services::countOldServices(); $show_old_services = $_GET['old'] ?? false; | > > > | < | 14 15 16 17 18 19 20 21 22 23 24 25 26 | $service->save(); Utils::redirect(ADMIN_URL . 'services/fees/?id=' . $service->id()); }, $csrf_key); $has_old_services = Services::countOldServices(); $show_old_services = $_GET['old'] ?? false; $list = Services::listWithStats(!$show_old_services); $list->loadFromQueryString(); $tpl->assign(compact('csrf_key', 'has_old_services', 'show_old_services', 'list')); $tpl->display('services/index.tpl'); |
Modified src/www/admin/services/reminders/index.php from [afd86fb166] to [e5bb7c08cf].
︙ | ︙ | |||
22 23 24 25 26 27 28 | $reminder->importForm(); $reminder->save(); }, $csrf_key, Utils::getSelfURI()); $list = Reminders::list(); $services_list = Services::listAssoc(); | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | $reminder->importForm(); $reminder->save(); }, $csrf_key, Utils::getSelfURI()); $list = Reminders::list(); $services_list = Services::listAssoc(); $default_subject = 'Échéance de cotisation'; $default_body = "Bonjour #IDENTITE,\n\nVotre cotisation arrive à échéance dans #NB_JOURS jours.\n\n" . "Merci de nous contacter pour renouveler votre cotisation.\n\nCordialement.\n\n" . "--\n#NOM_ASSO\n#ADRESSE_ASSO\nE-Mail : #EMAIL_ASSO\nSite web : #SITE_ASSO"; $tpl->assign(compact('csrf_key', 'list', 'services_list', 'default_subject', 'default_body')); $tpl->display('services/reminders/index.tpl'); |
Modified src/www/admin/services/user/_form.php from [9911775664] to [ff70cded0e].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin; use Garradin\Services\Services; if (!defined('\Garradin\ROOT')) { die(); } assert(isset($tpl, $form_url, $create)); | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php namespace Garradin; use Garradin\Services\Services; 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, $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; |
︙ | ︙ |
Modified src/www/admin/services/user/add.php from [1b2cc90329] to [b77ee3ba87].
︙ | ︙ | |||
13 14 15 16 17 18 19 | $count_all = Services::count(); if (!$count_all) { Utils::redirect(ADMIN_URL . 'services/?CREATE'); } | | | 13 14 15 16 17 18 19 20 21 22 23 24 | $count_all = Services::count(); if (!$count_all) { Utils::redirect(ADMIN_URL . 'services/?CREATE'); } $services = [0 => '-- Sélectionner une activité'] + Services::listAssoc(); $tpl->assign(compact('services')); $tpl->display('services/user/add.tpl'); |
Modified src/www/admin/services/user/index.php from [1dbf771034] to [45d438db71].
︙ | ︙ | |||
19 20 21 22 23 24 25 | throw new UserException("Cette inscription est introuvable"); } $su->paid = (bool)qg('paid'); $su->save(); }, null, ADMIN_URL . 'services/user/?id=' . $user->id); | > > | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | throw new UserException("Cette inscription est introuvable"); } $su->paid = (bool)qg('paid'); $su->save(); }, null, ADMIN_URL . 'services/user/?id=' . $user->id); $only = (int)qg('only') ?: null; $list = Services_User::perUserList($user->id, $only); $list->setTitle(sprintf('Inscriptions — %s', $user->identite)); $list->loadFromQueryString(); $tpl->assign('services', Services_User::listDistinctForUser($user->id)); $tpl->assign(compact('list', 'user', 'only')); $tpl->display('services/user/index.tpl'); |
Added src/www/admin/services/user/link.php version [534c8a1263].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php namespace Garradin; use Garradin\Services\Services_User; use Garradin\Accounting\Transactions; require_once __DIR__ . '/../_inc.php'; $session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE); $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); $su = Services_User::get((int)qg('id')); if (!$su) { throw new UserException("Cette inscription n'existe pas"); } $csrf_key = 'service_link'; $form->runIf('save', function () use ($su) { $id = (int)f('id_transaction'); $transaction = Transactions::get($id); if (!$transaction) { throw new UserException('Impossible de trouver l\'écriture #' . $id); } $transaction->linkToUser($su->id_user, $su->id); }, $csrf_key, '!acc/transactions/service_user.php?id=' . $su->id . '&user=' . $su->id_user); $tpl->assign(compact('csrf_key')); $tpl->display('services/user/link.tpl'); |
Modified src/www/admin/services/user/subscribe.php from [72bcfd51bb] to [e4d11cfc89].
︙ | ︙ | |||
23 24 25 26 27 28 29 | $users = null; $copy_service = null; $copy_service_only_paid = null; if (qg('user') && ($name = (new Membres)->getNom((int)qg('user')))) { $users = [(int)qg('user') => $name]; } | | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | $users = null; $copy_service = null; $copy_service_only_paid = null; if (qg('user') && ($name = (new Membres)->getNom((int)qg('user')))) { $users = [(int)qg('user') => $name]; } elseif (f('users') && is_array(f('users')) && count(f('users'))) { $users = f('users'); $users = array_filter($users, 'intval', \ARRAY_FILTER_USE_KEY); } elseif (f('copy_service') && ($copy_service = Services::get((int)f('copy_service')))) { $copy_service_only_paid = (bool) f('copy_service_only_paid'); } else { throw new UserException('Aucun membre n\'a été sélectionné'); } $form_url = '?'; |
︙ | ︙ |
Modified src/www/admin/static/font/config.json from [cec9b888ae] to [ce3115b784].
︙ | ︙ | |||
211 212 213 214 215 216 217 | "width": 857.1 }, "search": [ "edit" ] }, { | < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | "width": 857.1 }, "search": [ "edit" ] }, { "uid": "7091435c31f7a593060b9782a234db80", "css": "menu", "code": 119650, "src": "custom_icons", "selected": true, "svg": { "path": "M857 750V821Q857 836 847 846T821 857H36Q21 857 11 846T0 821V750Q0 735 11 725T36 714H821Q836 714 847 725T857 750ZM857 464V536Q857 550 847 561T821 571H36Q21 571 11 561T0 536V464Q0 450 11 439T36 429H821Q836 429 847 439T857 464ZM857 179V250Q857 265 847 275T821 286H36Q21 286 11 275T0 250V179Q0 164 11 153T36 143H821Q836 143 847 153T857 179Z", |
︙ | ︙ | |||
397 398 399 400 401 402 403 | "css": "list-ul", "code": 8226, "src": "fontawesome" }, { "uid": "f6766a8b042c2453a4e153af03294383", "css": "list-ol", | | | 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | "css": "list-ul", "code": 8226, "src": "fontawesome" }, { "uid": "f6766a8b042c2453a4e153af03294383", "css": "list-ol", "code": 49, "src": "fontawesome" }, { "uid": "0c708edd8fae2376b3370aa56d40cf9e", "css": "header", "code": 72, "src": "fontawesome" |
︙ | ︙ | |||
441 442 443 444 445 446 447 448 449 450 | "src": "fontawesome" }, { "uid": "399ef63b1e23ab1b761dfbb5591fa4da", "css": "right", "code": 8594, "src": "fontawesome" } ] } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | "src": "fontawesome" }, { "uid": "399ef63b1e23ab1b761dfbb5591fa4da", "css": "right", "code": 8594, "src": "fontawesome" }, { "uid": "ae4eb744170ff9a371afde2093bcd733", "css": "del-col", "code": 129940, "src": "custom_icons", "selected": true, "svg": { "path": "M0-35.7L0 961.5 306.8 961.5 306.8-35.7 0-35.7ZM383.5-35.7L383.5 961.4 1071 961.4 1071-35.7 383.5-35.7ZM600.7 219.5L744.3 363.3 888.1 219.5 992.7 324.1 848.9 467.9 992.7 611.6 888.1 716.2 744.3 572.5 600.7 716.2 496.1 611.6 639.7 467.9 496.1 324.1 600.7 219.5Z", "width": 1074 }, "search": [ "del-col" ] }, { "uid": "7034e4d22866af82bef811f52fb1ba46", "css": "code", "code": 60, "src": "fontawesome" }, { "uid": "53a4e0148b78afaa94955457773f48c2", "css": "col", "code": 9626, "src": "custom_icons", "selected": true, "svg": { "path": "M0-35.7L0 961.5 306.8 961.5 306.8-35.7 0-35.7ZM383.5-35.7L383.5 961.4 1071 961.4 1071-35.7 383.5-35.7Z", "width": 1074 }, "search": [ "add-col" ] }, { "uid": "c3e5dafba1739ef33cc574c7484febf7", "css": "quote", "code": 171, "src": "entypo" }, { "uid": "a73c5deb486c8d66249811642e5d719a", "css": "reload", "code": 128472, "src": "fontawesome" }, { "uid": "4267b9ce4d12532d3c6f14b5a8a02554", "css": "skriv", "code": 83, "src": "custom_icons", "selected": true, "svg": { "path": "M0 886.7V773.3H40C79.6 773.3 80 774.1 80 846.7V920H853.3V846.7C853.3 774.1 853.7 773.3 893.3 773.3H933.3V886.7 1000H0ZM248.7 762.3C236.6 748.9 226.7 715.9 226.7 688.9 226.7 643.4 229.5 640 266.7 640 300.4 640 306.7 645.2 306.7 673.3V706.7H453.3 600V628.1 549.5L438.9 544.7C240.3 538.9 240 538.6 240 371.7 240 211.6 237.6 213.3 458.9 213.3 659.8 213.3 680 222.3 680 311 680 356.5 677.2 360 640 360 606.2 360 600 354.8 600 326.7V293.3H460 320V380 466.7H477.9C677.5 466.7 680 468.7 680 629 680 786.7 680.1 786.7 451 786.7 293.4 786.7 268 783.6 248.7 762.3ZM0 580C0 551.9 6.2 546.7 40 546.7 73.8 546.7 80 551.9 80 580 80 608.1 73.8 613.3 40 613.3 6.2 613.3 0 608.1 0 580ZM853.3 580C853.3 551.9 859.6 546.7 893.3 546.7 927.1 546.7 933.3 551.9 933.3 580 933.3 608.1 927.1 613.3 893.3 613.3 859.6 613.3 853.3 608.1 853.3 580ZM0 427.6C0 390.8 4.1 386.7 40.9 386.7 77.6 386.7 81.3 390.4 77.6 423.3 74.2 452.1 65.5 460.9 36.7 464.2 3.7 468 0 464.3 0 427.6ZM862.2 457.8C857.3 452.9 853.3 434.9 853.3 417.8 853.3 392.1 860.3 386.7 893.3 386.7 928.9 386.7 933.3 391.1 933.3 426.7 933.3 459.7 927.9 466.7 902.2 466.7 885.1 466.7 867.1 462.7 862.2 457.8ZM0 113.3V0H933.3V113.3 226.7H893.3C893.3 226.7 853.3 173.4 853.3 80H80V153.3C80 225.9 79.6 226.7 40 226.7H0Z", "width": 933 }, "search": [ "skriv" ] }, { "uid": "47a1f80457068fbeab69fdb83d7d0817", "css": "video", "code": 9654, "src": "fontawesome" }, { "uid": "dd492243d64e21dfe16a92452f7861cb", "css": "gallery", "code": 128444, "src": "fontawesome" }, { "uid": "bf3c88e1a2208a0cf26001e0793ff403", "css": "print", "code": 9113, "src": "custom_icons", "selected": true, "svg": { "path": "M214 857H714V714H214V857ZM214 500H714V286H625Q603 286 587 270T571 232V143H214V500ZM857 536Q857 521 847 511T821 500 796 511 786 536 796 561 821 571 847 561 857 536ZM929 536V768Q929 775 923 780T911 786H786V875Q786 897 770 913T732 929H196Q174 929 159 913T143 875V786H18Q11 786 5 780T0 768V536Q0 492 32 460T107 429H143V125Q143 103 159 87T196 71H571Q594 71 621 83T663 109L748 194Q763 210 775 237T786 286V429H821Q866 429 897 460T929 536Z", "width": 928.6 }, "search": [ "print" ] }, { "uid": "4d879892b0e3a0da5d871e3df8d105e2", "css": "alert", "code": 9888, "src": "custom_icons", "selected": true, "svg": { "path": "M571 767V661Q571 653 566 648T554 643H446Q439 643 434 648T429 661V767Q429 775 434 780T446 786H554Q561 786 566 780T571 767ZM570 559L580 302Q580 296 575 292 568 286 561 286H439Q432 286 425 292 420 296 420 304L429 559Q429 564 435 568T448 571H551Q559 571 564 568T570 559ZM563 37L991 823Q1011 858 990 893 980 910 964 919T929 929H71Q53 929 36 919T10 893Q-11 858 9 823L438 37Q447 20 464 10T500 0 536 10 563 37Z", "width": 1000 }, "search": [ "alert" ] }, { "uid": "a29e53e4b5a383b4e4591cbba3f3fe1c", "css": "markdown", "code": 77, "src": "custom_icons", "selected": true, "svg": { "path": "M234.4 765.6V234.4H390.6L546.9 429.7 703.1 234.4H859.4V765.6H703.1V460.9L546.9 656.3 390.6 460.9V765.6ZM1210.9 765.6L976.6 507.8H1132.8V234.4H1289.1V507.8H1445.3Z", "width": 1625 }, "search": [ "markdown-mark" ] }, { "uid": "i6ej1r6t84xouh0dct7g9zyx3ya9s9eg", "css": "globe", "code": 127757, "src": "modernpics" }, { "uid": "92f93f22074943bf7c53782a46faf86f", "css": "euro", "code": 8364, "src": "custom_icons", "selected": true, "svg": { "path": "M831.2 104.2L765.4 238.1Q711.8 183.9 593.7 183.9 422.7 183.9 359.6 378.2H703.7L666.8 472.2H340.2Q338.1 497.1 338.1 521.4 338.1 546.9 340.2 571.2H623.9L587.7 665.2H358.9Q415.9 839.6 581.6 839.6 715.8 839.6 779.5 750.6V921.2Q700.4 983.4 568.9 983.4 411.9 983.4 312 895 219.4 812.8 187.9 665.2H102.7V571.2H174.5Q170.4 518.9 173.8 472.2H102.7V378.2H187.9Q222.8 231.2 321.4 140.3 429.4 40.1 583.6 40.1T831.2 104.2Z", "width": 1000 }, "search": [ "euro-svgrepo-com-(1)" ] } ] } |
Modified src/www/admin/static/font/garradin.css from [1274bb9a83] to [e87860e999].
1 | @charset "UTF-8"; | < | | | | | | | | < | | | | | | | | | > > > > | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 | @charset "UTF-8"; @font-face { font-family: 'garradin'; src: url('../font/garradin.eot?61352908'); src: url('../font/garradin.eot?61352908#iefix') format('embedded-opentype'), url('../font/garradin.woff2?61352908') format('woff2'), url('../font/garradin.woff?61352908') format('woff'), url('../font/garradin.ttf?61352908') format('truetype'), url('../font/garradin.svg?61352908#garradin') format('svg'); font-weight: normal; font-style: normal; } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ /* @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'garradin'; src: url('../font/garradin.svg?61352908#garradin') format('svg'); } } */ [class^="icn-"]:before, [class*=" icn-"]:before { font-family: "garradin"; font-style: normal; font-weight: normal; speak: never; display: inline-block; text-decoration: inherit; width: 1em; margin-right: .2em; text-align: center; /* opacity: .8; */ /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; text-transform: none; /* fix buttons height, for twitter bootstrap */ line-height: 1em; /* Animation center compensation - margins should be symmetric */ /* remove if not needed */ margin-left: .2em; /* you can be more comfortable with increased icons size */ /* font-size: 120%; */ /* Font smoothing. That was taken from TWBS */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Uncomment for 3D effect */ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } .icn-list-ol:before { content: '\31'; } /* '1' */ .icn-code:before { content: '\3c'; } /* '<' */ .icn-bold:before { content: '\42'; } /* 'B' */ .icn-header:before { content: '\48'; } /* 'H' */ .icn-italic:before { content: '\49'; } /* 'I' */ .icn-markdown:before { content: '\4d'; } /* 'M' */ .icn-skriv:before { content: '\53'; } /* 'S' */ .icn-paragraph:before { content: '\a7'; } /* '§' */ .icn-quote:before { content: '\ab'; } /* '«' */ .icn-list-ul:before { content: '\2022'; } /* '•' */ .icn-euro:before { content: '\20ac'; } /* '€' */ .icn-left:before { content: '\2190'; } /* '←' */ .icn-up:before { content: '\2191'; } /* '↑' */ .icn-right:before { content: '\2192'; } /* '→' */ .icn-down:before { content: '\2193'; } /* '↓' */ .icn-export:before { content: '\21b7'; } /* '↷' */ .icn-reset:before { content: '\21ba'; } /* '↺' */ .icn-upload:before { content: '\21d1'; } /* '⇑' */ .icn-download:before { content: '\21d3'; } /* '⇓' */ .icn-home:before { content: '\2302'; } /* '⌂' */ .icn-print:before { content: '\2399'; } /* '⎙' */ .icn-col:before { content: '\259a'; } /* '▚' */ .icn-video:before { content: '\25b6'; } /* '▶' */ .icn-table:before { content: '\25eb'; } /* '◫' */ .icn-radio-unchecked:before { content: '\25ef'; } /* '◯' */ .icn-star:before { content: '\2605'; } /* '★' */ .icn-uncheck:before { content: '\2610'; } /* '☐' */ .icn-check:before { content: '\2611'; } /* '☑' */ .icn-settings:before { content: '\2638'; } /* '☸' */ .icn-alert:before { content: '\26a0'; } /* '⚠' */ .icn-mail:before { content: '\2709'; } /* '✉' */ .icn-edit:before { content: '\270e'; } /* '✎' */ .icn-delete:before { content: '\2718'; } /* '✘' */ .icn-help:before { content: '\2753'; } /* '❓' */ .icn-plus:before { content: '\2795'; } /* '➕' */ .icn-minus:before { content: '\2796'; } /* '➖' */ .icn-logout:before { content: '\291d'; } /* '⤝' */ .icn-eye-off:before { content: '\292b'; } /* '⤫' */ .icn-radio-checked:before { content: '\2b24'; } /* '⬤' */ .icn-menu:before { content: '𝍢'; } /* '\1d362' */ .icn-globe:before { content: '🌍'; } /* '\1f30d' */ .icn-eye:before { content: '👁'; } /* '\1f441' */ .icn-user:before { content: '👤'; } /* '\1f464' */ .icn-users:before { content: '👪'; } /* '\1f46a' */ .icn-calendar:before { content: '📅'; } /* '\1f4c5' */ .icn-attach:before { content: '📎'; } /* '\1f4ce' */ .icn-search:before { content: '🔍'; } /* '\1f50d' */ .icn-lock:before { content: '🔒'; } /* '\1f512' */ .icn-unlock:before { content: '🔓'; } /* '\1f513' */ .icn-image:before { content: '🖻'; } /* '\1f5bb' */ .icn-gallery:before { content: '🖼'; } /* '\1f5bc' */ .icn-folder:before { content: '🗀'; } /* '\1f5c0' */ .icn-document:before { content: '🗅'; } /* '\1f5c5' */ .icn-reload:before { content: '🗘'; } /* '\1f5d8' */ .icn-del-col:before { content: '🮔'; } /* '\1fb94' */ |
Modified src/www/admin/static/font/garradin.eot from [6d9991856c] to [20dc4de2d6].
cannot compute difference between binary files
Modified src/www/admin/static/font/garradin.svg from [91f74fefc9] to [683208e5f6].
1 2 3 | <?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg"> | | > > > > > > > > < > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg"> <metadata>Copyright (C) 2022 by original authors @ fontello.com</metadata> <defs> <font id="garradin" horiz-adv-x="1000" > <font-face font-family="garradin" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" /> <missing-glyph horiz-adv-x="1000" /> <glyph glyph-name="list-ol" unicode="1" d="M213-54q0-45-31-70t-75-26q-60 0-96 37l31 49q28-25 60-25 16 0 28 8t12 24q0 35-59 31l-14 31q4 6 18 24t24 31 20 21v1q-9 0-27-1t-27 0v-30h-59v85h186v-49l-53-65q28-6 45-27t17-49z m1 350v-89h-202q-4 20-4 30 0 29 14 52t31 38 37 27 31 24 14 25q0 14-9 22t-22 7q-25 0-45-32l-47 33q13 28 40 44t59 16q40 0 68-23t28-63q0-28-19-51t-42-36-42-28-20-30h71v34h59z m786-178v-107q0-7-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 8 5 13t13 5h678q7 0 13-6t5-12z m-786 502v-56h-187v56h60q0 22 0 67t1 68v7h-1q-5-10-28-30l-40 42 76 71h59v-225h60z m786-216v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" /> <glyph glyph-name="code" unicode="<" d="M344 69l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13t-6-12l-219-220 219-219q6-6 6-13t-6-13z m330 596l-208-721q-2-7-9-11t-13-1l-34 9q-8 3-11 9t-2 14l209 720q2 8 8 11t13 2l35-10q7-2 11-9t1-13z m367-363l-260-261q-6-5-13-5t-13 5l-28 28q-5 6-5 13t5 13l219 219-219 220q-5 5-5 12t5 13l28 28q6 6 13 6t13-6l260-260q5-5 5-13t-5-12z" horiz-adv-x="1071.4" /> <glyph glyph-name="bold" unicode="B" d="M0-60l125 0 0 820-125 0 0 80 582 0q180 0 270-60t89-182q0-88-62-139t-184-64q147-14 226-79t79-173q0-145-111-215-110-68-344-68l-545 0 0 80z m379 0l94 0q125 0 185 49t61 154q0 105-62 157t-184 52l-94 0 0-412z m0 492l86 0q113 0 168 39 55 43 55 127 0 86-55 125-53 37-168 37l-86 0 0-328z" horiz-adv-x="1000" /> <glyph glyph-name="header" unicode="H" d="M939-79q-25 0-74 2t-75 2q-24 0-73-2t-74-2q-13 0-21 12t-7 25q0 18 9 26t22 9 29 4 25 9q18 11 18 78l0 218q0 12-1 17-7 3-28 3h-376q-22 0-29-3 0-5 0-17l-1-207q0-79 21-91 9-6 26-8t32-2 25-8 11-26q0-14-6-26t-21-13q-26 0-78 2t-77 2q-24 0-71-2t-71-2q-13 0-20 12t-7 25q0 17 9 25t20 10 26 4 24 9q18 13 18 80l-1 31v454q0 2 1 15t0 20-1 21-2 24-4 20-6 18-9 10q-8 5-25 7t-29 1-23 7-10 26q0 14 6 26t20 13q26 0 78-2t77-2q23 0 71 2t70 2q14 0 21-13t7-26q0-17-9-25t-22-8-27-2-24-7q-20-12-20-90l1-178q0-12 0-18 7-2 22-2h390q14 0 21 2 1 6 1 18l0 178q0 78-19 90-10 6-33 7t-37 7-14 28q0 14 7 26t21 13q24 0 74-2t73-2q24 0 72 2t72 2q14 0 21-13t7-26q0-17-10-25t-22-8-29-2-24-7q-20-13-20-90l1-526q0-66 19-78 9-6 25-8t30-2 23-9 10-25q0-14-6-26t-20-13z" horiz-adv-x="1000" /> <glyph glyph-name="italic" unicode="I" d="M0-78l10 48q12 4 34 9t40 11 33 13q16 19 23 56 1 4 35 162t63 303 29 165v14q-13 8-30 11t-39 4-32 3l10 58q19-1 67-4t84-4 67-1q27 0 55 1t68 4 54 4q-2-22-10-50-17-6-57-16t-60-19q-5-10-8-23t-5-23-4-25-4-24q-15-82-49-234t-43-198q-1-5-7-32t-11-51-9-46-4-32l1-10q9-3 103-18-2-24-9-55-6 0-18-1t-18-1q-16 0-49 6t-48 6q-77 1-115 1-28 0-79-5t-68-7z" horiz-adv-x="571.4" /> <glyph glyph-name="markdown" unicode="M" d="M234 84v532h157l156-196 156 196h156v-532h-156v305l-156-195-156 195v-305z m977 0l-234 258h156v274h156v-274h156z" horiz-adv-x="1625" /> <glyph glyph-name="skriv" unicode="S" d="M0-37v114h40c40 0 40-1 40-74v-73h773v73c0 73 1 74 40 74h40v-114-113h-933z m249 125c-12 13-22 46-22 73 0 46 3 49 40 49 33 0 40-5 40-33v-34h146 147v79 79l-161 4c-199 6-199 6-199 173 0 160-2 159 219 159 201 0 221-9 221-98 0-45-3-49-40-49-34 0-40 5-40 33v34h-140-140v-87-87h158c200 0 202-2 202-162 0-158 0-158-229-158-158 0-183 3-202 25z m-249 182c0 28 6 33 40 33 34 0 40-5 40-33 0-28-6-33-40-33-34 0-40 5-40 33z m853 0c0 28 7 33 40 33 34 0 40-5 40-33 0-28-6-33-40-33-33 0-40 5-40 33z m-853 152c0 37 4 41 41 41 37 0 40-3 37-36-4-29-12-38-41-41-33-4-37 0-37 36z m862-30c-5 5-9 23-9 40 0 26 7 31 40 31 36 0 40-4 40-40 0-33-5-40-31-40-17 0-35 4-40 9z m-862 345v113h933v-113-114h-40c0 0-40 54-40 147h-773v-73c0-73 0-74-40-74h-40z" horiz-adv-x="933" /> <glyph glyph-name="paragraph" unicode="§" d="M713 745v-41q0-16-10-34t-24-18q-28 0-30-1-14-3-18-17-1-6-1-36v-643q0-14-11-24t-24-10h-60q-14 0-24 10t-10 24v680h-80v-680q0-14-9-24t-25-10h-60q-14 0-24 10t-10 24v277q-82 7-137 33-70 33-107 100-36 65-36 145 0 92 50 159 49 66 116 89 62 21 233 21h267q14 0 24-10t10-24z" horiz-adv-x="714.3" /> <glyph glyph-name="quote" unicode="«" d="M146 680q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z m420 0q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z" horiz-adv-x="762" /> <glyph glyph-name="list-ul" unicode="•" d="M214 64q0-44-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m0 286q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-107q0-7-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 7 5 12t13 6h678q7 0 13-6t5-12z m-786 518q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" /> <glyph glyph-name="euro" unicode="€" d="M831 746l-66-134q-53 54-171 54-171 0-234-194h344l-37-94h-327q-2-25-2-49 0-26 2-50h284l-36-94h-229q57-175 223-175 134 0 198 89v-170q-80-62-211-62-157 0-257 88-93 82-124 230h-85v94h72q-5 52-1 99h-71v94h85q35 147 133 238 108 100 263 100t247-64z" horiz-adv-x="1000" /> <glyph glyph-name="left" unicode="←" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" /> <glyph glyph-name="up" unicode="↑" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" /> <glyph glyph-name="right" unicode="→" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" /> |
︙ | ︙ | |||
34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <glyph glyph-name="download" unicode="⇓" d="M714 100q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" /> <glyph glyph-name="home" unicode="⌂" d="M786 296v-267q0-15-11-26t-25-10h-214v214h-143v-214h-214q-15 0-25 10t-11 26v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-4-7 2-12 7l-35 41q-4 5-3 13t6 12l401 334q18 15 42 15t43-15l136-114v109q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q5-5 6-12t-4-13z" horiz-adv-x="928.6" /> <glyph glyph-name="print" unicode="⎙" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" /> <glyph glyph-name="table" unicode="◫" d="M286 82v107q0 8-5 13t-13 5h-179q-7 0-12-5t-6-13v-107q0-8 6-13t12-5h179q8 0 13 5t5 13z m0 214v108q0 7-5 12t-13 5h-179q-7 0-12-5t-6-12v-108q0-7 6-12t12-5h179q8 0 13 5t5 12z m285-214v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m-285 429v107q0 8-5 13t-13 5h-179q-7 0-12-5t-6-13v-107q0-8 6-13t12-5h179q8 0 13 5t5 13z m285-215v108q0 7-5 12t-12 5h-179q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h179q7 0 12 5t5 12z m286-214v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m-286 429v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m286-215v108q0 7-5 12t-13 5h-178q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h178q8 0 13 5t5 12z m0 215v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m72 178v-607q0-37-27-63t-63-26h-750q-36 0-63 26t-26 63v607q0 37 26 63t63 27h750q37 0 63-27t27-63z" horiz-adv-x="928.6" /> <glyph glyph-name="radio-unchecked" unicode="◯" d="M429 654q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152-41 152-110 111-152 41z m428-304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="star" unicode="★" d="M929 489q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" /> <glyph glyph-name="uncheck" unicode="☐" d="M625 707h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v464q0 37-26 63t-63 26z m161-89v-464q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q66 0 114-48t47-113z" horiz-adv-x="785.7" /> | > > > > | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | <glyph glyph-name="download" unicode="⇓" d="M714 100q0 15-10 25t-25 11-26-11-10-25 10-25 26-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" /> <glyph glyph-name="home" unicode="⌂" d="M786 296v-267q0-15-11-26t-25-10h-214v214h-143v-214h-214q-15 0-25 10t-11 26v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-4-7 2-12 7l-35 41q-4 5-3 13t6 12l401 334q18 15 42 15t43-15l136-114v109q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q5-5 6-12t-4-13z" horiz-adv-x="928.6" /> <glyph glyph-name="print" unicode="⎙" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" /> <glyph glyph-name="col" unicode="▚" d="M0 886l0-998 307 0 0 998-307 0z m384 0l0-997 687 0 0 997-687 0z" horiz-adv-x="1074" /> <glyph glyph-name="video" unicode="▶" d="M397 221l270 139-270 141v-280z m103 481q94 0 181-3t128-5l41-2q0 0 9-1t13-2 13-2 16-5 16-7 17-11 16-15q4-3 9-10t16-33 15-56q4-36 7-76t3-64v-98q1-81-10-162-4-30-14-55t-18-35l-8-9q-7-8-16-15t-17-10-16-7-16-5-13-2-13-2-9-1q-140-11-350-11-115 2-201 4t-111 4l-28 3-20 2q-20 3-30 5t-29 12-31 23q-4 3-9 10t-16 33-15 56q-4 36-7 76t-3 64v98q-1 81 10 162 4 31 14 55t18 35l8 9q8 9 16 15t17 11 16 7 16 5 13 2 13 2 9 1q140 10 350 10z" horiz-adv-x="1000" /> <glyph glyph-name="table" unicode="◫" d="M286 82v107q0 8-5 13t-13 5h-179q-7 0-12-5t-6-13v-107q0-8 6-13t12-5h179q8 0 13 5t5 13z m0 214v108q0 7-5 12t-13 5h-179q-7 0-12-5t-6-12v-108q0-7 6-12t12-5h179q8 0 13 5t5 12z m285-214v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m-285 429v107q0 8-5 13t-13 5h-179q-7 0-12-5t-6-13v-107q0-8 6-13t12-5h179q8 0 13 5t5 13z m285-215v108q0 7-5 12t-12 5h-179q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h179q7 0 12 5t5 12z m286-214v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m-286 429v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m286-215v108q0 7-5 12t-13 5h-178q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h178q8 0 13 5t5 12z m0 215v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m72 178v-607q0-37-27-63t-63-26h-750q-36 0-63 26t-26 63v607q0 37 26 63t63 27h750q37 0 63-27t27-63z" horiz-adv-x="928.6" /> <glyph glyph-name="radio-unchecked" unicode="◯" d="M429 654q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152-41 152-110 111-152 41z m428-304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="star" unicode="★" d="M929 489q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" /> <glyph glyph-name="uncheck" unicode="☐" d="M625 707h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v464q0 37-26 63t-63 26z m161-89v-464q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q66 0 114-48t47-113z" horiz-adv-x="785.7" /> |
︙ | ︙ | |||
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | <glyph glyph-name="eye-off" unicode="⤫" d="M0 326q6 49 64 110 79 80 176 128 129 61 260 61 29 0 59-2l74 129q10 16 23 18 4 0 8-2l51-32q17-7 2-33l-57-101-47-79-41-72-144-250-41-72-47-80-57-100q-15-25-31-15l-53 31q-15 8 0 33l49 86-8 4q-103 53-176 129-64 72-64 109z m264 0q0-74 47-133l48 84q-9 24-9 49 0 51 34 91t85 50l49 84-18 0q-98 0-167-66t-69-159z m177-295l41 71 18 0q98 0 167 65t69 159q0 74-47 133l63 109q2-2 4-3t4-1q103-52 176-128 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-25 0-59 4z m90 155l110 189q9-25 9-49 0-51-34-90t-85-50z" horiz-adv-x="1000" /> <glyph glyph-name="radio-checked" unicode="⬤" d="M571 350q0-59-41-101t-101-42-101 42-42 101 42 101 101 42 101-42 41-101z m-142 304q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152-41 152-110 111-152 41z m428-304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="menu" unicode="𝍢" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" /> <glyph glyph-name="eye" unicode="👁" d="M0 350q6 49 64 110 79 80 176 129 129 60 260 60 137-2 260-60 103-53 176-129 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-137 2-260 61-103 53-176 129-64 72-64 109z m264 0q0-94 69-159t167-65 167 65 69 159-69 159-167 66-167-66-69-159z m86 1q0 60 44 102t106 42 106-42 44-102-44-102-106-43-106 43-44 102z" horiz-adv-x="1000" /> <glyph glyph-name="user" unicode="👤" d="M786 66q0-67-41-106t-108-39h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q5 0 24-12t41-27 60-27 75-12 74 12 61 27 41 27 24 12q34 0 62-11t48-30 34-45 24-55 15-60 8-61 2-58z m-179 498q0-88-63-151t-151-63-152 63-62 151 62 152 152 63 151-63 63-152z" horiz-adv-x="785.7" /> <glyph glyph-name="users" unicode="👪" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 23 12q35 0 63-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" /> <glyph glyph-name="calendar" unicode="📅" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" /> <glyph glyph-name="attach" unicode="📎" d="M783 77q0-65-44-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 88 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-13 5l-338 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-35 81-35 36 0 59 23t24 59q0 46-36 81l-324 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-7 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 83-36l325-324q55-54 55-131z" horiz-adv-x="785.7" /> <glyph glyph-name="search" unicode="🔍" d="M643 386q0 103-74 176t-176 74-177-74-73-176 73-177 177-73 176 73 74 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 152-31 126-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" /> <glyph glyph-name="lock" unicode="🔒" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" /> <glyph glyph-name="unlock" unicode="🔓" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" /> <glyph glyph-name="image" unicode="🖻" d="M357 529q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" /> <glyph glyph-name="folder" unicode="🗀" d="M929 511v-393q0-51-37-88t-88-37h-679q-51 0-88 37t-37 88v536q0 51 37 88t88 37h179q51 0 88-37t37-88v-18h375q51 0 88-37t37-88z" horiz-adv-x="928.6" /> <glyph glyph-name="document" unicode="🗅" d="M571 564v264q13-8 21-16l227-228q8-7 16-20h-264z m-71-18q0-22 16-37t38-16h303v-589q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h446v-304z" horiz-adv-x="857.1" /> </font> </defs> </svg> | > > > > > > > > | 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 | <glyph glyph-name="eye-off" unicode="⤫" d="M0 326q6 49 64 110 79 80 176 128 129 61 260 61 29 0 59-2l74 129q10 16 23 18 4 0 8-2l51-32q17-7 2-33l-57-101-47-79-41-72-144-250-41-72-47-80-57-100q-15-25-31-15l-53 31q-15 8 0 33l49 86-8 4q-103 53-176 129-64 72-64 109z m264 0q0-74 47-133l48 84q-9 24-9 49 0 51 34 91t85 50l49 84-18 0q-98 0-167-66t-69-159z m177-295l41 71 18 0q98 0 167 65t69 159q0 74-47 133l63 109q2-2 4-3t4-1q103-52 176-128 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-25 0-59 4z m90 155l110 189q9-25 9-49 0-51-34-90t-85-50z" horiz-adv-x="1000" /> <glyph glyph-name="radio-checked" unicode="⬤" d="M571 350q0-59-41-101t-101-42-101 42-42 101 42 101 101 42 101-42 41-101z m-142 304q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152-41 152-110 111-152 41z m428-304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="menu" unicode="𝍢" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" /> <glyph glyph-name="globe" unicode="🌍" d="M406 755c227 0 405-180 405-404 0-226-178-406-405-406-225 0-406 180-406 406 0 224 181 404 406 404z m23-734c65 0 100 113 116 169-39-4-77-6-116-7l0-162z m-129 82c19-45 48-82 81-82l0 162c-38 1-77 3-115 7 8-32 19-61 34-87z m-222 202c16-19 57-44 132-57-5 31-7 66-7 100 0 25 1 45 3 68-57 12-91 20-117 32-9-30-14-64-14-97 0-16 0-32 3-46z m170 43c0-37 2-75 7-109 43-8 84-11 126-13l0 177c-45 1-90 3-132 8 0-22-1-41-1-63z m131 331c-56-23-104-110-124-225 41-4 84-6 126-8l0 233-2 0z m82-18c-11 8-23 18-32 18l0-233c44 2 85 4 127 8-16 90-52 169-95 207z m-32-435c44 2 85 5 126 12 6 33 8 72 8 110 0 22 0 41-2 62-42-4-86-6-132-7l0-177z m176 190c3-23 3-43 3-68 0-34-3-70-7-102 130 22 135 52 135 105 0 32-5 65-15 95-23-10-66-25-116-30z m99 74c-35 78-101 140-179 168 38-49 61-118 74-199 67 12 95 24 105 31z m-531 93c-29-27-50-59-65-92 11-10 30-11 103-31 14 80 38 146 75 196-42-20-80-37-113-73z m0-465c30-33 67-55 106-72-29 40-48 92-60 152-50 11-95 25-125 41 19-46 42-88 79-121z m466 0c35 32 59 72 75 116-33-15-75-28-121-38-14-60-32-108-62-150 39 17 78 39 108 72z" horiz-adv-x="811" /> <glyph glyph-name="eye" unicode="👁" d="M0 350q6 49 64 110 79 80 176 129 129 60 260 60 137-2 260-60 103-53 176-129 64-73 64-110-6-49-64-109-79-80-176-129-129-61-260-61-137 2-260 61-103 53-176 129-64 72-64 109z m264 0q0-94 69-159t167-65 167 65 69 159-69 159-167 66-167-66-69-159z m86 1q0 60 44 102t106 42 106-42 44-102-44-102-106-43-106 43-44 102z" horiz-adv-x="1000" /> <glyph glyph-name="user" unicode="👤" d="M786 66q0-67-41-106t-108-39h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q5 0 24-12t41-27 60-27 75-12 74 12 61 27 41 27 24 12q34 0 62-11t48-30 34-45 24-55 15-60 8-61 2-58z m-179 498q0-88-63-151t-151-63-152 63-62 151 62 152 152 63 151-63 63-152z" horiz-adv-x="785.7" /> <glyph glyph-name="users" unicode="👪" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 23 12q35 0 63-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" /> <glyph glyph-name="calendar" unicode="📅" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" /> <glyph glyph-name="attach" unicode="📎" d="M783 77q0-65-44-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 88 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-13 5l-338 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-35 81-35 36 0 59 23t24 59q0 46-36 81l-324 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-7 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 83-36l325-324q55-54 55-131z" horiz-adv-x="785.7" /> <glyph glyph-name="search" unicode="🔍" d="M643 386q0 103-74 176t-176 74-177-74-73-176 73-177 177-73 176 73 74 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 152-31 126-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" /> <glyph glyph-name="lock" unicode="🔒" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" /> <glyph glyph-name="unlock" unicode="🔓" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" /> <glyph glyph-name="image" unicode="🖻" d="M357 529q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" /> <glyph glyph-name="gallery" unicode="🖼" d="M429 279v-215q0-29-22-50t-50-21h-286q-29 0-50 21t-21 50v215q0 29 21 50t50 21h286q29 0 50-21t22-50z m0 428v-214q0-29-22-50t-50-22h-286q-29 0-50 22t-21 50v214q0 29 21 50t50 22h286q29 0 50-22t22-50z m500-428v-215q0-29-22-50t-50-21h-286q-29 0-50 21t-21 50v215q0 29 21 50t50 21h286q29 0 50-21t22-50z m0 428v-214q0-29-22-50t-50-22h-286q-29 0-50 22t-21 50v214q0 29 21 50t50 22h286q29 0 50-22t22-50z" horiz-adv-x="928.6" /> <glyph glyph-name="folder" unicode="🗀" d="M929 511v-393q0-51-37-88t-88-37h-679q-51 0-88 37t-37 88v536q0 51 37 88t88 37h179q51 0 88-37t37-88v-18h375q51 0 88-37t37-88z" horiz-adv-x="928.6" /> <glyph glyph-name="document" unicode="🗅" d="M571 564v264q13-8 21-16l227-228q8-7 16-20h-264z m-71-18q0-22 16-37t38-16h303v-589q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h446v-304z" horiz-adv-x="857.1" /> <glyph glyph-name="reload" unicode="🗘" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" /> <glyph glyph-name="del-col" unicode="🮔" d="M0 886l0-998 307 0 0 998-307 0z m384 0l0-997 687 0 0 997-687 0z m217-255l143-144 144 144 105-105-144-144 144-144-105-104-144 144-143-144-105 104 144 144-144 144 105 105z" horiz-adv-x="1074" /> </font> </defs> </svg> |
Modified src/www/admin/static/font/garradin.ttf from [4beed9ef00] to [2e98cc8df3].
cannot compute difference between binary files
Modified src/www/admin/static/font/garradin.woff from [c61f50249f] to [36fee96d8f].
cannot compute difference between binary files
Modified src/www/admin/static/font/garradin.woff2 from [a65634acfa] to [f144ee71de].
cannot compute difference between binary files
Modified src/www/admin/static/handheld.css from [bb9608a6af] to [532d18b295].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | body { background-position: -180px 0px; background-attachment: scroll; font-size: 11pt; } input[type=number], input[type=color], input[type=email], input[type=file], input[type=url], input[type=month], input[type=password], input[type=range], input[type=search], input[type=tel], textarea, select, .input-list, .file-selector, input[type=text]:not([data-input]) { min-width: 0 !important; max-width: calc(100% - 2em); width: calc(95% - 2em); } a.icn-btn, input[type=submit], input[type=button], button, input[type=file] { padding: .4em .6em; } a.icn-btn { white-space: normal; } nav.breadcrumbs .quota { float: none; } nav.breadcrumbs ul { display: block; margin: .8em 0; } .header h1 { | > > > > > > > > > > > > | < < | > > > > > | < < < < < < | | < < | | > > > > > | | < | > | | > | | > | | < < < | > | | < > | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | body { background-position: -180px 0px; background-attachment: scroll; font-size: 11pt; } body main { padding-bottom: 7em; } input[type=number], input[type=color], input[type=email], input[type=file], input[type=url], input[type=month], input[type=password], input[type=range], input[type=search], input[type=tel], textarea, select, .input-list, .file-selector, input[type=text]:not([data-input]) { min-width: 0 !important; max-width: calc(100% - 2em); width: calc(95% - 2em); } input[size] { width: auto; } img { max-width: 100% !important; } a.icn-btn, input[type=submit], input[type=button], button, input[type=file] { padding: .4em .6em; } a.icn-btn { white-space: normal; } nav.breadcrumbs .quota { float: none; } nav.breadcrumbs ul { display: block; margin: .8em 0; } .header h1 { margin: 1em; text-align: center; font-size: 1.2em; } .header .menu { background: none !important; position: fixed; overflow: visible; bottom: inherit; z-index: 10000; margin: 0; width: 100vw; padding: 0; display: block; bottom: 0; top: inherit; left: 0; right: 0; } .header .menu .logo { display: none; } .header .menu *, .header .menu a { margin: 0; padding: 0; } .header .menu > ul { background: rgb(var(--gMainColor)); flex-wrap: nowrap; grid-template: none / repeat(auto-fit, minmax(20px, 1fr)); display: grid; align-items: center; } .header .menu > ul > li > h3 { display: inline; } .header .menu > ul > li > h3 a span { display: none; } .header .menu > ul > li > ul { display: none; } .header .menu h3 a { text-align: center; text-decoration: none !important; } .header .menu h3 b[data-icn]::before { float: none; display: block; position: relative; right: 0; margin: 0; padding: 10pt 0; line-height: 10pt; color: rgb(var(--gBgColor)); font-size: 20pt; } .header .menu li.current_parent h3 a { background: rgb(var(--gBgColor)); } .header .menu li.current h3 b[data-icn]::before, .header .menu > ul > li.current_parent h3 b[data-icn]::before { background: rgb(var(--gSecondColor)); color: rgb(var(--gBgColor)); text-shadow: 0px 0px 5px rgb(var(--gTextColor)), 0px 0px 5px rgb(var(--gTextColor)); } .header .menu > ul > li.current > ul, .header .menu > ul > li.current_parent > ul { display: flex; flex-wrap: wrap; justify-content: center; position: absolute; bottom: 30pt; left: 0; right: 0; background: rgb(var(--gSecondColor)); } .header .menu > ul > li.current > ul li, .header .menu > ul > li.current_parent > ul li { text-align: center; display: block; margin: .3rem; } .header .menu > ul > li.current > ul a, .header .menu > ul > li.current_parent > ul a { background: rgb(var(--gBgColor)); border-radius: .5em; color: rgb(var(--gMainColor)); padding: .3rem .6rem; font-size: 1em; font-weight: normal; } .header .menu > ul > li > ul li.current a { background: rgb(var(--gMainColor)); color: rgb(var(--gBgColor)); } main { margin: 0; padding: .1em; } |
︙ | ︙ | |||
169 170 171 172 173 174 175 | } .filterCategory, .searchMember { width: auto; float: none; } | | | 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | } .filterCategory, .searchMember { width: auto; float: none; } fieldset.wikiMain, fieldset.wikiRights, fieldset.wikiEncrypt { float: none; width: auto; } dl.describe { margin: 0 .5em; } |
︙ | ︙ | |||
286 287 288 289 290 291 292 | } #dialog > button { border: none; background: none; } | < < < < | 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | } #dialog > button { border: none; background: none; } dl.list { text-align: center; } table.statement td, table.statement tr { display: block; } |
︙ | ︙ | |||
324 325 326 327 328 329 330 331 332 | .datepicker-parent { position: static; } dialog.datepicker { margin: auto; } } | > > > | 331 332 333 334 335 336 337 338 339 340 341 342 | .datepicker-parent { position: static; } dialog.datepicker { margin: auto; left: 0; right: 0; width: 95%; } } |
Modified src/www/admin/static/scripts/accounting.js from [2c31b52794] to [93a05e165e].
︙ | ︙ | |||
119 120 121 122 123 124 125 | // Toggle parts of the form when a type is selected function selectType(v) { hideAllTypes(); g.toggle('[data-types~=t' + v + ']', true); g.toggle('[data-types=all-but-advanced]', v != 0); // Disable required form elements, or the form won't be able to be submitted $('[data-types=all-but-advanced] input[required]').forEach((e) => { | | | 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | // Toggle parts of the form when a type is selected function selectType(v) { hideAllTypes(); g.toggle('[data-types~=t' + v + ']', true); g.toggle('[data-types=all-but-advanced]', v != 0); // Disable required form elements, or the form won't be able to be submitted $('[data-types=all-but-advanced] input[required]').forEach((e) => { e.disabled = v == 0 ? true : false; }); } var radios = $('fieldset input[type=radio][name=type]'); radios.forEach((e) => { |
︙ | ︙ |
Modified src/www/admin/static/scripts/code_editor.css from [7c23f5ec10] to [e8169a1071].
1 2 3 | .codeEditor { width: 100%; height: 600px; | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | .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; |
︙ | ︙ | |||
59 60 61 62 63 64 65 | border-radius: .2em; cursor: pointer; text-indent: -70em; overflow: hidden; background: transparent no-repeat center center; } | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 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="); } |
︙ | ︙ | |||
82 83 84 85 86 87 88 | .codeEditor .lineCount { position: absolute; top: 34px; left: 0; bottom: 22px; width: 46px; text-align: right; | | | | | 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 | .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; |
︙ | ︙ |
Modified src/www/admin/static/scripts/code_editor.js from [9902ec3c83] to [8da523e07f].
1 2 3 4 5 6 7 8 9 | (function (){ g.style('scripts/code_editor.css'); 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 '/')", | > | 1 2 3 4 5 6 7 8 9 10 | (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 '/')", |
︙ | ︙ | |||
87 88 89 90 91 92 93 | }; } else { appendButton('fullscreen', 'Plein écran', code.toggleFullscreen); } g.setParentDialogHeight('90%'); | | | 88 89 90 91 92 93 94 95 96 | }; } else { appendButton('fullscreen', 'Plein écran', code.toggleFullscreen); } g.setParentDialogHeight('90%'); })}); }()); |
Modified src/www/admin/static/scripts/color_helper.js from [65ca233b3d] to [e68352a65f].
1 2 3 4 5 6 7 8 | (function () { if (!document.documentElement.style.setProperty || !window.CSS || !window.CSS.supports || !window.CSS.supports('--var', 0)) { return; } | | > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | (function () { if (!document.documentElement.style.setProperty || !window.CSS || !window.CSS.supports || !window.CSS.supports('--var', 0)) { return; } const logo_limit_x = 170; const bg_color = getVariable('gBgColor').split(',').map(e => parseInt(e, 10)) || [255, 255, 255]; const text_color = getVariable('gTextColor').split(',').map(e => parseInt(e, 10)) || [0, 0, 0]; function getVariable(var_name) { return getComputedStyle(document.documentElement).getPropertyValue('--' + var_name); } function colorToRGB(color, type) { // Conversion vers décimal RGB return color.replace(/^#/, '').match(/.{1,2}/g).map(function (el) { // On limite la luminosité comme ça, c'est pas parfait mais ça marche return Math.min(parseInt(el, 16), type == 'gMainColor' ? 180 : 220); |
︙ | ︙ | |||
24 25 26 27 28 29 30 | }).join(''); } function changeColor(element, color) { let new_color = colorToRGB(color, element); | | > | | > > > > > > > | 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 | }).join(''); } function changeColor(element, color) { let new_color = colorToRGB(color, element); let contrast_color = element == 'gMainColor' ? bg_color : text_color; let sum = contrast_color.reduce((pv, cv) => pv + cv, 0); let change = sum < (127*3) ? 5 : -5; while (!checkContrast(new_color, contrast_color)) { new_color[0] += change; new_color[1] += change; new_color[2] += change; } for (i in new_color) { new_color[i] = Math.max(new_color[i], 0); new_color[i] = Math.min(new_color[i], 255); } console.log(new_color, contrast_color, change); // Mise à jour variable CSS document.documentElement.style.setProperty('--' + element, new_color.join(',')); applyColors(); return new_color.join(','); } |
︙ | ︙ | |||
158 159 160 161 162 163 164 | for(var y = 0; y < imgData.height; y++) { for(var x = 0; x < imgData.width; x++) { var avg = (data[i] * 0.3 + data[i+1] * 0.59 + data[i+2] * 0.11); var b = avg < 127 && (data[i+3] > 127); data[i] = b ? avg : 255; // red data[i+1] = b ? avg : 255; // green data[i+2] = b ? avg : 255; // blue | | | 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | for(var y = 0; y < imgData.height; y++) { for(var x = 0; x < imgData.width; x++) { var avg = (data[i] * 0.3 + data[i+1] * 0.59 + data[i+2] * 0.11); var b = avg < 127 && (data[i+3] > 127); data[i] = b ? avg : 255; // red data[i+1] = b ? avg : 255; // green data[i+2] = b ? avg : 255; // blue data[i+3] = b ? (x >> logo_limit_x ? 50 : 150) : 0; i += 4; } } ctx.putImageData(imgData, 0, 0); var i = canvas.toDataURL('image/png'); |
︙ | ︙ |
Deleted src/www/admin/static/scripts/datepicker2.js version [317a9279e3].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/www/admin/static/scripts/file_input.js from [4e17f68bb7] to [1419162054].
︙ | ︙ | |||
198 199 200 201 202 203 204 205 206 207 | for (var i = 0; i < items.length; i++) { if (IMAGE_MIME_REGEX.test(items[i].type)) { let f = items[i].getAsFile(); let name = f.name.replace(/\./, '-' + (+(new Date)) + '.'); let f2 = new File([f], name, {type: f.type}); addItem(f2); return; } } | > < < | 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | for (var i = 0; i < items.length; i++) { if (IMAGE_MIME_REGEX.test(items[i].type)) { let f = items[i].getAsFile(); let name = f.name.replace(/\./, '-' + (+(new Date)) + '.'); let f2 = new File([f], name, {type: f.type}); addItem(f2); e.preventDefault(); return; } } }); } }; document.querySelectorAll('input[type=file][data-enhanced]').forEach((e) => { enhanceFileInput(e); }); }()); |
Deleted src/www/admin/static/scripts/file_upload.js version [dc9fa75b77].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Modified src/www/admin/static/scripts/global.js from [e05e0a53c7] to [acdc2ba874].
︙ | ︙ | |||
58 59 60 61 62 63 64 | else if (selector instanceof HTMLElement) { var elements = [selector]; } else { var elements = document.querySelectorAll(selector); } | | > | < | | | > > | 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | else if (selector instanceof HTMLElement) { var elements = [selector]; } else { var elements = document.querySelectorAll(selector); } for (var i = 0; i < elements.length; i++) { elements[i].classList.toggle('hidden', visibility ? false : true); // Make sure hidden elements are not really required // Avoid Chrome bug "An invalid form control with name='' is not focusable." elements[i].querySelectorAll('input[required], textarea[required], select[required], button[required]').forEach((e) => { e.disabled = !visibility ? true : (e.getAttribute('disabled') ? true : false); }); } return true; }; g.script = function (file, callback) { if (file in g.loaded) { |
︙ | ︙ | |||
297 298 299 300 301 302 303 304 305 306 | g.enhanceDateField = (input) => { var span = document.createElement('span'); span.className = 'datepicker-parent'; var btn = document.createElement('button'); var cal = null; btn.className = 'icn-btn'; btn.setAttribute('data-icon', '📅'); btn.type = 'button'; btn.onclick = () => { | > | > > > > > > > > > > > > > > > > > > | 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 | g.enhanceDateField = (input) => { var span = document.createElement('span'); span.className = 'datepicker-parent'; var btn = document.createElement('button'); var cal = null; btn.className = 'icn-btn'; btn.title = 'Cliquer pour ouvrir le calendrier. Utiliser les flèches du clavier pour sélectionner une date, et page précédente suivante pour changer de mois.'; btn.setAttribute('data-icon', '📅'); btn.type = 'button'; btn.onclick = () => { g.script('scripts/lib/datepicker2.min.js', () => { if (null == cal) { btn.onclick = null; cal = new DatePicker(btn, input, {lang: 'fr', format: 1}); cal.open(); } }); }; span.appendChild(btn); input.parentNode.insertBefore(span, input.nextSibling); const getCaretPosition = e => e && e.selectionStart || -1; const inputKeyEvent = (e) => { if (input.value.match(/^\d$|^\d\d?\/\d$/) && e.key.match(/^[0-9]$/)) { input.value += e.key + '/'; e.preventDefault(); return false; } if (e.key == '/' && input.value.slice(-1) == '/') { e.preventDefault(); return false; } }; input.addEventListener('keydown', inputKeyEvent, true); }; g.current_list_input = null; g.inputListSelected = function(value, label) { var i = g.current_list_input; |
︙ | ︙ | |||
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | // Sélecteurs de listes g.onload(() => { var inputs = $('form .input-list > button'); inputs.forEach((i) => { i.onclick = () => { g.current_list_input = i.parentNode; let url = i.value + (i.value.indexOf('?') > 0 ? '&' : '?') + '_dialog'; g.openFrameDialog(url); return false; }; }); var multiples = $('form .input-list span button'); multiples.forEach((btn) => { btn.onclick = () => btn.parentNode.parentNode.removeChild(btn.parentNode); }); | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | // Sélecteurs de listes g.onload(() => { var inputs = $('form .input-list > button'); inputs.forEach((i) => { i.onclick = () => { i.setCustomValidity(''); g.current_list_input = i.parentNode; let url = i.value + (i.value.indexOf('?') > 0 ? '&' : '?') + '_dialog'; g.openFrameDialog(url); return false; }; }); // Set custom error message if required list is not selected document.querySelectorAll('form').forEach((form) => { form.addEventListener('submit', (e) => { let inputs = form.querySelectorAll('.input-list > button[required]'); for (var k = 0; k < inputs.length; k++) { var i2 = inputs[k]; i2.type = 'submit'; // Force button to have error message, <button type="button"> cannot show validity message // Element is hidden or disabled if (!i2.offsetParent || i2.disabled) { i2.required = false; continue; } let v = i2.parentNode.querySelector('input[type="hidden"]'); if (!v || !v.value) { i2.setCustomValidity('Merci de faire une sélection.'); i2.reportValidity(); e.preventDefault(); return false; } } }); }); var multiples = $('form .input-list span button'); multiples.forEach((btn) => { btn.onclick = () => btn.parentNode.parentNode.removeChild(btn.parentNode); }); |
︙ | ︙ |
Modified src/www/admin/static/scripts/lib/code_editor.min.js from [b81debcc16] to [206f11e50f].
|
| < | | 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}}(); |
Added src/www/admin/static/scripts/lib/datepicker2.min.js version [a4c81fc228].
> | 1 | !function(){DATEPICKER_L10N={},DATEPICKER_L10N.en={weekdays:["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],months:["January","February","March","April","May","June","July","August","September","October","November","December"]},DATEPICKER_L10N.fr={weekdays:["Lun","Mar","Mer","Jeu","Ven","Sam","Dim"],months:["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"],labels:{"Previous month":"Mois précédent","Next month":"Mois suivant","Change year":"Choisir l'année","Change month":"Choisir le mois",Today:"Aujourd'hui"}},window.DatePicker=class{constructor(t,e,a){this.button=t,this.input=e,this.date=null,this.nav=[],this.header=null,Object.assign(this,{format:0,lang:"fr",class:"datepicker"},a);var i=document.createElement("dialog");i.className=this.class,this.container=t.parentNode.insertBefore(i,t.nextSibling),t.addEventListener("click",(()=>this.container.hasAttribute("open")?this.close():this.open()),!1)}open(){var e="";this.input?e=this.input.value:this.button.dataset&&this.button.dataset.date&&(e=this.button.dataset.date),""==e?e=new t:1==this.format?(e=e.split("/"),e=new t(e[2],e[1]-1,e[0])):e=new t(e),isNaN(e.getTime())&&(e=new t),this.date=e,this.buildHeader(),this.refresh(),this.focus(),this.container.setAttribute("open","open"),this.keyEvent=t=>{if(t.ctrlKey||t.altKey||t.shiftKey||t.metaKey)return!0;if("Escape"==t.key)return!!this.close();if(this.header.contains(t.target))return!0;var e=this.key(t.key);return e||t.preventDefault(),e},this.clickEvent=t=>!!this.container.contains(t.target)||(!!this.button.contains(t.target)||void this.close()),document.addEventListener("keydown",this.keyEvent,!0),document.addEventListener("click",this.clickEvent,!0)}key(t){switch(t){case"Enter":return!!this.select();case"ArrowLeft":return!!this.day(-1);case"ArrowRight":return!!this.day(1);case"ArrowUp":return!!this.day(-7);case"ArrowDown":return!!this.day(7);case"PageDown":return!!this.month(1,!0);case"PageUp":return!!this.month(-1,!0)}return!0}close(){this.container.innerHTML="",this.container.removeAttribute("open"),this.input&&this.input.select(),document.removeEventListener("keydown",this.keyEvent,!0),document.removeEventListener("click",this.clickEvent,!0)}generateTable(){var t=t=>document.createElement(t),e=t("table"),a=t("thead"),i=t("tr"),s=t("tbody");return DATEPICKER_L10N[this.lang].weekdays.forEach((e=>{var a=t("td");a.innerHTML=e,i.appendChild(a)})),a.appendChild(i),e.appendChild(a),this.date.getCalendarArray().forEach((e=>{var a=t("tr");e.forEach((e=>{var i=t("td");e&&(i.innerHTML=`<input type="button" value="${e.getDate()}" />`,i.firstChild.onclick=t=>this.select(t.target.value),i.firstChild.onfocus=t=>this.date.setDate(t.target.value)&&this.focus(!1)),a.appendChild(i)})),s.appendChild(a)})),e.appendChild(s),e}buildHeader(){let t=["Previous month","Next month","Change month","Change year","Today"];t=t.map((t=>DATEPICKER_L10N[this.lang].labels[t]||t));let e=0,a=DATEPICKER_L10N[this.lang].months.map((t=>`<option value="${e++}">${t}</option>`)),i=this.date.getFullYear();this.header=document.createElement("nav"),this.header.innerHTML=`<input type="button" value="←" title="${t[0]}" />\n\t\t\t\t<span><select title="${t[2]}">${a}</select> <input type="number" size="5" step="1" min="1" max="2500" title="${t[3]}" value="${i}"></span>\n\t\t\t\t<input type="button" value="↺" title="${t[4]}" />\n\t\t\t\t<input type="button" value="→" title="${t[1]}" />`,this.nav=this.header.querySelectorAll("input, select"),this.nav[0].onclick=()=>(this.month(-1,!0),!1),this.nav[3].onclick=()=>(this.today(),!1),this.nav[4].onclick=()=>(this.month(1,!0),!1),this.nav[1].value=this.date.getMonth(),this.nav[1].onchange=()=>this.month(this.nav[1].value,!1),this.nav[2].onchange=()=>this.year(this.nav[2].value),this.nav[2].onclick=t=>t.target.select(),this.nav[2].onkeyup=()=>{let t=this.nav[2].value;4==t.length&&this.year(t)},this.container.appendChild(this.header)}refresh(){this.nav[1].value=this.date.getMonth(),this.nav[2].value=this.date.getFullYear();let t=this.container.childNodes;2==t.length&&t[1].remove(),this.container.appendChild(this.generateTable())}year(t){this.date.getFullYear()!=t&&(this.date.setYear(t),this.refresh())}today(){this.date=new t,this.refresh(),this.focus()}month(t,e){let a=this.date.getDate(),i=e?this.date.getMonth()+t:t;this.date.setMonth(i),this.date.getDate()!=a&&this.date.setDate(0),this.refresh(),this.focus(!1)}day(e){var a=new t(this.date);this.date.setDate(this.date.getDate()+e),this.date.getMonth()!=a.getMonth()&&this.refresh(),this.focus()}select(t){t&&this.date.setDate(parseInt(t,10));var e=this.date.getFullYear(),a=("0"+(this.date.getMonth()+1)).substr(-2),i=("0"+this.date.getDate()).substr(-2);let s=1==this.format?i+"/"+a+"/"+e:e+"-"+a+"-"+i;this.input&&(this.input.value=s),this.button.dataset&&this.button.dataset.date&&(this.button.dataset.date=s),this.close();var n=document.createEvent("HTMLEvents");n.initEvent("change",!0,!0),n.eventName="change",(this.input||this.button).dispatchEvent(n)}focus(t){this.container.querySelectorAll("tbody td").forEach((e=>{e.firstChild&&(parseInt(e.firstChild.value,10)===this.date.getDate()?(!1!==t&&e.firstChild.focus(),e.className="focus"):e.className="")}))}};class t extends Date{getCalendarArray(){for(var e=new t(this.getFullYear(),this.getMonth(),1),a=[],i=e.getDayOfWeek(),s=0;s<i-1;s++)a.push(null);for(;e.getMonth()===this.getMonth();)a.push(new t(e)),e.setDate(e.getDate()+1);i=e.getDayOfWeek();for(s=0;s<=7-i;s++)a.push(null);for(var n=[];a.length;)n.push(a.splice(0,7));return n}getDayOfWeek(){var t=this.getDay();return 0==t?7:t}}}(); |
Name change from src/www/admin/static/scripts/gibberish-aes.min.js to src/www/admin/static/scripts/lib/gibberish-aes.min.js.
︙ | ︙ |
Name change from src/www/admin/static/scripts/query_builder.min.js to src/www/admin/static/scripts/lib/query_builder.min.js.
Modified src/www/admin/static/scripts/lib/text_editor.min.js from [83bda3f0af] to [efdbdc1b49].
|
| | | 1 | window.textEditor=function(t){if(!document.getElementById(t))throw new Error("Invalid ID parameter: "+t);return this.id=t,this.textarea=document.getElementById(t),this.shortcuts=[],this.supportsExecCommand=null,"selectionStart"in this.textarea&&(this.textarea.addEventListener("keydown",this.keyEvent.bind(this),!0),this.textarea.addEventListener("keypress",this.keyEvent.bind(this),!0),!0)},textEditor.prototype.keyEvent=function(t){var e,t=t||window.event;for(e in this.shortcuts){var r=this.shortcuts[e];if(!t.metaKey&&(!(t.ctrlKey&&!r.ctrl||r.ctrl&&!t.ctrlKey)&&!(t.shiftKey&&!r.shift||r.shift&&!t.shiftKey)&&!(t.altKey&&!r.alt||r.alt&&!t.altKey)&&(e=this.matchKeyPress(r.key,t)))){if("function"==typeof r.callback)return!r.callback.call(this,t,e)||this.preventDefault(t);var n=(r.ctrl?"Ctrl-":"")+(r.alt?"Alt-":"");throw n+=(r.shift?"Shift-":"")+r,new Error("Invalid callback type for shortcut "+n)}}return!0},textEditor.prototype.matchKeyPress=function(t,e){return!(e.defaultPrevented||!e.key)&&(t=t.toLowerCase(),e.key.toLowerCase()==t&&t)},textEditor.prototype.preventDefault=function(t){return t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),t.returnValue=!1,!(t.cancelBubble=!0)},textEditor.prototype.getSelection=function(){var t=this.textarea,e=t.selectionEnd-t.selectionStart;return{start:t.selectionStart,end:t.selectionEnd,length:e,text:t.value.substr(t.selectionStart,e)}},textEditor.prototype.replaceSelection=function(t,e){var r=t.start,n=r+e.length;return this.setSelection(r,t.end),this.insert(e),this.setSelection(r,n),{start:r,end:n,length:e.length,text:e}},textEditor.prototype.insert=function(t){var e;return!0===this.supportsExecCommand?document.execCommand("insertText",!1,t):null===this.supportsExecCommand&&(e=this.textarea.value,document.execCommand("insertText",!1,t),this.supportsExecCommand=e!==this.textarea.value),!1===this.supportsExecCommand&&this.textarea.setRangeText(t,this.textarea.selectionStart,this.textarea.selectionEnd,"end"),this.supportsExecCommand},textEditor.prototype.insertAtPosition=function(t,e,r){var n=t+e.length,t=this.getSelection();return this.setSelection(t.start,t.end),this.insert(e),r=r||n,this.setSelection(r,r)},textEditor.prototype.setSelection=function(t,e){var r=this.textarea;return r.focus(),r.selectionStart=t,r.selectionEnd=e,this.getSelection()},textEditor.prototype.scrollToSelection=function(t){var e=this.textarea,r=e.value.substr(t.end);e.value=e.value.substr(0,t.end),e.scrollTop=1e5;var n=e.scrollTop;e.value+=r,e.scrollTop=n,this.setSelection(t.start,t.end)},textEditor.prototype.wrapSelection=function(t,e,r){var n=this.textarea,o=n.scrollTop,s=t.text,t=this.replaceSelection(t,e+s+r);return""==s&&(t=this.setSelection(t.start+e.length,t.start+e.length)),n.scrollTop=o,t}; |
Modified src/www/admin/static/scripts/wiki-encryption.js from [f41b9dc8c7] to [0742c294f5].
︙ | ︙ | |||
19 20 21 22 23 24 25 | if (aes_loaded) { if (callback) { callback(); } return; } | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | if (aes_loaded) { if (callback) { callback(); } return; } var url = www_url + 'admin/static/scripts/lib/gibberish-aes.min.js'; var s = document.createElement('script'); s.src = url; s.type = 'text/javascript'; s.onload = function () { aes_loaded = true; if (callback) { callback(); |
︙ | ︙ |
Modified src/www/admin/static/scripts/wiki_editor.css from [531ffd0eea] to [57a2c80c94].
1 2 | .textEditor { border-radius: .5em; | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | .textEditor { border-radius: .5em; background: var(--gLightBorderColor); padding: 1%; overflow: hidden; position: relative; } .textEditor textarea { border: none; margin: 0; background: var(--gLightBackgroundColor); border-radius: .5em; height: 96%; width: 98%; } nav.te { margin-bottom: .5em; height: 30px; } nav.te button { text-decoration: none; cursor: pointer; background: var(--gLightBackgroundColor) no-repeat center center; display: inline-block; vertical-align: bottom; transition: all .2s; border: 1px solid var(--gBorderColor); box-shadow: 2px 2px 5px var(--gBorderColor); } nav.te .bold, nav.te .italic, nav.te .title, nav.te .link { font-family: Georgia, "Times New Roman", serif; } nav.te .bold { font-weight: bold; } nav.te .italic { font-style: italic; } nav.te .link { text-decoration: underline; color: var(--gLinkColor); } nav.te .fullscreen { text-indent: -70em; width: 32px; overflow: hidden; } |
︙ | ︙ | |||
115 116 117 118 119 120 121 | .textEditor.iframe nav button.close, .textEditor.iframe nav button.reload { display: inline-block; } .textEditor iframe { border: none; | | | 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | .textEditor.iframe nav button.close, .textEditor.iframe nav button.reload { display: inline-block; } .textEditor iframe { border: none; background: rgb(var(--gBgColor)); border-radius: .5em; padding: 1%; width: 98%; } .textEditor iframe.hidden { visibility: hidden; |
︙ | ︙ | |||
161 162 163 164 165 166 167 | background: rgba(0, 0, 0, 0.75); } form#insertImage fieldset { box-shadow: 5px 5px 10px #000; margin: 1em; padding: 1em; | | < < | 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 | background: rgba(0, 0, 0, 0.75); } form#insertImage fieldset { box-shadow: 5px 5px 10px #000; margin: 1em; padding: 1em; background: var(--gLightBackgroundColor); border-radius: .5em; text-align: center; } form#insertImage dt { margin: .2em 0; } form#insertImage .align input { font-size: 1.2em; line-height: 32px; background-position: 5px center; background-repeat: no-repeat; padding: 5px; padding-left: 42px; border-radius: 5px; margin: .5em; } form#insertImage .align input[name=""] { background-image: url("../pics/img_flow.png"); } |
︙ | ︙ | |||
201 202 203 204 205 206 207 | form#insertImage .align input[name="center"] { background-image: url("../pics/img_center.png"); } form#insertImage .cancel input { font-size: 0.8em; | < < < < < < < | < < < < | 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | form#insertImage .align input[name="center"] { background-image: url("../pics/img_center.png"); } 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; } |
Modified src/www/admin/static/scripts/wiki_fichiers.js from [6d13b317ce] to [88a6ff9b9e].
︙ | ︙ | |||
21 22 23 24 25 26 27 | f.querySelector('dd.image').innerHTML = ''; var img = document.createElement('img'); img.src = file.thumb; img.alt = ''; f.querySelector('dd.image').appendChild(img); | | | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | f.querySelector('dd.image').innerHTML = ''; var img = document.createElement('img'); img.src = file.thumb; img.alt = ''; f.querySelector('dd.image').appendChild(img); f.querySelector('dd.cancel [type=reset]').onclick = function() { f.style.display = 'none'; if (from_upload) { location.href = location.href; } }; |
︙ | ︙ |
Modified src/www/admin/static/styles/01-layout.css from [9046a79977] to [f9bc121654].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* marron : #9c4f15 rgb(156, 79, 21) orange : #d98628 rgb(217, 134, 40) */ :root { --gBgColor: 255, 255, 255; --gMainColor: 156, 79, 21; --gSecondColor: 217, 134, 40; --gBgImage: url("../gdin_bg.png"); } html { width: 100%; height: 100%; } body { font-size: 100%; | > > > > > > > > > > > > > > > > > > > | > > | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | /* marron : #9c4f15 rgb(156, 79, 21) orange : #d98628 rgb(217, 134, 40) */ /* Light colors */ :root { --gBgColor: 255, 255, 255; --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("../gdin_bg.png"); } /* Dark colors */ html.dark { --gBgColor: 30, 30, 30; --gTextColor: 225, 225, 225; --gBorderColor: #999; --gLightBorderColor: #333; --gLightBackgroundColor: #222; --gLinkColor: #99f; --gHoverLinkColor: 250, 127, 127; } html { width: 100%; height: 100%; } body { font-size: 100%; color: rgb(var(--gTextColor)); font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif; padding-bottom: 1em; background: rgb(var(--gBgColor)) var(--gBgImage) no-repeat 0px 0px fixed; } main { margin: 0px 1em 1em 180px; position: relative; } main img { max-width: 100%; } a { color: var(--gLinkColor); } a:hover { color: rgb(var(--gHoverLinkColor)); } .header h1 { color: rgb(var(--gMainColor)); margin-left: 180px; margin-bottom: 0.4em; } |
︙ | ︙ | |||
66 67 68 69 70 71 72 | top: 0; bottom: 0; background: rgb(var(--gMainColor)) var(--gBgImage) no-repeat 0px 0px; } .header .menu::-webkit-scrollbar { width: 8px; | | | | > | | 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 | top: 0; bottom: 0; background: rgb(var(--gMainColor)) var(--gBgImage) no-repeat 0px 0px; } .header .menu::-webkit-scrollbar { width: 8px; background: rgba(var(--gBgColor), 0.25); box-shadow: inset 0px 0px 10px #666; } .header .menu::-webkit-scrollbar-thumb { background: rgba(var(--gBgColor), 0.5); border-radius: 10px; } .header .menu h3 { font-weight: bold; font-size: inherit; } .header .menu a { color: #fff; color: rgb(var(--gBgColor)); font-weight: bold; padding: 0.4em 0.4em 0.4em 1em; |
︙ | ︙ | |||
99 100 101 102 103 104 105 | } .header .menu li li a { font-size: 0.8em; padding-left: 2em; } | | < | > > > > > > > > > | > | | < | 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 | } .header .menu li li a { font-size: 0.8em; padding-left: 2em; } .header .menu li.current h3 a, .header .menu ul ul li.current a { background: rgb(var(--gBgColor)); color: rgb(var(--gMainColor)); } .header .menu h3 a { position: relative; } .header .menu h3 b[data-icn]::before { position: absolute; right: .4rem; content: attr(data-icn); display: block; font-family: "gicon"; float: right; text-decoration: none; margin-top: -.2em; font-size: 20pt; color: rgba(var(--gBgColor), .5); font-weight: normal; } .header .menu li.current h3 b[data-icn]::before { color: rgba(var(--gMainColor)); } ul.gallery { text-align: center; } ul.gallery li { display: inline-block; |
︙ | ︙ |
Modified src/www/admin/static/styles/02-common.css from [72a91c6a02] to [cc95968637].
︙ | ︙ | |||
23 24 25 26 27 28 29 | .block table { margin: 1rem 0; } .block table th, .block table td { vertical-align: top; padding: .2rem .4rem; | | | > > | | > > > > | | | | 23 24 25 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 | .block table { margin: 1rem 0; } .block table th, .block table td { vertical-align: top; padding: .2rem .4rem; border: 1px solid var(--gBorderColor); } .alert.block, .error.block, .confirm.block, .help.block { border: 1px solid var(--gLightBorderColor); padding: .5em; margin: .5em 0; border-radius: .3em; padding-left: 3em; position: relative; clear: both; color: #000; } .alert.block { border-color: #cc0; background-color: #ffc; } .error.block { border-color: #c00; background-color: #fcc; } .confirm.block { border-color: #0c0; background-color: #cfc; } .help.block { color: rgb(var(--gTextColor)); border-color: var(--gLightBorderColor); background-color: var(--gLightBackgroundColor); } .confirm.block a.icn-btn, .alert.block a.icn-btn, .error.block a.icn-btn { color: #000; } .confirm.block::before, .alert.block::before, .error.block::before, .help.block::before { font-family: "gicon"; left: .5em; top: .2em; position: absolute; font-size: 1.5em; text-shadow: 2px 2px 5px var(--gLightBorderColor); } .confirm.block::before { content: "☑"; color: green; } .alert.block::before { content: "⚠"; color: yellow; } .error.block::before { content: "⚠"; color: red; } .help.block::before { content: "❓"; color: var(--gBorderColor); } .help { color: var(--gBorderColor); } p.help:not(.block) { margin: 1em; } .help ul li { |
︙ | ︙ | |||
115 116 117 118 119 120 121 | margin-left: 1.5em; list-style: disc; } .ruler { margin: .5em; text-align: center; | < | | 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | margin-left: 1.5em; list-style: disc; } .ruler { margin: .5em; text-align: center; overflow: hidden; } .ruler:before, .ruler:after { background-color: var(--gLightBorderColor); content: ""; display: inline-block; height: 1px; position: relative; vertical-align: middle; width: 50%; } |
︙ | ︙ | |||
146 147 148 149 150 151 152 | .num a, a.num { text-decoration: none; border-radius: .5rem; display: inline-block; text-align: center; padding: 0 .3rem; background: rgba(var(--gMainColor), 0.7); | | | 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | .num a, a.num { text-decoration: none; border-radius: .5rem; display: inline-block; text-align: center; padding: 0 .3rem; background: rgba(var(--gMainColor), 0.7); color: rgb(var(--gBgColor)); white-space: pre; } .permissions b { border: 2px solid #999; border-radius: 1em; color: #000; |
︙ | ︙ | |||
221 222 223 224 225 226 227 228 229 230 231 232 233 234 | .infos dl { margin-bottom: 0.8em; } .infos dl dd { margin: 0.2em 1em; } .shortFormRight { width: 30em; float: right; text-align: center; margin-left: 1em; } | > > > > > > > > > | 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | .infos dl { margin-bottom: 0.8em; } .infos dl dd { margin: 0.2em 1em; } .shortForm { text-align: center; } .shortForm p.help { margin: .5em 0; font-size: .9em; } .shortFormRight { width: 30em; float: right; text-align: center; margin-left: 1em; } |
︙ | ︙ | |||
281 282 283 284 285 286 287 | dl.list dt { font-size: 1.2em; font-weight: bold; margin-top: .8em; } dl.list dd.desc { | | | < < > > > > | | | 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 | dl.list dt { font-size: 1.2em; font-weight: bold; margin-top: .8em; } dl.list dd.desc { color: var(--gLightBorderColor); } dl.describe { margin-bottom: 1rem; display: grid; grid-template: auto / 15rem 1fr; } dl.describe > dt { grid-column: 1; margin: .2rem .5rem; text-align: right; color: var(--gBorderColor); align-self: start; } dl.describe > dd { grid-column: 2; margin: .2rem .5rem; align-self: center; } dl.describe ul { margin-left: 1.5em; list-style-type: disc; } dl.cotisation { background: rgba(var(--gSecondColor), 0.2); padding: .5em; border-radius: .5em; margin: 1em; } dl.cotisation dt { font-weight: bold; } dl.cotisation dd { margin: .2em 0 .4em 1em; } dl.cotisation dd.disabled { color: var(--gBorderColor); } aside.describe { width: 20em; float: right; margin: .5em; background: rgba(var(--gSecondColor), 0.2); border-radius: .5em; border: 2px solid rgba(var(--gSecondColor), 0.5); padding: .5em; z-index: 200; color: var(--gBorderColor); } aside.describe dl.describe { display: block; } aside.describe dl.describe dt { text-align: left; font-weight: bold; color: rgb(var(--gTextColor)); } .hidden { display: none; } img.qrcode { |
︙ | ︙ | |||
388 389 390 391 392 393 394 | content: "↓"; position: absolute; left: 0; top: 0; bottom: 0; /* From .icn-btn */ cursor: pointer; | < | | 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 | content: "↓"; position: absolute; left: 0; top: 0; bottom: 0; /* From .icn-btn */ cursor: pointer; border: 1px solid rgba(var(--gMainColor), 0.5); background-color: rgba(var(--gSecondColor), 0.1); user-select: none; display: inline-block; font-size: inherit; border-radius: .2em; padding: .2em .4em; margin: auto .5em; height: 1em; white-space: pre; transition: color .3s, background-color .3s; font-family: "gicon", sans-serif; text-shadow: 1px 1px 1px var(--gLightBorderColor); font-size: 1.2em; } details[open] summary::after { content: "↑"; } |
︙ | ︙ | |||
438 439 440 441 442 443 444 | text-align: center; padding: .5rem; margin: .5rem; } .files-list aside small { display: block; | | | | | 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 | text-align: center; padding: .5rem; margin: .5rem; } .files-list aside small { display: block; color: var(--gBorderColor); } nav.breadcrumbs { margin: .5em 0; color: var(--gLightBorderColor); } nav.breadcrumbs a { color: var(--gBorderColor); } nav.breadcrumbs ul, nav.breadcrumbs li { list-style-type: none; display: inline; } |
︙ | ︙ | |||
471 472 473 474 475 476 477 | float: right; } aside.quota { background: rgba(var(--gMainColor), 0.1); border-radius: .5rem; padding: .2rem .5rem; | | | 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 | float: right; } aside.quota { background: rgba(var(--gMainColor), 0.1); border-radius: .5rem; padding: .2rem .5rem; color: rgb(var(--gTextColor)); margin: .5em 0; } aside.quota i { font-style: normal; } |
︙ | ︙ | |||
517 518 519 520 521 522 523 | } .search-results h3 { margin: .3em 0; } .search-results h3 a { | < | 532 533 534 535 536 537 538 539 540 | } .search-results h3 { margin: .3em 0; } .search-results h3 a { font-weight: normal; } |
Modified src/www/admin/static/styles/03-forms.css from [8a097bba5f] to [b01f09fb90].
1 2 | /* Forms */ fieldset { | | | > > > > | | < < < < < | | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | /* Forms */ fieldset { border: 1px solid var(--gLightBorderColor); padding: 0.8em 1em 0 1em; margin-bottom: 1em; padding: 0.5em; } fieldset legend { padding: 0 0.5em; font-weight: bold; color: rgb(var(--gTextColor)); } /* Override selector in 06-tables.css */ table tr.clickable:hover, table tr.clickable:nth-child(even):hover { cursor: pointer; color: #633; background: #ffc; } table tr.clickable:hover button, table tr.focused button { color: rgb(var(--gHoverLinkColor)); } table tr.focused { color: #633; background: #ffc !important; box-shadow: 0 0 5px .2rem #990; } dl dt label { font-weight: bold; } fieldset dl dt b { color: rgb(var(--gHoverLinkColor)); font-size: 0.7em; font-weight: normal; vertical-align: super; } fieldset dl dt i { color: var(--gLightBorderColor); font-size: 0.7em; font-weight: normal; vertical-align: super; } fieldset dl dd { padding: 0.2em 0.5em 0.2em 1em; } fieldset dl dd ol, fieldset dl dd ul { margin-left: 1.5em; } fieldset dl dl { margin: .5em 0 .5em 1.2em; } label:hover { cursor: pointer; } input[type=checkbox] + label:hover { border: none; } /* We can't use :not([type=checkbox]):not([type=radio]) here as it is too specific and then it's a mess to override the selector after... */ input[type=text], input[type=number], input[type=color], input[type=date], input[type=datetime-local], input[type=datetime], input[type=time], input[type=week], input[type=email], input[type=file], input[type=url], input[type=month], input[type=password], input[type=range], input[type=search], input[type=tel], textarea, select, .input-list, .file-selector { padding: .4rem .6rem; font-family: inherit; min-width: 20em; max-width: 100%; border: 1px solid rgb(var(--gMainColor)); font-size: inherit; background: rgb(var(--gBgColor)); color: rgb(var(--gTextColor)); border-radius: .25rem; transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; } textarea.full-width { width: calc(100% - 1rem); } input:not(:placeholder-shown):focus:invalid, textarea:not(:placeholder-shown):focus:invalid { border-color: rgb(var(--gHoverLinkColor)); box-shadow: 0 0 5px .3rem rgba(var(--gHoverLinkColor), 0.5); } input.time { text-align: center; padding: .2em 0; } /* Fake checkbox and radio buttons */ input[type=checkbox], input[type=radio] { position: absolute; opacity: 0; } input[type=checkbox] + label::before, input[type=radio] + label::before { display: inline-block; width: 1em; height: 1em; text-align: center; transition: color .2s, box-shadow .2s ease-in-out; text-shadow: 1px 1px 3px var(--gLightBorderColor); cursor: pointer; font-family: gicon; font-size: 1.2rem; font-weight: normal; color: rgb(var(--gMainColor)); margin-right: .5em; border-radius: .25rem; |
︙ | ︙ | |||
140 141 142 143 144 145 146 | } input:hover + label::before { color: rgb(var(--gSecondColor)); } input:checked + label::before { | | < < < < < | | | > > > > > | | | > > > > > > | | 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 | } input:hover + label::before { color: rgb(var(--gSecondColor)); } input:checked + label::before { text-shadow: 1px 1px 5px rgba(var(--gSecondColor), 0.5); } #queryBuilder input[type=checkbox] { position: unset; opacity: unset; } /* buttons */ input[type=submit], input[type=button], button, input[type=file] { border-radius: 1em; border: none; box-shadow: 0px 0px 5px 0 var(--gLightBorderColor); cursor: pointer; border: 2px solid rgba(var(--gMainColor), 0.5); background: rgba(var(--gSecondColor), 0.2); display: inline-block; font-size: inherit; border-radius: .2em; padding: .2em .4em; margin: .2em .5em; text-decoration: none; transition: color .3s, background-color .3s; color: rgb(var(--gTextColor)); } input:focus, button:focus, select:focus, textarea:focus, input[type=radio]:focus + label::before, input[type=checkbox]:focus + label::before { box-shadow: 0 0 5px .2rem rgba(var(--gMainColor), 0.5); outline: 0; } a.icn-btn, b.btn { cursor: pointer; color: rgb(var(--gTextColor)); border: 1px solid rgba(var(--gMainColor), 0.5); background: rgba(var(--gSecondColor), 0.1); user-select: none; display: inline-block; font-size: inherit; border-radius: .2em; padding: .2em .4em; margin: .2em .5em; white-space: pre; transition: color .3s, background-color .3s; text-decoration: underline; } b.btn { cursor: unset; text-decoration: none; border-bottom: 1px dashed rgba(var(--gMainColor), 0.5); } [data-icon]:before, .main[data-icon]:after { display: inline-block; font-family: "gicon", sans-serif; text-shadow: 1px 1px 1px var(--gLightBorderColor); padding-right: .5em; font-size: 1.2em; line-height: .8em; vertical-align: middle; content: attr(data-icon); } |
︙ | ︙ | |||
223 224 225 226 227 228 229 | display: inline-block; text-align: center; font-size: 1.2em; line-height: .8em; vertical-align: middle; padding: .2em; font-family: "gicon", sans-serif; | < | | | 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 | display: inline-block; text-align: center; font-size: 1.2em; line-height: .8em; vertical-align: middle; padding: .2em; font-family: "gicon", sans-serif; color: rgb(var(--gMainColor)); text-shadow: 1px 1px 1px var(--gBorderColor); border: none; cursor: pointer; position: relative; z-index: 200; } button.main, .icn-btn.main { color: rgb(var(--gTextColor)); font-size: 1.2em; border-radius: 1em; padding: .5em 1em; } button.main[data-icon]:before, .icn-btn.main:before { display: none; |
︙ | ︙ | |||
260 261 262 263 264 265 266 | .submit .minor { font-size: .9em; } input[type=submit]:hover, input[type=button]:hover, button:hover, a.icn-btn:hover, input[type=file]:hover, .radio-btn:hover div, a.num:hover, .num a:hover { background-color: rgba(var(--gSecondColor), 0.2); | | | | | | | | 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 | .submit .minor { font-size: .9em; } input[type=submit]:hover, input[type=button]:hover, button:hover, a.icn-btn:hover, input[type=file]:hover, .radio-btn:hover div, a.num:hover, .num a:hover { background-color: rgba(var(--gSecondColor), 0.2); color: rgb(var(--gHoverLinkColor)) !important; border-color: rgb(var(--gSecondColor)); } input[type=submit]:active, input[type=button]:active, button:active, input[type=file]:active { box-shadow: 0 0 10px .1rem rgb(var(--gSecondColor)); } input[type=color] { cursor: pointer; } input.resetButton { margin-left: 1em; } input[readonly], input.disabled, input[disabled], textarea[disabled], select[disabled] { cursor: not-allowed; color: var(--gBorderColor); background-color: var(--gLightBackgroundColor); border-color: var(--gLightBorderColor); } input[disabled]:hover, input[readonly]:hover { background-color: unset; color: unset; border-color: unset; } input[disabled] + label { color: var(--gBorderColor); } input[disabled] + label::before { color: var(--gBorderColor); cursor: not-allowed; } select, input[size], input[type=color], button, input[type=button], input[type=submit], input[type=number] { min-width: 0; } |
︙ | ︙ | |||
334 335 336 337 338 339 340 | display: table-cell; border: 1px solid rgba(var(--gSecondColor), 0.5); background-color: rgba(var(--gSecondColor), 0.1); font-size: inherit; border-radius: .2em; padding: .2em .4em; transition: color .3s, background-color .3s; | | | | | 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 | display: table-cell; border: 1px solid rgba(var(--gSecondColor), 0.5); background-color: rgba(var(--gSecondColor), 0.1); font-size: inherit; border-radius: .2em; padding: .2em .4em; transition: color .3s, background-color .3s; color: rgb(var(--gTextColor)); } form .radio-btn h3 { text-decoration: underline; } form .radio-btn input { margin: 1em; } form .radio-btn .help { margin: 0; font-size: .9em; } form .radio-btn input:checked + label div { background-color: rgba(var(--gSecondColor), 0.3); } /* Custom list input */ |
︙ | ︙ | |||
389 390 391 392 393 394 395 | input.money { text-align: right; } input.money + b { padding: .2rem .6rem; line-height: 1.5rem; | | > > > > > > > > | 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 | input.money { text-align: right; } input.money + b { padding: .2rem .6rem; line-height: 1.5rem; color: var(--gBorderColor); } p.submit { margin: 1em; } form .checkUncheck { float: left; } form span.password_check { margin-left: 1em; padding: .1em .3em; border-radius: .5em; color: #000; } form span.password_check.fail { background-color: #f99; } form span.password_check.weak { background-color: #ff9; } form span.password_check.medium { background-color: #ccf; } form span.password_check.ok { background-color: #cfc; } dd.help input[type=text] { cursor: pointer; font-family: monospace; } dd.help.example { margin-left: 2.5em; font-size: .9em; } form p.actions { float: right; } /** Datepicker widget */ .datepicker-parent { position: relative; } dialog { display: none; background: rgb(var(--gBgColor)); color: rgb(var(--gTextColor)); } dialog[open] { display: block; } dialog.datepicker { |
︙ | ︙ | |||
449 450 451 452 453 454 455 456 457 458 | border-radius: .5rem; z-index: 1000; } .datepicker nav { display: flex; justify-content: space-between; text-align: center; } | > > > > > > > > > > > > | | > | > > > | > | | | | | < > | > | | > > | | < | > | | | > > > > > | > | 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 | border-radius: .5rem; z-index: 1000; } .datepicker nav { display: flex; justify-content: space-between; align-items: center; text-align: center; } .datepicker nav input[type=button] { font-family: gicon; height: 2em; width: 2em; } .datepicker span { white-space: nowrap; } .datepicker span * { font-size: 1rem; font-family: inherit; margin: .1rem; padding: .1rem; border: 1px solid var(--gLightBorderColor); text-align: center; } .datepicker table { border-collapse: collapse; width: 100%; } .datepicker thead td { font-size: 80%; color: var(--gBorderColor); text-align: center; } .datepicker tbody tr:nth-child(even) { background-color: var(--gLightBackgroundColor); } .datepicker tbody td:nth-child(6) input { color: var(--gBorderColor); } .datepicker tbody td:nth-child(7) input { color: var(--gBorderColor); } .datepicker tbody td { text-align: center; width: 14.3%; padding: 0; } .datepicker tbody td input { padding: .4rem .7rem; border: none; background: none; box-shadow: none; border-radius: .2rem; margin: 0; } .datepicker tbody td.focus input { background: #339; color: #fff; } .datepicker tbody input:hover { background: #ccf; color: darkred; } fieldset.mailing { max-width: 40em; } fieldset.mailing dd.preview > * { border-radius: .5em; background: var(--gLightBackgroundColor); padding: 1em; } #queryBuilderForm textarea, fieldset.mailing textarea, fieldset.mailing #f_subject, fieldset.mailing #f_target { width: calc(100% - 2em); } #queryBuilder .column select, #queryBuilderForm .actions select { max-width: 15em; } |
︙ | ︙ |
Modified src/www/admin/static/styles/04-dialogs.css from [0eac2544ce] to [e8d4e068e3].
1 2 3 4 5 6 7 8 9 10 11 | body.transparent { background: none; } html.dialog body { background: transparent; overflow: auto; } html.dialog { height: auto; | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | body.transparent { background: none; } html.dialog body { background: transparent; overflow: auto; } html.dialog { height: auto; background: rgb(var(--gBgColor)); } html.dialog main { background: rgb(var(--gBgColor)); padding: .5em; margin: 0; } /** Dialogs pop-ins */ #dialog { width: 100%; |
︙ | ︙ | |||
70 71 72 73 74 75 76 | opacity: 1; } #dialog > button.closeBtn { background: unset; border: unset; box-shadow: unset; | | | < | 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 | opacity: 1; } #dialog > button.closeBtn { background: unset; border: unset; box-shadow: unset; color: #999; font-size: 1.3em; display: block; width: 90%; } #dialog > button.closeBtn:hover { color: #fff !important; } .loader { width: 100%; min-height: 32px; display: block; position: relative; } .loader.install { margin-top: -40px; } .loader b { text-shadow: 2px 2px 5px #999; background: rgba(255, 255, 255, 0.5); border-radius: .5em; font-size: 16px; line-height: 16px; height: 16px; z-index: 9999; position: absolute; |
︙ | ︙ |
Modified src/www/admin/static/styles/05-navigation.css from [2228a1d3bc] to [f5cdf102c5].
︙ | ︙ | |||
21 22 23 24 25 26 27 | nav.tabs .sub .title { margin: 0 1em 0 -1em; font-weight: bold; padding: .1em .5em; } nav.tabs li { | | | | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | nav.tabs .sub .title { margin: 0 1em 0 -1em; font-weight: bold; padding: .1em .5em; } nav.tabs li { margin: 0 0.2em; } nav.tabs li a { display: inline-block; background: rgba(var(--gSecondColor), .5); border-radius: .5em .5em 0 0; padding: .1em .5em; color: rgb(var(--gTextColor)); text-decoration: none; transition: background-color .2s, color .2s; } nav.tabs .current a { background: rgb(var(--gMainColor)); color: rgb(var(--gBgColor)); |
︙ | ︙ | |||
52 53 54 55 56 57 58 | nav.tabs aside { float: right; max-width: 50%; text-align: right; clear: right; } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | nav.tabs aside { float: right; max-width: 50%; text-align: right; clear: right; } main nav.menu > b { font-weight: normal; } main nav.menu { display: inline-block; position: relative; } main nav.menu span { display: none; } main nav.menu:hover span, main nav.menu:active span { display: flex; position: absolute; flex-direction: column; text-align: left; background: rgb(var(--gBgColor)); border-radius: .3em; padding: .3em; z-index: 1000; box-shadow: 2px 2px 5px var(--gLightBorderColor); } main nav.menu button { text-align: left; } |
Modified src/www/admin/static/styles/06-tables.css from [30c2ec2452] to [3a60ef01c4].
1 2 3 4 5 6 7 8 9 10 11 12 | table.list { margin-bottom: 1em; width: 100%; } table.list caption { text-align: center; font-size: 1.2em; } table.list tbody td.desc { font-size: .9em; | > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | table.list { margin-bottom: 1em; width: 100%; } table.list.auto { width: auto; } table.list caption { text-align: center; font-size: 1.2em; } table.list tbody td.desc { font-size: .9em; color: var(--gBorderColor); } table.list.auto { width: auto; } table.list table { |
︙ | ︙ | |||
43 44 45 46 47 48 49 | transition: background .2s } table.list tr:nth-child(even), table.multi tbody:nth-child(even) { background: rgba(var(--gSecondColor), 0.2); } | < < < < | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | transition: background .2s } table.list tr:nth-child(even), table.multi tbody:nth-child(even) { background: rgba(var(--gSecondColor), 0.2); } table.multi tr { background: inherit !important; } table.list tr.checked { color: #633; background: #ffc; |
︙ | ︙ | |||
79 80 81 82 83 84 85 | } table.list .check { width: 1%; } table.search th { | < < > > > > | | < | | 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 | } table.list .check { width: 1%; } table.search th { background: rgba(var(--gSecondColor), 0.5); } table.list tr.disabled, table.list td.disabled, table.list th.disabled { color: var(--gBorderColor); } table.list tr:nth-child(even).disabled { background: var(--gLightBackgroundColor); } .userOrder .cur { background: rgba(var(--gSecondColor), 1.0); color: rgb(var(--gBgColor)); } .userOrder a { text-decoration: none; display: block; color: inherit; padding: .2em; border-radius: .5em; } .userOrder a:hover { background: rgba(var(--gBgColor), 0.5); } table.list .userOrder td, table.list .userOrder th { padding: .2em; } table.list .userOrder .check { |
︙ | ︙ | |||
126 127 128 129 130 131 132 | width: 1em; text-align: center; vertical-align: middle; font-weight: normal; } thead .cur .icn { | | | | | 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 | width: 1em; text-align: center; vertical-align: middle; font-weight: normal; } thead .cur .icn { color: rgb(var(--gBgColor)); } table.list .actions { text-align: right; } table.list .separator { border-left: 2px dashed #999; } table.list .icon { width: 1.5em; color: rgba(var(--gTextColor), 0.3); } table.list .folder .icon { color: rgba(var(--gTextColor), 0.5); } |
Modified src/www/admin/static/styles/10-accounting.css from [1b7cf590bb] to [6403de6e55].
︙ | ︙ | |||
22 23 24 25 26 27 28 | } .transaction-lines input[type=text] { min-width: 0 !important; } nav.acc-year { | > | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | } .transaction-lines input[type=text] { min-width: 0 !important; } nav.acc-year { color: rgb(var(--gTextColor)); background: rgb(var(--gBgColor)); text-align: center; border-radius: .5rem; border: .2rem solid rgba(var(--gMainColor), 0.5); display: flex; align-items: center; margin-bottom: .5rem; } |
︙ | ︙ | |||
69 70 71 72 73 74 75 | color: rgb(var(--gMainColor)); } .year-header { text-align: center; margin-bottom: .8em; padding-bottom: .5em; | | | < < < > > > > | 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 | color: rgb(var(--gMainColor)); } .year-header { text-align: center; margin-bottom: .8em; padding-bottom: .5em; border-bottom: 1pt solid var(--gBorderColor); } .year-header .print-btn button { font-size: 1.3rem; } .year-header form { max-width: 30em; margin: 1em auto; } .year-infos .graphs { text-align: center; display: flex; flex-wrap: wrap; justify-content: center; } .year-infos .graphs figure { margin: 1rem; } .year-infos .graphs.small figure img { max-width: 500px; } table.accounts { width: 100%; } table.accounts .actions { text-align: right; } table.accounts tbody tr td:first-child { font-family: monospace; } table.accounts th { font-weight: normal; } table.accounts .account-level-1 th { font-size: 1.6em; } table.accounts .account-level-2 th { padding-left: 1em; font-size: 1.3em; } |
︙ | ︙ |
Modified src/www/admin/static/styles/config.css from [33fa4a39f3] to [cd8a35a686].
1 2 3 4 5 6 7 | .error .trace { border: 1px solid #ccc; margin: 1em; } .error .trace h4 { background: #ccc; | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | .error { background: #fff; color: #000; } .error .trace { border: 1px solid #ccc; margin: 1em; } .error .trace h4 { background: #ccc; |
︙ | ︙ |
Modified src/www/admin/static/styles/wiki.css from [5b5f5a2486] to [8d582848fc].
︙ | ︙ | |||
20 21 22 23 24 25 26 | .web-edit { display: block; } } #encryptPasswordDisplay { cursor: help; | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | .web-edit { display: block; } } #encryptPasswordDisplay { cursor: help; background: var(--gLightBackgroundColor); } fieldset.wikiEncrypt, fieldset.wikiMain, .web-edit p.submit { grid-column: 2; } .web-edit dd.help { |
︙ | ︙ | |||
47 48 49 50 51 52 53 | .wikiFiles { text-align: center; } .wikiFooter, .wikiFiles { font-size: 0.8em; | | | < < < < < < < < < < < < < < < < < < < < < < | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | .wikiFiles { text-align: center; } .wikiFooter, .wikiFiles { font-size: 0.8em; color: var(--gBorderColor); border-top: 0.1em solid var(--gLightBorderColor); clear: both; } form#f_upload fieldset { position: relative; } .diff { margin: 1em; } |
︙ | ︙ |
Modified src/www/admin/web/_syntax_markdown.html from [cb696910b0] to [602dc80a26].
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 | h5 { font-size: 0.9em; } h6 { font-size: 0.8em; } article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; } body { font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif; padding: .8em; } pre, samp { display: block; background: #ccc; color: #000; border-radius: .5em; padding: .4em; | > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | h5 { font-size: 0.9em; } h6 { font-size: 0.8em; } article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; } body { font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif; padding: .8em; background: #eee; } pre, samp { display: block; background: #ccc; color: #000; border-radius: .5em; padding: .4em; |
︙ | ︙ |
Modified src/www/admin/web/_syntax_skriv.html from [a383e52cdf] to [8451d4b45e].
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 | h5 { font-size: 0.9em; } h6 { font-size: 0.8em; } article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; } body { font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif; padding: .8em; } pre, samp { display: block; background: #ccc; color: #000; border-radius: .5em; padding: .4em; | > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | h5 { font-size: 0.9em; } h6 { font-size: 0.8em; } article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; } body { font-family: "Trebuchet MS", Arial, Helvetica, Sans-serif; padding: .8em; background: #eee; } pre, samp { display: block; background: #ccc; color: #000; border-radius: .5em; padding: .4em; |
︙ | ︙ |
Modified src/www/admin/web/config.php from [3dd1c46ed9] to [2f908bf8b1].
︙ | ︙ | |||
27 28 29 30 31 32 33 | return; } Skeleton::resetSelected(f('select')); }, 'squelettes', Utils::getSelfURI('reset_ok')); if (qg('edit')) { | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | return; } Skeleton::resetSelected(f('select')); }, 'squelettes', Utils::getSelfURI('reset_ok')); if (qg('edit')) { $source = qg('edit'); $csrf_key = 'edit_skel_' . md5($source); $form->runIf('save', function () use ($source) { $tpl = new Skeleton($source); $tpl->edit(f('content')); $fullscreen = null !== qg('fullscreen') ? '#fullscreen' : ''; Utils::redirect(Utils::getSelfURI(sprintf('edit=%s&ok%s', rawurlencode($source), $fullscreen))); |
︙ | ︙ |
Modified src/www/admin/web/css.php from [31cfe203b8] to [6239ccc079].
1 2 3 4 5 6 7 8 9 10 11 | <?php /** * This file is an alias to /content.css basically, * but it is required for when WWW_URL is on a different domain than ADMIN_URL */ namespace Garradin; use Garradin\Web\Skeleton; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php /** * This file is an alias to /content.css basically, * but it is required for when WWW_URL is on a different domain than ADMIN_URL */ namespace Garradin; use Garradin\Web\Skeleton; require_once __DIR__ . '/../_inc.php'; $s = new Skeleton('content.css'); $s->serve(); |
Modified src/www/admin/web/edit.php from [4b21a56838] to [e27e68cd64].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Entities\Web\Page; use Garradin\Entities\Files\File; use KD2\SimpleDiff; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_WEB, $session::ACCESS_WRITE); | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Web\Render\Render; use Garradin\Entities\Web\Page; use Garradin\Entities\Files\File; use KD2\SimpleDiff; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_WEB, $session::ACCESS_WRITE); |
︙ | ︙ | |||
46 47 48 49 50 51 52 | die(json_encode(['success' => true, 'modified' => $page->modified->getTimestamp()])); } Utils::redirect('!web/page.php?p=' . $page->path); }, $csrf_key); $parent = $page->parent ? [$page->parent => Web::get($page->parent)->title] : ['' => 'Racine du site']; | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | die(json_encode(['success' => true, 'modified' => $page->modified->getTimestamp()])); } Utils::redirect('!web/page.php?p=' . $page->path); }, $csrf_key); $parent = $page->parent ? [$page->parent => Web::get($page->parent)->title] : ['' => 'Racine du site']; $encrypted = f('encrypted') || $page->format == Render::FORMAT_ENCRYPTED; $old_content = f('content'); $new_content = $page->content; $formats = $page::FORMATS_LIST; $tpl->assign(compact('page', 'parent', 'editing_started', 'encrypted', 'csrf_key', 'old_content', 'new_content', 'show_diff', 'formats')); $tpl->assign('custom_js', ['wiki_editor.js', 'wiki-encryption.js']); $tpl->assign('custom_css', ['wiki.css']); $tpl->display('web/edit.tpl'); |
Modified src/www/admin/web/page.php from [fa91cc041f] to [f1db880f10].
︙ | ︙ | |||
13 14 15 16 17 18 19 | $page = Web::get(qg('p')); if (!$page) { throw new UserException('Page inconnue'); } | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | $page = Web::get(qg('p')); if (!$page) { throw new UserException('Page inconnue'); } if (qg('toggle_type') !== null && $session->canAccess($session::SECTION_WEB, $session::ACCESS_ADMIN)) { $page->toggleType(); $page->save(); Utils::redirect('!web/page.php?p=' . $page->path); } $membres = new Membres; |
︙ | ︙ |
Modified src/www/admin/web/search.php from [7ffb84ef47] to [1a70f9e5ce].
1 2 3 4 5 6 7 8 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Entities\Web\Page; require_once __DIR__ . '/_inc.php'; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Web\Web; use Garradin\Entities\Web\Page; require_once __DIR__ . '/_inc.php'; $q = trim((string) f('q')); $tpl->assign('query', $q); if ($q) { $r = Web::search($q); $tpl->assign('results', $r); $tpl->assign('results_count', count($r)); |
︙ | ︙ |
Modified src/www/index.php from [058717db8f] to [0b19b27e48].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\Web\Web; require __DIR__ . '/_inc.php'; Web::dispatchURI(); | > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 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\Users\Emails; use Garradin\Web\Web; require __DIR__ . '/_inc.php'; // Handle __un__subscribe URL if (!empty($_GET['un'])) { $params = array_intersect_key($_GET, ['un' => null, 'v' => null]); // RFC 8058 if (!empty($_POST['Unsubscribe']) && $_POST['Unsubscribe'] == 'Yes') { $email = Emails::getEmailFromOptout($params['un']); if (!$email) { throw new UserException('Adresse email introuvable.'); } $email->setOptout(); $email->save(); http_response_code(200); echo 'Unsubscribe successful'; exit; } Utils::redirect('!optout.php?' . http_build_query($params)); } Web::dispatchURI(); |
Modified src/www/plugin.php from [3b761f779d] to [a16d61011d].
︙ | ︙ | |||
8 9 10 11 12 13 14 | $plugin = new Plugin(!empty($_GET['_p']) ? $_GET['_p'] : null); define('Garradin\PLUGIN_ROOT', $plugin->path()); define('Garradin\PLUGIN_URL', WWW_URL . 'p/' . $plugin->id() . '/'); define('Garradin\PLUGIN_QSP', '?'); | > | > > > > > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | $plugin = new Plugin(!empty($_GET['_p']) ? $_GET['_p'] : null); define('Garradin\PLUGIN_ROOT', $plugin->path()); define('Garradin\PLUGIN_URL', WWW_URL . 'p/' . $plugin->id() . '/'); define('Garradin\PLUGIN_QSP', '?'); try { $plugin->call('public/' . $page); } catch (\UnexpectedValueException $e) { http_response_code(404); throw new UserException($e->getMessage()); } |
Added src/www/skel-dist/email.html version [a0349f5d77].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | {{* Ce squelette est utilisé pour l'envoi d'un e-mail au format Skriv ou Markdown *}} <!DOCTYPE html> <html> <head> <style type="text/css"> * { margin: 0; padding: 0; } body { background: #eee; color: #000; } a { color: #009; text-decoration: underline; } .web-content, .footer { padding: 10px; background: #fff; max-width: 750px; margin: 10px auto; border-radius: 5px; } .footer { background: #ddd; } .footer h3, .footer h4, .footer h5 { text-align: center; margin: 5px 0; } {{* Inclure le contenu de content.css pour s'assurer que le style du texte Markdown/Skriv est correct *}} {{:include file="content.css"}} </style> </head> <body> {{* Le contenu du mail est dans la variable $html, ne pas supprimer sinon le message sera vide ! *}} {{$html|raw}} {{* D'autres variables sont disponibles, permettant de personnaliser le message : - $recipient contient l'adresse email du destinataire - $data contient les données disponibles pour le message (par exemple $data.nom contiendra le nom du membre dans un message collectif) - $context contient le contexte du message (0 = changement de mot de passe, 1 = message privé entre membres, 2 = message collectif) - $from contient l'expéditeur (si NULL, l'expéditeur sera l'association) *}} <div class="footer"> <h3><a href="{{$config.site_asso}}">{{$config.nom_asso}}</a></h3> {{if $config.adresse_asso}} <h4>{{$config.adresse_asso}}</h4> {{/if}} {{if $config.telephone_asso}} <h5><a href="tel:{{$config.telephone_asso}}">{{$config.telephone_asso|raw}}</a></h5> {{/if}} </div> {{* Le lien de désinscription sera ajouté automatiquement en bas du message, il n'est pas possible de le modifier ou le supprimer. *}} </body> </html> |
Added tools/factory/config.local.php version [ea59f9592b].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 | <?php namespace Garradin; /** * Ce fichier permet de configurer Garradin pour une utilisation * avec plusieurs associations, mais une seule copie du code source. * (aussi appelé installation multi-sites, ferme ou usine) * * Voir la doc : https://fossil.kd2.org/garradin/wiki?name=Multi-sites * * N'oubliez pas d'installer également le script cron.sh fournit * pour lancer les rappels automatiques et sauvegardes. * * Si cela ne suffit pas à vos besoins, contactez-nous : https://garradin.eu/contact * pour une aide spécifique à votre installation. */ // Nom de domaine parent des associations hébergées // Exemple : si vos associations sont hébergées en clubdetennis.garradin.eu, // indiquer ici 'garradin.eu' const FACTORY_DOMAIN = 'monsite.tld'; // Répertoire où seront stockées les données des utilisateurs // Dans ce répertoire, un sous-répertoire sera créé pour chaque compte // Ainsi 'clubdetennis.garradin.eu' sera dans le répertoire courant (__DIR__), // sous-répertoire 'users' et dans celui-ci, sous-répertoire 'clubdetennis' // // Pour chaque utilisateur il faudra créer le sous-répertoire en premier lieu // (eg. mkdir .../users/clubdetennis) const FACTORY_USER_DIRECTORY = __DIR__ . '/users'; // Envoyer les erreurs PHP par mail à l'adresse de l'administrateur système // (mettre à null pour ne pas recevoir d'erreurs) const MAIL_ERRORS = 'administrateur@monsite.tld'; // IMPORTANT ! // Modifier pour indiquer une valeur aléatoire de plus de 30 caractères const SECRET_KEY = 'Indiquer ici une valeur aléatoire !'; // Quota de stockage de documents (en octets) // Définit la taille de stockage disponible pour chaque association pour ses documents const FILE_STORAGE_QUOTA = 1 * 1024 * 1024 * 1024; // 1 Go //////////////////////////////////////////////////////////////// // Réglages conseillés, normalement il n'y a rien à modifier ici // Indiquer que l'on va utiliser cron pour lancer les tâches à exécuter (envoi de rappels de cotisation) const USE_CRON = true; // Cache partagé const SHARED_CACHE_ROOT = __DIR__ . '/cache'; // Désactiver le log des erreurs PHP visible dans l'interface (sécurité) const ENABLE_TECH_DETAILS = false; // Désactiver les mises à jour depuis l'interface web // Pour être sûr que seul l'admin sys puisse faire des mises à jour const ENABLE_UPGRADES = false; // Ne pas afficher les erreurs de code PHP const SHOW_ERRORS = false; //////////////////////////////////////////////////////////////// // Code 'magique' qui va configurer Garradin selon les réglages $login = null; // Un sous-domaine ne peut pas faire plus de 63 caractères $login_regexp = '([a-z0-9_-]{1,63})'; $domain_regexp = sprintf('/^%s\.%s$/', $login_regexp, preg_quote(FACTORY_DOMAIN, '/')); if (isset($_SERVER['SERVER_NAME']) && preg_match($regexp, $_SERVER['SERVER_NAME'], $match)) { $login = $match[1]; } elseif (PHP_SAPI == 'cli' && !empty($_SERVER['GARRADIN_FACTORY_USER']) && preg_match('/^' . $login_regexp . '$/', $_SERVER['GARRADIN_FACTORY_USER'])) { $login = $_SERVER['GARRADIN_FACTORY_USER']; } else { // Login invalide ou non fourni http_response_code(404); die('<h1>Page non trouvée</h1>'); } $user_data_dir = rtrim(FACTORY_USER_DIRECTORY, '/') . '/' . $login; if (!is_dir($user_data_dir)) { http_response_code(404); die("<h1>Cette association n'existe pas.</h1>"); } // Définir le dossier où sont stockés les données define('Garradin\DATA_ROOT', $user_data_dir); // Définir l'URL define('Garradin\WWW_URL', 'https://' . $login . FACTORY_USER_DIRECTORY . '/'); define('Garradin\WWW_URI', '/'); |
Added tools/factory/factory_cron.sh version [b42ba034be].
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/bin/sh # Répertoire où sont stockées les données des utilisateurs # veiller à ce que ce soit le même que dans config.local.php FACTORY_USER_DIRECTORY="users" # Chemin vers le script cron.php de Garradin GARRADIN_CRON_SCRIPT="scripts/cron.php" for user in $(cd ${FACTORY_USER_DIRECTORY} && ls -1d */) do GARRADIN_FACTORY_USER=$(basename "$user") php $GARRADIN_CRON_SCRIPT echo $GARRADIN_FACTORY_USER done |
Added tools/factory/factory_cron_emails.sh version [a95d76d346].
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/bin/sh # Répertoire où sont stockées les données des utilisateurs # veiller à ce que ce soit le même que dans config.local.php FACTORY_USER_DIRECTORY="users" # Chemin vers le script emails.php de Garradin GARRADIN_CRON_SCRIPT="scripts/emails.php" for user in $(cd ${FACTORY_USER_DIRECTORY} && ls -1d */) do GARRADIN_FACTORY_USER=$(basename "$user") php $GARRADIN_CRON_SCRIPT echo $GARRADIN_FACTORY_USER done |
Added tools/factory/factory_upgrade.sh version [1fb3422189].
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/bin/sh # Répertoire où sont stockées les données des utilisateurs # veiller à ce que ce soit le même que dans config.local.php FACTORY_USER_DIRECTORY="users" # Chemin vers le script upgrade.php de Garradin GARRADIN_UPGRADE_SCRIPT="scripts/cron.php" for user in $(cd ${FACTORY_USER_DIRECTORY} && ls -1d */) do GARRADIN_FACTORY_USER=$(basename "$user") php $GARRADIN_UPGRADE_SCRIPT done |