diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..e4e5dae --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,87 @@ +0.12 +- Ajout Mollie à la table moyens de paiement +0.11 +- Changement mention finale pour devis +0.9 +- Ajout possibilité choisir champs identité et adresse membre +0.8.8 +- correction typo +0.8.7 +- correction typo +0.8.6 +- Correction erreur si pas de prix saisi +0.8.5 +- Ajout numéro SIREN/SIRET pour les clients +- Ajout pagination liste factures + +0.8.4 +- Correction bug si identité définie par plusieurs champs +- Correction bug oubli choix receveur +- Mise-à-jour Changelog + +0.8.3 : +- Mise-à-jour README + +0.8.2 : +- Tri des membres en ordre alphabétique +- Élimination de plusieurs bugs + +0.8.1 : +- reprise du code par jce (lesanges@zaclys.net ; https://git.roflcopter.fr/lesanges/paheko-plugin-facturation) +- Achèvement migration vers Paheko 1.3 + +0.7.4 : +- Corrections de dépréciations PHP, le plugin être compatible PHP8.2 +- Quelques problèmes réglés de formulaires invalidables +- Nettoyage du code et factorization des facture_{ajouter,modifier}.php + +0.7.3 : +- Règle le problème de formulaire bloqué que je croyais avoir résolu la dernière fois :) + +0.7.2 : +- Petite correction du moyen de paiement qui n'était pas retenu à cause d'un doublon dans le formulaire. + +0.7.1 : +- Ajout d'une case dans la config pour choisir si les factures sont TTC ou HT +- Le formulaire de document était cassé à cause d'un champs obligatoire invisible + +0.7.0 : +- Modernisation des formulaires, de la fiche client +- Liste dynamiques des documents et des clients + export CSV/ODS +- Numérotation automatique des documents +- Ajout du bouton Dupliquer sur les documents +- Divers fixes +- Changements de la gestion des CERFA : + - organisation du champ `contenu` pour gérer les paramètres de CERFA sous la forme {"forme":"3","nature":"1","texte":"1"} + - ajout d'une table `plugin_facturation_txt_cerfa` pour stocker les textes explicatifs additionnels + - formulaires adaptés aux CERFA pour permettre le choix de la forme, de la nature, du mode de versement et du texte explicatif éventuel + +0.6.0 : +- Génération des PDF par Garradin, on se débarasse de mPDF et l'installation du plugin devient standard +- Fix #32 (prix total affiché incorrect), en stockant les prix sous formes d'entiers à la manière de Garradin +- Possibilité de supprimer un document +- Fix #34, problème sélection membre + +0.5.0 : +Mise à jour rapide du plugin pour Garradin 1.1. +La gestion de la signature dans les reçus fiscaux a été désactivés pour l'instant (changements de méthodes avec Garradin 1.1, flemme de m'y pencher pour le moment) + +0.4.1 : +Release mineure qui corrige un problème qui apparaît, selon la configuration de PHP (?), lors de la migration depuis une version précédente. + +0.4.0 : +Cette version a pour principal but d'être compatible avec Garradin 1.0 et de ne pas empêcher la mise à jour d'une ancienne installation comprenant une ancienne version du plugin. +Le formulaire de création/édition de document a été modifié pour simplifier l'expérience utilisateur : tout les types de documents se font depuis le même formulaire, suivi des évolutions graphiques/ergonomiques de Garradin. + +0.3.0 : +- Ajouts des reçus fiscaux et des reçus sur des cotisations : +- Génération du cerfa pour reçu fiscaux, paramètres disponibles dans la config +- Ajout d'une page d'aide +- L'identité des destinataires membres est maintenant définie selon la configuration de Garradin, et non plus de manière statique sur la colonne "nom" +- Suppression de la classe Facturation\Config pour utiliser celle que propose Garradin +- Ajout du champs de numéro RNA + + +0.2.0 : +- Stockage des désignations des documents changé de serialize à json_decode +- Petites améliorations sur les formulaires diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index cbd5b07..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,160 +0,0 @@ -# Changelog - -## v0.27.2 - 2026-07-02 -### Changed -- Conserver les tags html dans les libellés - -## v0.25 - 2026-05-15 - Jason Pelletier -### Added -- Options d'affichage du rendu facture - -## v0.19 - 2026-05-15 - Jason Pelletier -### Fixed -- Réparation des schémas partiellement migrés - -## v0.18 - 2026-05-15 - Jason Pelletier -### Changed -- améliorations proposées par Jason Pelletier -### Added -- Personnalisation du rendu des factures - -## v0.17 - 2026-02-26 - @jce -### Added -- archivage factures - -## v0.15 - 2025-03-25 - @jce -### Added -- champ contact à la fiche client - -## v0.13 - 2025-02-26 - @jce -### Changed -- Amélioration contrôles saisie facture - -## v0.12 - 2025-01-04 - @jce -### Added -- Mollie à la table moyens de paiement - -## v0.11 - 2024-10-19 - @jce -### Changed -- mention finale pour devis - -## v0.9 - 2024-08-30 - @jce -### Added -- possibilité choisir champs identité et adresse membre - -## v0.8.8 - 2024-08-13 - @jce -### Added -- impression logo avec option de configuration -### Changed -- Adaptation Readme à la situation actuelle - -## v0.8.6 - 2024-03-29 - @jce -### Fixed -- Erreur si pas de prix saisi - -## v0.8.5 - 2024-01-12 - @jce -### Added -- numéro SIREN/SIRET pour les clients -- pagination liste factures - -## v0.8.4 - 2024-01-11 - @jce -### Fixed -- Bug si identité définie par plusieurs champs -- Bug oubli choix receveur - -## v0.8.2 - 2024-01-10 - @jce -### Added -- Tri des membres en ordre alphabétique -### Fixed -- Élimination de plusieurs bugs - -## v0.8.1 - 2023-11-03 - @jce -### Changed -- reprise du code par jce (lesanges@zaclys.net ; https://gitea.zaclys.com/lesanges/facturation) -- Achèvement migration vers Paheko 1.3 - -## v0.7.4 - 2023-04-16 - @noizette -### Fixed -- Corrections de dépréciations PHP, le plugin être compatible PHP8.2 -### Changed -- Quelques problèmes réglés de formulaires invalidables -- Nettoyage du code et factorization des facture_{ajouter,modifier}.php - -## v0.7.3 - 2022-03-03 - @noizette -### Fixed -- Règle le problème de formulaire bloqué que je croyais avoir résolu la dernière fois :) - -## v0.7.2 - 2022-01-31 - @noizette -### Fixed -- Petite correction du moyen de paiement qui n'était pas retenu à cause d'un doublon dans le formulaire. - -## v0.7.1 - 2022-01-10 - @noizette -### Added -- ajout d'une case dans la config pour choisir si les factures sont TTC ou HT -### Fixed -- Le formulaire de document était cassé à cause d'un champs obligatoire invisible - -## v0.7.0 - @noizette -### Changed -- Modernisation des formulaires, de la fiche client -- Liste dynamiques des documents et des clients + export CSV/ODS -- Numérotation automatique des documents -- Divers fixes -- Changements de la gestion des CERFA : - - organisation du champ `contenu` pour gérer les paramètres de CERFA sous la forme {"forme":"3","nature":"1","texte":"1"} - - ajout d'une table `plugin_facturation_txt_cerfa` pour stocker les textes explicatifs additionnels - - formulaires adaptés aux CERFA pour permettre le choix de la forme, de la nature, du mode de versement et du texte explicatif éventuel -### Added -- Ajout du bouton Dupliquer sur les documents - -## v0.6.0 - @noizette -### Changed -- Génération des PDF par Garradin, on se débarasse de mPDF et l'installation du plugin devient standard -- Possibilité de supprimer un document -### Fixed -- Fix #32 (prix total affiché incorrect), en stockant les prix sous formes d'entiers à la manière de Garradin -- Fix #34, problème sélection membre - -## v0.5.0 - @noizette -### Added -Mise à jour rapide du plugin pour Garradin 1.1. -### Changed -- La gestion de la signature dans les reçus fiscaux a été désactivés - pour l'instant (changements de méthodes avec Garradin 1.1, flemme de - m'y pencher pour le moment) - -## v0.4.1 - @noizette -### Fixed -- Release mineure qui corrige un problème qui apparaît, selon la - configuration de PHP (?), lors de la migration depuis une version - précédente. - -## v0.4.0 - @noizette -### Changed -- Cette version a pour principal but d'être compatible avec Garradin - 1.0 et de ne pas empêcher la mise à jour d'une ancienne installation - comprenant une ancienne version du plugin. -- Le formulaire de création/édition de document a été modifié pour - simplifier l'expérience utilisateur : tout les types de documents se - font depuis le même formulaire, suivi des évolutions - graphiques/ergonomiques de Garradin. - -## v0.3.0 - @noizette -### Added -- Ajouts des reçus fiscaux et des reçus sur des cotisations : -- Génération du cerfa pour reçu fiscaux, paramètres disponibles dans la config -- Ajout d'une page d'aide -- Ajout du champs de numéro RNA -### Changed -- L'identité des destinataires membres est maintenant définie selon la - configuration de Garradin, et non plus de manière statique sur la - colonne "nom" -### Removed -- Suppression de la classe Facturation\Config pour utiliser celle que - propose Garradin - -## v0.2.0 - @noizette -### Added -- Stockage des désignations des documents changé de serialize à json_decode -### Changed -- Petites améliorations sur les formulaires diff --git a/admin/_facture_common.php b/admin/_facture_common.php index 2be8c6b..9fbb19c 100644 --- a/admin/_facture_common.php +++ b/admin/_facture_common.php @@ -4,8 +4,30 @@ namespace Paheko; require_once __DIR__ . '/_inc.php'; +function toArray($array, $cle, $sep=",") +{ + $result = array(); + foreach ($array as $elem) + { + $ro = new \ReflectionObject($elem); + $proprietes = $ro->getProperties(); + $ligne = ""; + foreach ($proprietes as $p) + { + if ($p->getName() == $cle) { + $key = $p->getValue($elem); + } + else { + $ligne .= $sep . $p->getValue($elem); + } + } + $result[$key] = substr($ligne, strlen($sep)); + } + return $result; +} + if (!isset($target) || !in_array( $target, ['new', 'edit'])) { - throw new \LogicException('Appel interne invalide du formulaire de document.'); + throw new Exception('blabla illegal call'); // Fix: exception type? } else { $target = ($target === 'new' ? true:false); } @@ -13,6 +35,7 @@ if (!isset($target) || !in_array( $target, ['new', 'edit'])) { $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); use Paheko\DB; +use stdClass; use const \Paheko\Plugin\Facturation\PATTERNS_LIST; $db = DB::getInstance(); @@ -46,18 +69,21 @@ if ( !$target ) { throw new UserException("Ce document n'existe pas."); } } -if (!$target && $f->archivee) +if (! $target && $f->archivee) { throw new UserException("Ce document est archivé, vous n'avez pas le droit de le modifier"); } -// Traitement +// Traitement $data=[]; $form->runIf(f('save') && !$form->hasErrors(), - function () use (&$data, $form) + function () use ($client, &$data, $form) { try { - $type = (int) f('type'); + if ( count(f('designation')) !== count(f('prix')) ) + { + throw new UserException('Nombre de désignations et de prix reçus différent.'); + } $data = [ 'numero' => f('numero_facture'), @@ -67,51 +93,37 @@ $form->runIf(f('save') && !$form->hasErrors(), 'archivee' => f('archivee') == 1?1:0, 'moyen_paiement' => f('moyen_paiement'), 'nom_contact' => f('nom_contact'), - 'numero_commande' => f('numero_commande') ?: null, - 'reference_acheteur' => f('reference_acheteur') ?: null, + 'toto' => 0 ]; - - $data['type_facture'] = $type; - - if (in_array($type, [DEVIS, FACT], true)) + $data['type_facture'] = f('type'); + if (in_array(f('type'), [DEVIS, FACT])) { - $designations = (array) f('designation'); - $prix = (array) f('prix'); - - if (count($designations) !== count($prix)) - { - throw new UserException('Nombre de désignations et de prix reçus différent.'); - } - - $total = 0; - - foreach($designations as $k=>$value) + foreach(f('designation') as $k=>$value) { - $prix_ligne = $prix[$k] ?? null; - - if (empty($value) && $prix_ligne !== null && $prix_ligne !== '') { - throw new UserException("Il manque la désignation de la ligne " . ($k + 1) . " !!"); + if (empty($value) && f('prix')[$k] != null) { + throw new UserException("Il manque la désignation de la ligne " . $k+1 . " !!"); } - elseif ($value != '' && ($prix_ligne === null || $prix_ligne === '')) { - throw new UserException('Il manque le prix sur la ligne '. ($k + 1) . ' !!'); - } - elseif (empty($value) && ($prix_ligne === null || $prix_ligne === '')) { + elseif ($value != '' && f('prix')[$k] == null) { + throw new UserException('Il manque le prix sur la ligne '. $k+1 . ' !!'); + } elseif (empty($value) && f('prix')[$k] == null) { continue; } - $prix_ligne = Utils::moneyToInteger($prix_ligne); $data['contenu'][$k]['designation'] = $value; - $data['contenu'][$k]['prix'] = $prix_ligne; - $total += $prix_ligne; + $data['contenu'][$k]['prix'] = Utils::moneyToInteger(f('prix')[$k]); + $data['toto'] += Utils::moneyToInteger(f('prix')[$k]); } - - $data['total'] = $total; - + $data['total'] = $data['toto']; + unset($data['toto']); if (! isset($data['contenu'])) { throw new UserException("Aucune désignation ni aucun prix saisi !!"); } + if (f('type') == FACT) { + $data['numero_commande'] = f('numero_commande'); + $data['reference_acheteur'] = f('reference_acheteur'); + } } - elseif ($type === CERFA) + elseif ( f('type') == CERFA ) { $data['moyen_paiement'] = f('moyen_paiement_cerfa'); $data['contenu'] = [ @@ -119,12 +131,8 @@ $form->runIf(f('save') && !$form->hasErrors(), 'nature' => f('nature_don'), 'texte' => f('texte_don')]; $data['total'] = Utils::moneyToInteger(f('total')); + unset($data['toto']); } - else - { - throw new UserException('Ce type de document doit être créé avec son formulaire dédié.'); - } - if (f('base_receveur') == 'client') { $data['receveur_membre'] = 0; @@ -149,20 +157,18 @@ $form->runIf(f('save') && !$form->hasErrors(), }, $csrf_key); $form->runIf(f('select_cotis') && !$form->hasErrors(), - function () use (&$step) + function () use ($step) { $step = true; }, 'add_cotis_1'); $form->runIf(f('add_cotis') && !$form->hasErrors(), - function () use (&$data, &$radio, $fields, $facture, $form) + function () use ($radio, $fields, $facture, $form) { $radio['type'] = f('cotisation'); try { $num = (int) str_replace('cotis_', '', $radio['type']); - $cotis = []; - foreach($fields as $field) { $cotis[$field] = f($field.'_'.$num); @@ -177,12 +183,8 @@ $form->runIf(f('add_cotis') && !$form->hasErrors(), 'receveur_membre' => 1, 'receveur_id' => f('membre_cotis'), 'date_emission' => f('date_emission'), - 'date_echeance' => f('date_echeance') ?: f('date_emission'), - 'reglee' => 1, - 'archivee' => 0, 'moyen_paiement' => 'AU', 'total' => $r->paid_amount ?? $r->amount, - 'nom_contact' => null, 'contenu' => ['id' => $cotis['id'], 'intitule' => $cotis['label'], 'souscription' => $cotis['date'], @@ -262,7 +264,7 @@ if ($target) } // ... s'il est défini dans le document copié elseif (isset($doc['type'])) { $radio['type'] = $doc['type']; - } // ... ou par défaut + } // ... ou par défaut else { $radio['type'] = FACT; @@ -282,9 +284,9 @@ else $doc['date_emission'] = f('date_emission') ?: $f->date_emission; $doc['date_echeance'] = f('date_echeance')?: $f->date_echeance; // Smarty m'a saoulé pour utiliser form_field|date_fr:--- - $doc['nom_contact'] = $f->nom_contact ?? null; - $doc['numero_commande'] = $f->numero_commande ?? null; - $doc['reference_acheteur'] = $f->reference_acheteur ?? null; + $doc['nom_contact'] = $f->nom_contact; + $doc['numero_commande'] = $f->numero_commande; + $doc['reference_acheteur'] = $f->reference_acheteur; /* modif DD -- CERFA -------------------------------------- */ if ( $f->type_facture == CERFA ) { $doc['total'] = $f->total; @@ -344,11 +346,11 @@ $date->setTimestamp(time()); $tpl->assign('date', $date->format('d/m/Y')); $tpl->assign(compact('liste', 'radio', 'step', 'designations', 'prix', 'from_user', 'identite', 'csrf_key', 'doc')); -$tpl->assign('users', iterator_to_array(Users\Users::iterateAssocByCategory())); +$tpl->assign('users', toArray($db->get('SELECT id, '.$identite.' FROM users WHERE id_category != -2 NOT IN (SELECT id FROM users_categories WHERE hidden = 1) ORDER BY ' .$identite. ';'), 'id', " ")); $tpl->assign('clients', $db->getAssoc('SELECT id, nom FROM plugin_facturation_clients;')); $tpl->assign('contacts', $db->getAssoc('SELECT id, nom_contact FROM plugin_facturation_clients;')); $tpl->assign('require_number', $require_number); -$tpl->assign('number_pattern', PATTERNS_LIST[$plugin->getConfig('pattern')] ?? PATTERNS_LIST['']); +$tpl->assign('number_pattern', PATTERNS_LIST[$plugin->getConfig('pattern')]); if ($target) { $tpl->display(PLUGIN_ROOT . '/templates/facture_ajouter.tpl'); diff --git a/admin/_inc.php b/admin/_inc.php index b0f7f07..f11fc22 100644 --- a/admin/_inc.php +++ b/admin/_inc.php @@ -22,42 +22,6 @@ const PATTERNS_LIST = [ '%06{id}' => 'Numéro unique du document sur 6 chiffres ("000042")', ]; -function ensureSchemaColumns(): void -{ - $db = \Paheko\DB::getInstance(); - - $tables = [ - 'plugin_facturation_clients' => [ - 'siret' => 'siret TEXT', - 'telephone' => 'telephone TEXT', - 'email' => 'email TEXT', - 'nom_contact' => 'nom_contact TEXT', - 'note' => 'note TEXT', - ], - 'plugin_facturation_factures' => [ - 'nom_contact' => 'nom_contact TEXT', - 'numero_commande' => 'numero_commande TEXT', - 'reference_acheteur' => 'reference_acheteur TEXT', - ], - ]; - - foreach ($tables as $table => $columns) { - $schema = $db->getTableSchema($table); - - if (!$schema) { - continue; - } - - foreach ($columns as $column => $definition) { - if (!isset($schema['columns'][$column])) { - $db->exec(sprintf('ALTER TABLE %s ADD COLUMN %s;', $db->quoteIdentifier($table), $definition)); - } - } - } -} - -ensureSchemaColumns(); - $client = new Client; $facture = new Facture; @@ -99,7 +63,7 @@ $tpl->register_function('money_fac', function (array $params) $current_value = htmlspecialchars($current_value, ENT_QUOTES, 'UTF-8'); } - $currency = Config::getInstance()->get('currency'); + $currency = Config::getInstance()->get('monnaie'); return sprintf('%s', $name, $current_value, $currency); } ); diff --git a/admin/client_supprimer.php b/admin/client_supprimer.php index 2e8244f..64a3395 100644 --- a/admin/client_supprimer.php +++ b/admin/client_supprimer.php @@ -12,7 +12,7 @@ $id = (int) qg('id'); $c = $client->get($id); -if (!$c) +if (!$client) { throw new UserException("Ce client n'existe pas."); } diff --git a/admin/config.php b/admin/config.php index e6f57fd..638cd56 100644 --- a/admin/config.php +++ b/admin/config.php @@ -11,17 +11,6 @@ $champsPaheko = DynamicFields::getInstance()->listAssocNames(); $champsPaheko = array('' => '- Choisir un champ -') + $champsPaheko; $form->runIf('save', function () use ($plugin) { - $invoice_theme = f('invoice_theme') ?: 'modern'; - $invoice_accent_color = trim(f('invoice_accent_color') ?: '#4656A5'); - - if (!in_array($invoice_theme, ['modern', 'classic', 'minimal'], true)) { - throw new UserException('Le style de facture sélectionné est invalide.'); - } - - if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $invoice_accent_color)) { - throw new UserException('La couleur d\'accent doit être au format hexadécimal, par exemple #4656A5.'); - } - $plugin->setConfigProperty('rna_asso', trim(f('rna_asso'))); $plugin->setConfigProperty('siret_asso', trim(f('siret_asso'))); $plugin->setConfigProperty('ttc', (bool) f('ttc')); @@ -33,14 +22,6 @@ $form->runIf('save', function () use ($plugin) { $plugin->setConfigProperty('logo', (bool)f('logo')); $plugin->setConfigProperty('footer', f('footer')); - $plugin->setConfigProperty('invoice_theme', $invoice_theme); - $plugin->setConfigProperty('invoice_accent_color', strtoupper($invoice_accent_color)); - $plugin->setConfigProperty('invoice_note', trim(f('invoice_note') ?: '')); - $plugin->setConfigProperty('payment_terms', trim(f('payment_terms') ?: '')); - $plugin->setConfigProperty('invoice_show_status', (bool) f('invoice_show_status')); - $plugin->setConfigProperty('invoice_show_address_borders', (bool) f('invoice_show_address_borders')); - $plugin->setConfigProperty('invoice_show_email', (bool) f('invoice_show_email')); - $plugin->setConfigProperty('invoice_show_web', (bool) f('invoice_show_web')); $plugin->setConfigProperty('adresse_client', f('adresse_client')); $plugin->setConfigProperty('code_postal_client', f('code_postal_client')); @@ -55,21 +36,8 @@ $form->runIf('save', function () use ($plugin) { $tpl->assign('ok', qg('ok') !== null); -$conf = $plugin->getConfig(); - -foreach (['invoice_show_status', 'invoice_show_address_borders', 'invoice_show_email', 'invoice_show_web'] as $key) { - if (!isset($conf->$key)) { - $conf->$key = true; - } -} - -$tpl->assign('conf', $conf); +$tpl->assign('conf', $plugin->getConfig()); $tpl->assign('patterns', \Paheko\Plugin\Facturation\PATTERNS_LIST); $tpl->assign('champsPaheko', $champsPaheko); -$tpl->assign('invoice_themes', [ - 'modern' => 'Moderne', - 'classic' => 'Classique', - 'minimal' => 'Minimal', -]); $tpl->display(PLUGIN_ROOT . '/templates/config.tpl'); diff --git a/admin/facture_supprimer.php b/admin/facture_supprimer.php index 7640466..4711ddb 100644 --- a/admin/facture_supprimer.php +++ b/admin/facture_supprimer.php @@ -12,13 +12,11 @@ $id = (int) qg('id'); $f = $facture->get($id); -if (!$f) +if (!$client) { throw new UserException("Ce document n'existe pas."); } -$csrf_key = 'delete_doc_' . $f->id; - $form->runIf(f('delete') && !$form->hasErrors(), function () use ($facture, $f, $form) { @@ -30,8 +28,7 @@ $form->runIf(f('delete') && !$form->hasErrors(), { $form->addError($e->getMessage()); } - }, $csrf_key); + }); $tpl->assign('doc', $f); -$tpl->assign(compact('csrf_key')); $tpl->display(PLUGIN_ROOT . '/templates/facture_supprimer.tpl'); diff --git a/admin/pdf.php b/admin/pdf.php index 49ff41d..d953414 100644 --- a/admin/pdf.php +++ b/admin/pdf.php @@ -4,30 +4,11 @@ namespace Paheko; require_once __DIR__ . '/_inc.php'; $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); +$users = new Users\Users; f(['id' => 'required|numeric']); $id = (int) qg('id'); $sign_tag = UserTemplate\Functions::signature(); -$h = static function ($value): string { - return htmlspecialchars_decode( - htmlentities($value, ENT_NOQUOTES, 'UTF-8', false), - ENT_NOQUOTES - ); -}; -$br = static function ($value) use ($h): string { - return nl2br($h($value)); -}; -$format_identifier = static function ($value) use ($h): string { - $value = preg_replace('/\s+/', '', (string) $value); - return $value !== '' ? $h(implode(' ', str_split($value, 3))) : ''; -}; -$detail_line = static function (string $label, $value) use ($h, $br): string { - if ($value === null || trim((string) $value) === '') { - return ''; - } - - return sprintf('
%s%s
', $h($label), $br($value)); -}; // Vérification que le document existe if (!$f = $facture->get($id)) @@ -37,42 +18,47 @@ if (!$f = $facture->get($id)) $moyen_paiement = $facture->getMoyenPaiement($f->moyen_paiement); -// Récupération infos membre/client dans un objet normalisé pour le rendu -if ($f->receveur_membre) +// Récupération infos membre +try { - $user = Users\Users::get($f->receveur_id); + if ($f->receveur_membre) + { + $c = $users->get($f->receveur_id); + // l'identité du membre peut être redéfinie dans la configuration des membres + $name_fields = \Paheko\Users\DynamicFields::getNameFields(); + array_walk($name_fields, function(&$elem) use ($c) { + $elem = $c->$elem; + }); + $nom_client = implode(" ", $name_fields); + if (preg_match('/^ +$/', $nom_client)) { + $nom_client = "** ABSENT **"; + } - if (!$user) { - throw new UserException("Ce membre n'existe plus."); + // adresse, code postal et ville peuvent être redéfini(e)s dans la configuration du plugin + $adresse_client = $plugin->getConfig('adresse_client'); + if ($adresse_client != null && $c->$adresse_client != null) { $c->adresse = $c->$adresse_client; } + $code_postal_client = $plugin->getConfig('code_postal_client'); + if ($code_postal_client != null && $c->$code_postal_client != null) { $c->code_postal = $c->$code_postal_client; } + $ville_client = $plugin->getConfig('ville_client'); + if ($ville_client != null && $c->$ville_client != null) { $c->ville = $c->$ville_client; } + + foreach(['ville','code_postal','adresse'] as $v) + { + if($c->$v == '') + { + $c->$v = '[À RENSEIGNER DANS LA FICHE MEMBRE]'; + } + } + } + else + { + $c = $client->get($f->receveur_id); + $nom_client = $c->nom; } - - $nom_client = Users\DynamicFields::getNameFromArray($user) ?: '** ABSENT **'; - $user_fields = Users\DynamicFields::getInstance()->listAssocNames(); - $get_user_field = static function (?string $configured, string $fallback) use ($user, $user_fields) { - $field = $configured ?: $fallback; - return array_key_exists($field, $user_fields) ? ($user->$field ?? null) : null; - }; - - $c = (object) [ - 'nom' => $nom_client, - 'adresse' => $get_user_field($plugin->getConfig('adresse_client'), 'adresse') ?: '[À RENSEIGNER DANS LA FICHE MEMBRE]', - 'code_postal' => $get_user_field($plugin->getConfig('code_postal_client'), 'code_postal') ?: '[À RENSEIGNER]', - 'ville' => $get_user_field($plugin->getConfig('ville_client'), 'ville') ?: '[À RENSEIGNER]', - 'siret' => null, - 'email' => Users\DynamicFields::getFirstEmailField() ? ($user->{Users\DynamicFields::getFirstEmailField()} ?? null) : null, - 'telephone' => null, - ]; } -else +catch(UserException $e) { - $client_data = $client->get($f->receveur_id); - - if (!$client_data) { - throw new UserException("Ce client n'existe plus."); - } - - $nom_client = $client_data->nom; - $c = $client_data; + $form->addError($e); } // Formatage dates @@ -88,417 +74,307 @@ if (isset($f->date_echeance)) // Génération factures, devis et cotisation if ($f->type_facture != CERFA) { - $labels = [ - FACT => ['Facture', 'Émise le', 'Adressée à', 'Échéance de paiement'], - DEVIS => ['Devis', 'Émis le', 'Adressé à', 'Échéance du devis'], - COTIS => ['Reçu de cotisation', 'Émis le', 'Adressé à', 'Date du reçu'], - ]; - [$document_label, $emission_label, $recipient_label, $due_label] = $labels[$f->type_facture] ?? $labels[FACT]; - $doc = sprintf('%s n° %s', $document_label, $f->numero); - $theme = $plugin->getConfig('invoice_theme') ?: 'modern'; - $theme = in_array($theme, ['modern', 'classic', 'minimal'], true) ? $theme : 'modern'; - $accent = $plugin->getConfig('invoice_accent_color') ?: '#4656A5'; - $accent = preg_match('/^#[0-9A-Fa-f]{6}$/', $accent) ? $accent : '#4656A5'; - $show_address_borders = $plugin->getConfig('invoice_show_address_borders') !== false; - $show_email = $plugin->getConfig('invoice_show_email') !== false; - $show_web = $plugin->getConfig('invoice_show_web') !== false; - $address_border_class = $show_address_borders ? 'invoice-address-borders' : 'invoice-no-address-borders'; - $currency = $config->get('currency') ?: '€'; + switch ($f->type_facture) + { + case FACT: + $doc = 'Facture n° '. $f->numero; + $txtemis = $doc . " - Émise le " . $emission; + $txtdest = "Adressée à :"; + break; + case DEVIS: + $doc = 'Devis n° '. $f->numero; + $txtemis = $doc . " - Émis le " . $emission; + $txtdest = "Adressé à :"; + break; + case COTIS: + $doc = 'Reçu de cotisation n° '. $f->numero; + $txtemis = $doc . " - Émis le " . $emission; + $txtdest = "Adressé à :"; + break; + } + + // utiliser l'adresse configurée dans le plugin sinon celle de l'asso sinon rien ! + if ($plugin->getConfig('rue_asso') != null && + $plugin->getConfig('cp_asso') != null && + $plugin->getConfig('ville_asso') != null) + { + $adresse = + (($plugin->getConfig('numero_rue_asso') != null) ? $plugin->getConfig('numero_rue_asso') . " " : "") . + $plugin->getConfig('rue_asso') . "
" . + $plugin->getConfig('cp_asso') . " " . + $plugin->getConfig('ville_asso'); + } + else if ($config->get('org_address') != null) + { + $adresse = str_replace("\n", '
', $config->get('org_address')); + } + else { + $adresse = ""; + } + + $logo=''; + if ($plugin->getConfig('logo')) { + $logo = ''; + } + $asso = + // 'Émis par :

'. + ''.$config->get('org_name')."
". + $adresse ."
". + (($t = $plugin->getConfig('rna_asso'))?"RNA : $t
":''). + (($t = $plugin->getConfig('siret_asso'))?"SIRET : " . implode(' ', str_split($t, 3)) . "
":''). + (($t = $config->get('email_asso'))?"Email : $t
":''). + (($t = $config->get('site_asso'))?"Site web : $t
":''); + + $receveur = + $txtdest.'
'. + ''.$nom_client.'
'. + (($t = $f->nom_contact)?"Contact : $t
":''). + $c->adresse."
". + $c->code_postal.' '.$c->ville."
". + (($t = $c->siret)?"SIREN/SIRET : " . implode(' ', str_split($t, 3)) . "
":''). + (($t = $c->email)?"Email : $t
":''). + (($t = $c->telephone)?"Tel : $t
":''); + if ($f->type_facture == FACT) { + $receveur .= + (($t = $f->numero_commande)?"Commande N° : $t
":''). + (($t = $f->reference_acheteur)?"Référence : $t
":''); + } $total = Utils::money_format($f->total, ',', ' '); - $ttc = $plugin->getConfig('ttc') ? 'TTC' : 'HT'; - if ($plugin->getConfig('rue_asso') && $plugin->getConfig('cp_asso') && $plugin->getConfig('ville_asso')) { - $adresse = trim(($plugin->getConfig('numero_rue_asso') ? $plugin->getConfig('numero_rue_asso') . ' ' : '') . $plugin->getConfig('rue_asso')) - . "\n" . $plugin->getConfig('cp_asso') . ' ' . $plugin->getConfig('ville_asso'); - } - else { - $adresse = $config->get('org_address_public') ?: $config->get('org_address') ?: ''; - } + // Devis et facture + if ($f->type_facture != COTIS) + { + $echeance = ($f->type_facture?'Échéance de paiement':'Échéance du devis')." : ".$echeance; + if ($f->type_facture == FACT) { + $reglee = !$f->reglee?'Cette facture est en attente de règlement.':'Cette facture a été réglée.'; + } else { + $reglee = ""; + } + $footer = str_replace("\n", '
', $plugin->getConfig('footer') ?? '[Pied de page à configurer]'); + $ttc = $plugin->getConfig('ttc') ? 'TTC':'HT'; - $logo = ''; - if ($plugin->getConfig('logo') && ($logo_url = $config->fileURL('logo'))) { - $logo = sprintf('', $h($logo_url)); - } + // Génération du contenu de la facture + ob_start(); + echo << + Contenu - $doc + +
- $asso_details = '' . $h($config->get('org_name')) . ''; - $asso_details .= $adresse ? '

' . $br($adresse) . '

' : ''; - $asso_details .= ($t = $plugin->getConfig('rna_asso')) ? $detail_line('RNA', $t) : ''; - $asso_details .= ($t = $plugin->getConfig('siret_asso')) ? '
SIRET' . $format_identifier($t) . '
' : ''; - $asso_details .= $show_email && ($t = $config->get('org_email')) ? $detail_line('Email', $t) : ''; - $asso_details .= ($t = $config->get('org_phone')) ? $detail_line('Tel.', $t) : ''; - $asso_details .= $show_web && ($t = $config->get('org_web')) ? $detail_line('Web', $t) : ''; - $brand_summary = '
' . $logo . '' . $h($config->get('org_name')) . '
'; + + + + + + + + +EOF; - $receveur = '' . $h($nom_client) . ''; - $receveur .= ($t = ($f->nom_contact ?? null)) ? $detail_line('Contact', $t) : ''; - $receveur .= '

' . $br($c->adresse ?? '') . '
' . $h(trim(($c->code_postal ?? '') . ' ' . ($c->ville ?? ''))) . '

'; - $receveur .= ($t = $c->siret ?? null) ? '
SIREN/SIRET' . $format_identifier($t) . '
' : ''; - $receveur .= $show_email && ($t = $c->email ?? null) ? $detail_line('Email', $t) : ''; - $receveur .= ($t = $c->telephone ?? null) ? $detail_line('Tel.', $t) : ''; - $receveur .= $f->type_facture == FACT && ($t = ($f->numero_commande ?? null)) ? $detail_line('Commande', $t) : ''; - $receveur .= $f->type_facture == FACT && ($t = ($f->reference_acheteur ?? null)) ? $detail_line('Référence', $t) : ''; - - if ($f->type_facture != COTIS) { - $rows = ''; - - foreach($f->contenu as $v) { - $rows .= sprintf( - '', - $br($v['designation']), - $h(Utils::money_format($v['prix'], ',', ' ')), - $h($currency) - ); + $i = 1; + foreach($f->contenu as $k=>$v) + { + echo ''; + $i++; } - $payment_terms = trim((string) ($plugin->getConfig('payment_terms') ?: '')); - $invoice_note = trim((string) ($plugin->getConfig('invoice_note') ?: '')); - $status = ''; + echo << + + + + + + +
+ Désignations + + Prix +
%s%s %s
'; + echo str_replace("\n", '
', $v['designation']); + echo '
'; + echo Utils::money_format($v['prix'], ',', ' ') .' €'; + echo '
Total
Net à payer
$total €
({$ttc})
+
+
Détails
+
- if ($plugin->getConfig('invoice_show_status') && $f->type_facture == FACT) { - $status_class = $f->reglee ? 'paid' : 'pending'; - $status_label = $f->reglee ? 'Facture réglée' : 'En attente de règlement'; - $status = sprintf('

%s

', $status_class, $h($status_label)); + $echeance
+ $reglee + Moyen de paiement : $moyen_paiement +

+ $footer +

+
+ +EOF; + if ($f->type_facture == DEVIS) { + echo <<Bon pour accord, date et signature

+EOF; } - $approval = $f->type_facture == DEVIS ? '
Bon pour accordDate, nom et signature
' : ''; - $note = $invoice_note !== '' ? '

' . $br($invoice_note) . '

' : ''; - $terms = $payment_terms !== '' ? $detail_line('Conditions', $payment_terms) : ''; - $footer = $br($plugin->getConfig('footer') ?? '[Pied de page à configurer]'); - $echeance_html = $detail_line($due_label, $echeance ?? ''); - $moyen_html = $detail_line('Moyen de paiement', $moyen_paiement); + $content = ob_get_clean(); - $content = << - - - - - - - - - $rows - -
DésignationMontant
- -
-
- $echeance_html - $terms - $moyen_html - $status -
-
- Total $ttc - $total {$h($currency)} - Net à payer -
-
-$note -$approval -
$footer
-HTML; } - else { - $lieu = $plugin->getConfig('ville_asso') ?: ''; + else // Reçu de cotisation + { + $lieu = $plugin->getConfig('ville_asso'); $intitule = $f->contenu['intitule']; - $souscription = date('d/m/Y', strtotime($f->contenu['souscription'])); - $expiration = $f->contenu['expiration'] == '1970-01-01' - ? "le jour même, s'agissant d'une cotisation ponctuelle" - : 'le ' . date('d/m/Y', strtotime($f->contenu['expiration'])); - $footer = $br($plugin->getConfig('footer') ?? ''); - $content = << -

À {$h($lieu)}, le {$h($emission)},

+ $souscription = date('d/m/Y', strtotime($f->contenu['souscription'])); + + if($f->contenu['expiration'] == '1970-01-01') + { + $expiration = "jour même, s'agissant d'une cotisation ponctuelle."; + } + else { + $expiration = date('d/m/Y', strtotime($f->contenu['expiration'])); + } + + // Génération du contenu du reçu de cotisation + $content = << + Reçu de votre cotisation - $doc + +
+
+

À $lieu, le $emission,

Bonjour,

-

Nous accusons réception de votre cotisation « {$h($intitule)} » reçue le {$h($emission)} et nous vous en remercions.

-

Nous reconnaissons que vous avez acquitté la somme de {$h($total)} {$h($currency)}. Votre adhésion sera donc effective à compter du {$h($souscription)} jusqu'à {$h($expiration)}.

-

Nous vous prions de recevoir nos meilleures salutations.

-

Pour {$h($config->get('org_name'))}

- - -
$footer
-HTML; + +

Nous accusons réception de votre cotisation « $intitule » reçue le $emission et nous vous en remercions.

+

Nous reconnaissons que vous avez acquitté la somme de {$total} €.
111 + Votre adhésion sera donc effective à compter du $souscription jusqu’au $expiration.

+
+ +

Nous vous prions de recevoir, chère adhérente, cher adhérent, nos meilleures salutations,

+
+

-représentant·e de l'asso-

+
+

Nous vous rappelons que la cotisation n’est pas soumise à la TVA et qu’elle ne donne pas lieu à la délivrance d’une facture. Elle n’ouvre pas droit au bénéfice des dispositions des articles 200, 238 bis et 885-0 V bis A du code général des impôts.

+
+EOF; } - $html_title = $h($doc . '_' . $emission); - $doc_html = $h($doc); - $txtemis = $h($emission_label . ' ' . $emission); - $txtdest = $h($recipient_label); + //-- Layout du document - $html = << - + - $html_title + {$doc}_{$emission} -
-
-
- $brand_summary -
-
-

$doc_html

-

$txtemis

-
-
-
-
-

Émetteur

- $asso_details -
-
-

$txtdest

- $receveur -
-
- $content -
+

+ $txtemis +

+ + + + + + + +
$asso$receveur
+
+ $content -HTML; +EOF; + + $html = ob_get_clean(); + } // Génération du CERFA elseif ($f->type_facture == CERFA) { @@ -518,9 +394,9 @@ elseif ($f->type_facture == CERFA) $t['objet2'] = $plugin->getConfig('objet_2'); $t['nom'] = $nom_client; - $t['adresse'] = $c->adresse ?? ''; - $t['cp'] = $c->code_postal ?? ''; - $t['ville'] = $c->ville ?? ''; + $t['adresse'] = $c->adresse; + $t['cp'] = $c->code_postal; + $t['ville'] = $c->ville; $t['total'] = '***'.Utils::money_format($f->total).'***'; $t['total_lettre'] = numfmt_create('fr_FR', \NumberFormatter::SPELLOUT)->format($f->total/100). ' euros'; diff --git a/config.json b/config.json index 4f5079f..b2f1b4d 100644 --- a/config.json +++ b/config.json @@ -1,14 +1,6 @@ { "footer": "[EXEMPLE]\nAssociation exonérée des impôts commerciaux\nEn cas de retard de paiement, indemnité forfaitaire légale pour frais de recouvrement : 40,00 €\n[Coordonnées bancaires]\nAssociation enregistrée en préfecture de XXX au numéro YYY", "pattern": "%{type}-%{year}-%{ynumber}", - "invoice_theme": "modern", - "invoice_accent_color": "#4656A5", - "invoice_note": "", - "payment_terms": "Paiement à réception", - "invoice_show_status": true, - "invoice_show_address_borders": true, - "invoice_show_email": true, - "invoice_show_web": true, "ttc": false, "validate_cp": true -} +} \ No newline at end of file diff --git a/data/schema.sql b/data/schema.sql index fc5ec68..923b4eb 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -3,13 +3,13 @@ CREATE TABLE IF NOT EXISTS plugin_facturation_factures ( type_facture INTEGER NOT NULL DEFAULT 0, numero TEXT NOT NULL UNIQUE, receveur_membre INTEGER NOT NULL, -- bool - receveur_id INTEGER NOT NULL, - date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), - date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance), + receveur_id INTEGER NOT NULL, + date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), + date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance), reglee INTEGER DEFAULT 0, -- bool archivee INTEGER DEFAULT 0, -- bool moyen_paiement TEXT NOT NULL, - contenu TEXT NOT NULL, + contenu TEXT NOT NULL, total INTEGER DEFAULT 0, nom_contact TEXT, numero_commande TEXT, @@ -20,10 +20,10 @@ CREATE TABLE IF NOT EXISTS plugin_facturation_factures ( CREATE TABLE IF NOT EXISTS plugin_facturation_clients ( id INTEGER PRIMARY KEY, - nom TEXT NOT NULL, - adresse TEXT NOT NULL, - code_postal TEXT NOT NULL, - ville TEXT NOT NULL, + nom TEXT NOT NULL, + adresse TEXT NOT NULL, + code_postal TEXT NOT NULL, + ville TEXT NOT NULL, siret TEXT, date_creation TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date_creation) IS NOT NULL AND date(date_creation) = date_creation), -- Date d\'inscription telephone TEXT, diff --git a/install.php b/install.php index b72d912..81250a2 100644 --- a/install.php +++ b/install.php @@ -1,13 +1,8 @@ import(dirname(__FILE__) . "/data/schema.sql"); - -$default_config = json_decode(file_get_contents(__DIR__ . '/config.json')); - -if ($default_config instanceof \stdClass) { - $plugin->setConfig($default_config); - $plugin->save(); -} diff --git a/lib/Client.php b/lib/Client.php index f1fcbb9..66e7d75 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -4,7 +4,7 @@ namespace Paheko\Plugin\Facturation; use Paheko\DB; use Paheko\DynamicList; -use Paheko\Plugins; +use Paheko\Entities\Plugin; use Paheko\UserException; use Paheko\Utils; @@ -29,12 +29,9 @@ class Client public function __construct() { - $plugin = Plugins::get('facturation'); - - if ($plugin) { - $this->config['unique_client_name'] = (bool) $plugin->getConfig('unique_client_name'); - $this->config['validate_cp'] = (bool) ($plugin->getConfig('validate_cp') ?? true); - } + $plugin = new Plugin('facturation'); + $this->config['unique_client_name'] = $plugin->getConfig('unique_client_name') ?: false; + $this->config['validate_cp'] = $plugin->getConfig('validate_cp') ?: false; } public function _checkFields(&$data) @@ -105,20 +102,8 @@ class Client { $db = DB::getInstance(); - $client = $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation + return $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation FROM plugin_facturation_clients WHERE id = ? LIMIT 1;', (int)$id); - - if (!$client) { - return null; - } - - foreach (['siret', 'telephone', 'email', 'nom_contact', 'note'] as $key) { - if (!property_exists($client, $key)) { - $client->$key = null; - } - } - - return $client; } public function listAll() diff --git a/lib/Facture.php b/lib/Facture.php index 1cdaaf5..e6f766e 100644 --- a/lib/Facture.php +++ b/lib/Facture.php @@ -6,10 +6,9 @@ use DateTime; use Paheko\Config; use Paheko\DB; use Paheko\DynamicList; -use Paheko\Plugins; use Paheko\UserException; -use Paheko\Users\DynamicFields; use Paheko\Utils; +use Paheko\Services\Services_User; class Facture { @@ -48,52 +47,17 @@ class Facture 'accounts' => [], 'label' => 'Facture', 'help' => ''], - CERFA => [ - 'id' => CERFA, - 'accounts' => [], - 'label' => 'Reçu fiscal', - 'help' => 'Fonction historique conservée pour les anciens reçus. Le module Paheko "Reçus fiscaux" est recommandé pour les nouveaux usages.'], - COTIS => [ - 'id' => COTIS, - 'accounts' => [], - 'label' => 'Reçu de cotisation', - 'help' => 'Fonction historique conservée pour les anciens reçus. Le module Paheko "Reçu de paiement" est recommandé pour les nouveaux usages.'], ]; public function __construct() { } - private function normalizeDate($value, string $field): string - { - if ($value instanceof \DateTimeInterface) { - return $value->format('Y-m-d'); - } - - $value = trim((string) $value); - - foreach (['!Y-m-d', '!d/m/Y'] as $format) { - $date = DateTime::createFromFormat($format, $value); - $errors = DateTime::getLastErrors(); - - if ($date && (!$errors || (!$errors['warning_count'] && !$errors['error_count']))) { - return $date->format('Y-m-d'); - } - } - - throw new UserException(sprintf('La date "%s" est invalide pour le champ %s.', $value, $field)); - } // Fix : est dépendant de l'ordre des données dans l'array // et implique que toutes les données soient présentes (pas possible de faire un update partiel) public function _checkFields(&$datas) { - $type_facture = isset($datas['type_facture']) ? (int) $datas['type_facture'] : null; - $fac = in_array($type_facture, [DEVIS, FACT], true); - $cerfa = $type_facture === CERFA; - $recu = $type_facture === COTIS; - $total = 0; - foreach($datas as $k=>$data) { if (!in_array($k, $this->keys)) @@ -112,11 +76,24 @@ class Facture switch($k) { case 'type_facture': - $datas[$k] = (int) $datas[$k]; - - if (!array_key_exists($datas[$k], self::TYPES_NAMES)) { + if (!array_key_exists($datas[$k], $this->types)) { throw new UserException("$k est de type non-attendue ($data)."); } + if ($datas[$k] < 2) { + $fac = true; + $cerfa = false; + $recu = false; + } + elseif ($datas[$k] == 2) { + $fac = false; + $cerfa = true; + $recu = false; + } + elseif ($datas[$k] == 3) { + $fac = false; + $cerfa = false; + $recu = true; + } break; case 'receveur_membre': case 'reglee': @@ -131,11 +108,11 @@ class Facture } break; case 'date_emission': - $datas[$k] = $this->normalizeDate($data, $k); + $datas[$k] = \DateTime::createFromFormat('!d/m/Y', $data)->format('Y-m-d'); break; case 'date_echeance': - $datas[$k] = $this->normalizeDate($data, $k); - if (isset($datas['date_emission']) && DateTime::createFromFormat('!Y-m-d', $datas[$k])->format('U') < DateTime::createFromFormat('!Y-m-d', $datas['date_emission'])->format('U')) + $datas[$k] = \DateTime::createFromFormat('!d/m/Y', $data)->format('Y-m-d'); + if (DateTime::createFromFormat('!Y-m-d', $datas[$k])->format('U') < DateTime::createFromFormat('!Y-m-d', $datas['date_emission'])->format('U')) { throw new UserException("La date d'échéance est antérieure à la date d'émission ($data)."); } @@ -157,12 +134,12 @@ class Facture if (empty($r['designation']) && empty($r['prix'])) { unset($datas[$k][$g]); + unset($datas[$k]['prix']); continue; } - elseif (!isset($r['prix']) || $r['prix'] === '') + elseif (! is_numeric($r['prix']) && empty($r['prix'])) { - $datas[$k][$g]['prix'] = 0; - $r['prix'] = 0; + $datas[$k]['prix'] = 0; } elseif (empty($r['designation'])) { throw new UserException("Une au moins des désignations est absente."); @@ -274,7 +251,7 @@ class Facture $type = ctype_digit($v) ? 'd' : 's'; return sprintf('%' . $match[1] . $type, $v); }, $selpattern); - $modele = '/^' . preg_quote($prefixe, '/') . '\d+$/'; + $modele = '/^' . $prefixe . '\d+$/'; $numeros_filtres = array_filter($numeros, function($elem) use ($modele) { return preg_match($modele, $elem); }, 0); @@ -315,12 +292,6 @@ class Facture $r->contenu = json_decode($r->contenu, true); } - foreach (['nom_contact', 'numero_commande', 'reference_acheteur'] as $key) { - if (!property_exists($r, $key)) { - $r->$key = null; - } - } - $r->date_emission = \DateTime::createFromFormat('!Y-m-d', $r->date_emission); if ($r->date_echeance) { @@ -349,19 +320,15 @@ class Facture public function list($all = true): DynamicList { - $db = DB::getInstance(); - $id_field = DynamicFields::getNameFieldsSQL('u'); - $plugin = Plugins::get('facturation'); - $user_fields = DynamicFields::getInstance()->listAssocNames(); + $id_field = \Paheko\Users\DynamicFields::getNameFieldsSQL('u'); + $plugin_name = preg_replace('/^.*\/(\w+)\/$/', '${1}', \Paheko\PLUGIN_ADMIN_URL); + $plugin = \Paheko\Plugins::get($plugin_name); // adresse et ville peuvent être redéfinies dans la configuration du plugin - $adresse_client = $plugin ? $plugin->getConfig('adresse_client') : null; - $adresse_client = $adresse_client ?: 'adresse'; - $adresse = array_key_exists($adresse_client, $user_fields) ? 'u.' . $db->quoteIdentifier($adresse_client) : 'NULL'; - - $ville_client = $plugin ? $plugin->getConfig('ville_client') : null; - $ville_client = $ville_client ?: 'ville'; - $ville = array_key_exists($ville_client, $user_fields) ? 'u.' . $db->quoteIdentifier($ville_client) : 'NULL'; + $adresse_client = $plugin->getConfig('adresse_client'); + if ($adresse_client == null) { $adresse = 'u.adresse'; } else { $adresse = 'u.' . $adresse_client; } + $ville_client = $plugin->getConfig('ville_client'); + if ($ville_client == null) { $ville = 'u.ville'; } else { $ville = 'u.' . $ville_client; } $columns = [ // Sélectionner cette colonne, mais ne pas la mettre dans la liste des colonnes @@ -390,7 +357,7 @@ class Facture 'receveur' => [ 'label' => 'Receveur', // l'identité du membre peut être redéfinie dans la configuration des membres - 'select' => sprintf('CASE WHEN receveur_membre THEN COALESCE(NULLIF(%s, \'\'), \'** ABSENT **\') ELSE c.nom END', $id_field), + 'select' => sprintf('CASE WHEN receveur_membre THEN CASE %s WHEN "" THEN "** ABSENT **" ELSE %s END ELSE c.nom END', $id_field, $id_field), ], 'receveur_adresse' => [ // l'adresse peut être redéfinie dans la configuration du plugin @@ -426,7 +393,7 @@ class Facture $tables = 'plugin_facturation_factures AS f INNER JOIN plugin_facturation_paiement AS mp ON mp.code = f.moyen_paiement - LEFT JOIN users_view AS u ON f.receveur_membre = 1 AND u.id = f.receveur_id + LEFT JOIN users AS u ON f.receveur_membre = 1 AND u.id = f.receveur_id LEFT JOIN plugin_facturation_clients AS c ON f.receveur_membre = 0 AND c.id = f.receveur_id'; if ($all) { @@ -437,7 +404,7 @@ class Facture $list = new DynamicList($columns, $tables, $where); $list->orderBy('date_emission', true); - $currency = Config::getInstance()->currency; + $currency = Config::getInstance()->monnaie; $list->setModifier(function ($row) use ($currency) { // Remplir la colonne virtuelle @@ -493,10 +460,7 @@ class Facture } else // Si c'est un membre de l'asso { - if (!\Paheko\Users\Users::exists((int) $id)) - { - throw new UserException("Ce membre n'existe pas."); - } + throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients"); } $r = (array)DB::getInstance()->get('SELECT *, strftime(\'%s\', date_emission) AS date_emission, @@ -528,10 +492,7 @@ class Facture } else // Si c'est un membre de l'asso { - if (!\Paheko\Users\Users::exists((int) $id)) - { - throw new UserException("Ce membre n'existe pas."); - } + throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients"); } return DB::getInstance()->test('plugin_facturation_factures', 'receveur_membre = ? AND receveur_id = ?', $base, $id); diff --git a/plugin.ini b/plugin.ini index b90c87f..1916e45 100644 --- a/plugin.ini +++ b/plugin.ini @@ -1,8 +1,8 @@ name="Facturation" description="Permet d'éditer des factures et devis à ses membres ainsi qu'à une base de clients supplémentaire." -author="Sur la base de Jean-Christophe Engel, amélioré par Jordan" +author="zou ; adapté par Jean-Christophe Engel" url="https://gitea.zaclys.com/lesanges/facturation" -version="0.27.2" +version="0.17.2" menu=true restrict_section="accounting" restrict_level="read" diff --git a/templates/_form.tpl b/templates/_form.tpl index 31168c7..4ab076e 100644 --- a/templates/_form.tpl +++ b/templates/_form.tpl @@ -63,7 +63,7 @@
{if !empty($clients)}
- {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default="membre" source=$doc} + {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default=1 source=$doc} {input type="radio" name="base_receveur" value="client" label="Un·e client·e" source=$doc}
{/if} diff --git a/templates/_js.tpl b/templates/_js.tpl index 6b1ff1c..ed7feeb 100644 --- a/templates/_js.tpl +++ b/templates/_js.tpl @@ -45,15 +45,9 @@ function changeTypeSaisie(type) { g.toggle(['.type_client', '.type_membre'], false); - document.querySelectorAll('.type_client input, .type_client select, .type_client textarea, .type_membre input, .type_membre select, .type_membre textarea').forEach((e) => { - e.disabled = true; - }); if (type) { g.toggle('.type_' + type, true); - document.querySelectorAll('.type_' + type + ' input, .type_' + type + ' select, .type_' + type + ' textarea').forEach((e) => { - e.disabled = false; - }); } } @@ -82,11 +76,9 @@ } const selclient = document.querySelector('#f_client'); - if (selclient) { - selclient.addEventListener("change", () => { - modifierContact(selclient, '#f_contact_list', '#f_nom_contact'); - }); - } + selclient.addEventListener("change", () => { + modifierContact(selclient, '#f_contact_list', '#f_nom_contact'); + }); } ()); @@ -94,18 +86,12 @@ // Hide type specific parts of the form function hideAllTypes() { g.toggle('[data-types]', false); - document.querySelectorAll('[data-types] input, [data-types] select, [data-types] textarea, [data-types] button').forEach((e) => { - e.disabled = true; - }); } // Toggle parts of the form when a type is selected function selectType(v) { hideAllTypes(); g.toggle('[data-types~=t' + v + ']', true); - document.querySelectorAll('[data-types~=t' + v + '] input, [data-types~=t' + v + '] select, [data-types~=t' + v + '] textarea, [data-types~=t' + v + '] button').forEach((e) => { - e.disabled = false; - }); 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) => { diff --git a/templates/clients.tpl b/templates/clients.tpl index da8a48a..2c30760 100644 --- a/templates/clients.tpl +++ b/templates/clients.tpl @@ -25,7 +25,7 @@ {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {linkbutton shape="delete" href="client_supprimer.php?id=%d"|args:$row.id label="Supprimer"} {/if} - {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} + {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)} {linkbutton shape="edit" href="client_modifier.php?id=%d"|args:$row.id label="Modifier"} {/if} {linkbutton shape="user" href="client.php?id=%d"|args:$row.id label="Fiche client"} diff --git a/templates/config.tpl b/templates/config.tpl index b9f20c9..903f809 100644 --- a/templates/config.tpl +++ b/templates/config.tpl @@ -38,27 +38,14 @@ {input type="checkbox" name="logo" value="1" source=$conf label="Imprimer le logo de l'association"} {input type="textarea" class="full-width" rows="5" name="footer" source=$conf label="Pied de document — informations légales" required=true} -
- Présentation des factures -
- {input type="select" name="invoice_theme" label="Style visuel" options=$invoice_themes source=$conf} - {input type="color" name="invoice_accent_color" label="Couleur d'accent" source=$conf default="#4656A5"} - {input type="text" name="payment_terms" label="Conditions de paiement" source=$conf} - {input type="textarea" class="full-width" rows="3" name="invoice_note" source=$conf label="Note affichée sous le tableau"} - {input type="checkbox" name="invoice_show_status" value="1" source=$conf label="Afficher le statut de règlement sur la facture"} - {input type="checkbox" name="invoice_show_address_borders" value="1" source=$conf label="Afficher les bordures des blocs émetteur et destinataire"} - {input type="checkbox" name="invoice_show_email" value="1" source=$conf label="Afficher les adresses email"} - {input type="checkbox" name="invoice_show_web" value="1" source=$conf label="Afficher le site web de l'association"} -
-
Choisir les champs à faire figurer sur la facture ou le devis pour l'adresse d'un membre
- {input type="select" name="adresse_client" label="Adresse" required=false options=$champsPaheko source=$conf} - {input type="select" name="code_postal_client" label="Code postal" required=false options=$champsPaheko source=$conf} - {input type="select" name="ville_client" label="Ville" required=false options=$champsPaheko source=$conf} + {input type="select" name="adresse_client" label="Adresse" required=true options=$champsPaheko source=$conf} + {input type="select" name="code_postal_client" label="Code postal" required=true options=$champsPaheko source=$conf} + {input type="select" name="ville_client" label="Ville" required=true options=$champsPaheko source=$conf}
@@ -73,6 +60,7 @@ F = Facture, D = Devis + Pour personnaliser l'apparence de la facture, il faut pour l'instant se retrousser les manches et éditer soi-même le fichier admin/pdf.php du plugin !

diff --git a/templates/facture_supprimer.tpl b/templates/facture_supprimer.tpl index 62096a2..3f9c761 100644 --- a/templates/facture_supprimer.tpl +++ b/templates/facture_supprimer.tpl @@ -16,7 +16,7 @@

- {csrf_field key=$csrf_key} + {csrf_field key="delete_doc_"|cat:$doc.id}

diff --git a/upgrade.php b/upgrade.php index ff81e15..f47ac27 100644 --- a/upgrade.php +++ b/upgrade.php @@ -2,6 +2,7 @@ namespace Paheko; use Paheko\Plugin\Facturation\Facture; +use Paheko\Entities\Files\File; $db = DB::getInstance(); $old_version = $plugin->oldVersion(); @@ -24,9 +25,11 @@ if (version_compare($old_version, '0.3.0', '<')) $conf = $db->getAssoc('SELECT cle, valeur FROM plugin_facturation_config ORDER BY cle;'); foreach($conf as $k=>$v) { - $plugin->setConfigProperty($k, $v); + if(!$plugin->setConfig($k, $v)) + { + throw new UserException('Erreur dans la conversion de la configuration pour la clé : '.$k); + } } - $plugin->save(); $db->exec('DROP TABLE `plugin_facturation_config`;'); } @@ -56,13 +59,13 @@ if (version_compare($old_version, '0.4.0', '<')) type_facture INTEGER NOT NULL DEFAULT 0, numero TEXT NOT NULL UNIQUE, receveur_membre INTEGER NOT NULL, -- bool - receveur_id INTEGER NOT NULL, - date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), - date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance), + receveur_id INTEGER NOT NULL, + date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), + date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance), reglee INTEGER DEFAULT 0, -- bool archivee INTEGER DEFAULT 0, -- bool moyen_paiement TEXT NOT NULL, - contenu TEXT NOT NULL, + contenu TEXT NOT NULL, total REAL DEFAULT 0 ); @@ -78,13 +81,13 @@ EOT // 0.6.0 - if (version_compare($old_version, '0.6.0', '<')) { - defined('DEVIS') || define('DEVIS', 0); - defined('FACT') || define('FACT', 1); - defined('CERFA') || define('CERFA', 2); - defined('COTIS') || define('COTIS', 3); + define('DEVIS', 0); + define('FACT', 1); + define('CERFA', 2); + define('COTIS', 3); $facture = new Facture; $r = $db->first('SELECT id, total FROM plugin_facturation_factures;'); - if ($r && strpos((string) $r->total, '.') !== false) + if (strpos($r->total,'.')) { // SQL -> total integer $db->exec(<<listAll(); foreach($factures as $k=>$f) { - $contenu = []; - foreach($f->contenu as $line => $content) { // Petit bug qui peut arriver avec des contenus mal enregistrés en db @@ -136,16 +137,19 @@ EOT $facture->edit($f->id, $data); unset($contenu); } + + $path = __DIR__.'/data/default_sign.png'; + $png = (new File)->createAndStore('skel/plugin/facturation','sign.png', $path, null); } } // 0.6.2 - if (version_compare($old_version, '0.6.2', '<')) { - defined('DEVIS') || define('DEVIS', 0); - defined('FACT') || define('FACT', 1); - defined('CERFA') || define('CERFA', 2); - defined('COTIS') || define('COTIS', 3); + define('DEVIS', 0); + define('FACT', 1); + define('CERFA', 2); + define('COTIS', 3); $facture = new Facture; $db->exec(<<setConfigProperty('ttc', false); - $plugin->save(); + $plugin->setConfig('ttc', false); } // 0.8.1 - Signal menu item @@ -214,10 +217,10 @@ if (version_compare($old_version, '0.8.5', '<')) CREATE TABLE IF NOT EXISTS plugin_facturation_clients_tmp ( id INTEGER PRIMARY KEY, - nom TEXT NOT NULL, - adresse TEXT NOT NULL, - code_postal TEXT NOT NULL, - ville TEXT NOT NULL, + nom TEXT NOT NULL, + adresse TEXT NOT NULL, + code_postal TEXT NOT NULL, + ville TEXT NOT NULL, siret TEXT, date_creation TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date_creation) IS NOT NULL AND date(date_creation) = date_creation), telephone TEXT, @@ -258,10 +261,10 @@ if (version_compare($old_version, '0.15', '<')) CREATE TABLE IF NOT EXISTS plugin_facturation_clients_tmp ( id INTEGER PRIMARY KEY, - nom TEXT NOT NULL, - adresse TEXT NOT NULL, - code_postal TEXT NOT NULL, - ville TEXT NOT NULL, + nom TEXT NOT NULL, + adresse TEXT NOT NULL, + code_postal TEXT NOT NULL, + ville TEXT NOT NULL, siret TEXT, date_creation TEXT NOT NULL DEFAULT CURRENT_DATE CHECK (date(date_creation) IS NOT NULL AND date(date_creation) = date_creation), telephone TEXT, @@ -290,13 +293,13 @@ EOT type_facture INTEGER NOT NULL DEFAULT 0, numero TEXT NOT NULL UNIQUE, receveur_membre INTEGER NOT NULL, -- bool - receveur_id INTEGER NOT NULL, - date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), - date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance), + receveur_id INTEGER NOT NULL, + date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), + date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance), reglee INTEGER DEFAULT 0, -- bool archivee INTEGER DEFAULT 0, -- bool moyen_paiement TEXT NOT NULL, - contenu TEXT NOT NULL, + contenu TEXT NOT NULL, total INTEGER DEFAULT 0, nom_contact TEXT, numero_commande TEXT, @@ -319,64 +322,3 @@ EOT ); } - -// 0.18 - Personnalisation du rendu des factures -if (version_compare($old_version, '0.18', '<')) -{ - $defaults = json_decode(file_get_contents(__DIR__ . '/config.json'), true); - - foreach (['invoice_theme', 'invoice_accent_color', 'invoice_note', 'payment_terms', 'invoice_show_status'] as $key) { - if ($plugin->getConfig($key) === null && array_key_exists($key, $defaults)) { - $plugin->setConfigProperty($key, $defaults[$key]); - } - } - - $plugin->save(); -} - -// 0.19 - Réparation des schémas partiellement migrés -if (version_compare($old_version, '0.19', '<')) -{ - $tables = [ - 'plugin_facturation_clients' => [ - 'siret' => 'siret TEXT', - 'telephone' => 'telephone TEXT', - 'email' => 'email TEXT', - 'nom_contact' => 'nom_contact TEXT', - 'note' => 'note TEXT', - ], - 'plugin_facturation_factures' => [ - 'nom_contact' => 'nom_contact TEXT', - 'numero_commande' => 'numero_commande TEXT', - 'reference_acheteur' => 'reference_acheteur TEXT', - ], - ]; - - foreach ($tables as $table => $columns) { - $schema = $db->getTableSchema($table); - - if (!$schema) { - continue; - } - - foreach ($columns as $column => $definition) { - if (!isset($schema['columns'][$column])) { - $db->exec(sprintf('ALTER TABLE %s ADD COLUMN %s;', $db->quoteIdentifier($table), $definition)); - } - } - } -} - -// 0.25 - Options d'affichage du rendu facture -if (version_compare($old_version, '0.25', '<')) -{ - $defaults = json_decode(file_get_contents(__DIR__ . '/config.json'), true); - - foreach (['invoice_show_address_borders', 'invoice_show_email', 'invoice_show_web'] as $key) { - if ($plugin->getConfig($key) === null && array_key_exists($key, $defaults)) { - $plugin->setConfigProperty($key, $defaults[$key]); - } - } - - $plugin->save(); -}