From 4c7110036687d9535d831f0a579c287f74d7b78b Mon Sep 17 00:00:00 2001
From: Jean-Christophe Engel
Date: Thu, 2 Jul 2026 13:38:15 +0200
Subject: [PATCH 1/5] =?UTF-8?q?Correction=20erreurs,=20s=C3=A9curisation?=
=?UTF-8?q?=20code,=20personnalisation=20pr=C3=A9sentation=20(Jordan)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG | 10 +
admin/_facture_common.php | 114 +++--
admin/_inc.php | 38 +-
admin/client_supprimer.php | 2 +-
admin/config.php | 34 +-
admin/facture_supprimer.php | 7 +-
admin/pdf.php | 733 +++++++++++++++++++-------------
config.json | 10 +-
data/schema.sql | 16 +-
install.php | 9 +-
lib/Client.php | 25 +-
lib/Facture.php | 111 +++--
plugin.ini | 4 +-
templates/_form.tpl | 2 +-
templates/_js.tpl | 20 +-
templates/clients.tpl | 2 +-
templates/config.tpl | 20 +-
templates/facture_supprimer.tpl | 2 +-
upgrade.php | 134 ++++--
19 files changed, 822 insertions(+), 471 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index e4e5dae..d6a0e9f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,13 @@
+0.25
+- Options d'affichage du rendu facture (Jason)
+0.19
+- Réparation des schémas partiellement migrés (Jason)
+0.18
+- Personnalisation du rendu des factures (Jason)
+0.17
+- Ajout archivage factures
+0.15
+- Ajout champ contact à la fiche client
0.12
- Ajout Mollie à la table moyens de paiement
0.11
diff --git a/admin/_facture_common.php b/admin/_facture_common.php
index 9fbb19c..2be8c6b 100644
--- a/admin/_facture_common.php
+++ b/admin/_facture_common.php
@@ -4,30 +4,8 @@ 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 Exception('blabla illegal call'); // Fix: exception type?
+ throw new \LogicException('Appel interne invalide du formulaire de document.');
} else {
$target = ($target === 'new' ? true:false);
}
@@ -35,7 +13,6 @@ 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();
@@ -69,21 +46,18 @@ 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 ($client, &$data, $form)
+ function () use (&$data, $form)
{
try
{
- if ( count(f('designation')) !== count(f('prix')) )
- {
- throw new UserException('Nombre de désignations et de prix reçus différent.');
- }
+ $type = (int) f('type');
$data = [
'numero' => f('numero_facture'),
@@ -93,37 +67,51 @@ $form->runIf(f('save') && !$form->hasErrors(),
'archivee' => f('archivee') == 1?1:0,
'moyen_paiement' => f('moyen_paiement'),
'nom_contact' => f('nom_contact'),
- 'toto' => 0
+ 'numero_commande' => f('numero_commande') ?: null,
+ 'reference_acheteur' => f('reference_acheteur') ?: null,
];
- $data['type_facture'] = f('type');
- if (in_array(f('type'), [DEVIS, FACT]))
+
+ $data['type_facture'] = $type;
+
+ if (in_array($type, [DEVIS, FACT], true))
{
- foreach(f('designation') as $k=>$value)
+ $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)
{
- if (empty($value) && f('prix')[$k] != null) {
- throw new UserException("Il manque la désignation de la ligne " . $k+1 . " !!");
+ $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) . " !!");
}
- 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) {
+ 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 === '')) {
continue;
}
+ $prix_ligne = Utils::moneyToInteger($prix_ligne);
$data['contenu'][$k]['designation'] = $value;
- $data['contenu'][$k]['prix'] = Utils::moneyToInteger(f('prix')[$k]);
- $data['toto'] += Utils::moneyToInteger(f('prix')[$k]);
+ $data['contenu'][$k]['prix'] = $prix_ligne;
+ $total += $prix_ligne;
}
- $data['total'] = $data['toto'];
- unset($data['toto']);
+
+ $data['total'] = $total;
+
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 ( f('type') == CERFA )
+ elseif ($type === CERFA)
{
$data['moyen_paiement'] = f('moyen_paiement_cerfa');
$data['contenu'] = [
@@ -131,8 +119,12 @@ $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;
@@ -157,18 +149,20 @@ $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 ($radio, $fields, $facture, $form)
+ function () use (&$data, &$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);
@@ -183,8 +177,12 @@ $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'],
@@ -264,7 +262,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;
@@ -284,9 +282,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;
- $doc['numero_commande'] = $f->numero_commande;
- $doc['reference_acheteur'] = $f->reference_acheteur;
+ $doc['nom_contact'] = $f->nom_contact ?? null;
+ $doc['numero_commande'] = $f->numero_commande ?? null;
+ $doc['reference_acheteur'] = $f->reference_acheteur ?? null;
/* modif DD -- CERFA -------------------------------------- */
if ( $f->type_facture == CERFA ) {
$doc['total'] = $f->total;
@@ -346,11 +344,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', 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('users', iterator_to_array(Users\Users::iterateAssocByCategory()));
$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')]);
+$tpl->assign('number_pattern', PATTERNS_LIST[$plugin->getConfig('pattern')] ?? PATTERNS_LIST['']);
if ($target) {
$tpl->display(PLUGIN_ROOT . '/templates/facture_ajouter.tpl');
diff --git a/admin/_inc.php b/admin/_inc.php
index f11fc22..b0f7f07 100644
--- a/admin/_inc.php
+++ b/admin/_inc.php
@@ -22,6 +22,42 @@ 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;
@@ -63,7 +99,7 @@ $tpl->register_function('money_fac', function (array $params)
$current_value = htmlspecialchars($current_value, ENT_QUOTES, 'UTF-8');
}
- $currency = Config::getInstance()->get('monnaie');
+ $currency = Config::getInstance()->get('currency');
return sprintf('%s ', $name, $current_value, $currency);
}
);
diff --git a/admin/client_supprimer.php b/admin/client_supprimer.php
index 64a3395..2e8244f 100644
--- a/admin/client_supprimer.php
+++ b/admin/client_supprimer.php
@@ -12,7 +12,7 @@ $id = (int) qg('id');
$c = $client->get($id);
-if (!$client)
+if (!$c)
{
throw new UserException("Ce client n'existe pas.");
}
diff --git a/admin/config.php b/admin/config.php
index 638cd56..e6f57fd 100644
--- a/admin/config.php
+++ b/admin/config.php
@@ -11,6 +11,17 @@ $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'));
@@ -22,6 +33,14 @@ $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'));
@@ -36,8 +55,21 @@ $form->runIf('save', function () use ($plugin) {
$tpl->assign('ok', qg('ok') !== null);
-$tpl->assign('conf', $plugin->getConfig());
+$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('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 4711ddb..7640466 100644
--- a/admin/facture_supprimer.php
+++ b/admin/facture_supprimer.php
@@ -12,11 +12,13 @@ $id = (int) qg('id');
$f = $facture->get($id);
-if (!$client)
+if (!$f)
{
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)
{
@@ -28,7 +30,8 @@ $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 d953414..6b32848 100644
--- a/admin/pdf.php
+++ b/admin/pdf.php
@@ -4,11 +4,27 @@ 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((string) $value, ENT_QUOTES, 'UTF-8');
+};
+$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))
@@ -18,47 +34,42 @@ if (!$f = $facture->get($id))
$moyen_paiement = $facture->getMoyenPaiement($f->moyen_paiement);
-// Récupération infos membre
-try
+// Récupération infos membre/client dans un objet normalisé pour le rendu
+if ($f->receveur_membre)
{
- 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 **";
- }
+ $user = Users\Users::get($f->receveur_id);
- // 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; }
+ if (!$user) {
+ throw new UserException("Ce membre n'existe plus.");
+ }
- 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,
+ ];
}
-catch(UserException $e)
+else
{
- $form->addError($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;
}
// Formatage dates
@@ -74,307 +85,417 @@ if (isset($f->date_echeance))
// Génération factures, devis et cotisation
if ($f->type_facture != CERFA)
{
- 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 ":'');
- }
+ $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') ?: '€';
$total = Utils::money_format($f->total, ',', ' ');
+ $ttc = $plugin->getConfig('ttc') ? 'TTC' : 'HT';
- // 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';
-
- // Génération du contenu de la facture
- ob_start();
- echo <<
- Contenu - $doc
-
-
-
-
-
-
-
- Désignations
-
-
- Prix
-
-
-
-
-EOF;
-
- $i = 1;
- foreach($f->contenu as $k=>$v)
- {
- echo '';
- echo str_replace("\n", ' ', $v['designation']);
- echo ' ';
- echo Utils::money_format($v['prix'], ',', ' ') .' €';
- echo ' ';
- $i++;
- }
-
- echo <<
-
-
- Total Net à payer
- $total € ({$ttc})
-
-
-
-
- Détails
-
-
- $echeance
- $reglee
- Moyen de paiement : $moyen_paiement
-
- $footer
-
-
-
-EOF;
- if ($f->type_facture == DEVIS) {
- echo <<Bon pour accord, date et signature
-EOF;
- }
-
- $content = ob_get_clean();
-
+ 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 // Reçu de cotisation
- {
- $lieu = $plugin->getConfig('ville_asso');
+ else {
+ $adresse = $config->get('org_address_public') ?: $config->get('org_address') ?: '';
+ }
+
+ $logo = '';
+ if ($plugin->getConfig('logo') && ($logo_url = $config->fileURL('logo'))) {
+ $logo = sprintf(' ', $h($logo_url));
+ }
+
+ $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')) . '
';
+
+ $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(
+ '%s %s %s ',
+ $br($v['designation']),
+ $h(Utils::money_format($v['prix'], ',', ' ')),
+ $h($currency)
+ );
+ }
+
+ $payment_terms = trim((string) ($plugin->getConfig('payment_terms') ?: ''));
+ $invoice_note = trim((string) ($plugin->getConfig('invoice_note') ?: ''));
+ $status = '';
+
+ 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));
+ }
+
+ $approval = $f->type_facture == DEVIS ? 'Bon pour accord Date, 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 = <<
+
+
+
+ Désignation
+ Montant
+
+
+
+ $rows
+
+
+
+
+
+ $echeance_html
+ $terms
+ $moyen_html
+ $status
+
+
+ Total $ttc
+ $total {$h($currency)}
+ Net à payer
+
+
+$note
+$approval
+
+HTML;
+ }
+ else {
+ $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') ?? '');
- 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,
+ $content = <<
+
À {$h($lieu)}, le {$h($emission)},
Bonjour,
-
-
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;
+ 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'))}
+ 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.
+
+
+HTML;
}
- //-- Layout du document
+ $html_title = $h($doc . '_' . $emission);
+ $doc_html = $h($doc);
+ $txtemis = $h($emission_label . ' ' . $emission);
+ $txtdest = $h($recipient_label);
- ob_start();
- echo <<
-
+
- {$doc}_{$emission}
+ $html_title
-
- $txtemis
-
-
-
-
- $logo
- $asso
- $receveur
-
-
-
- $content
+
+
+
+
+
Émetteur
+ $asso_details
+
+
+
$txtdest
+ $receveur
+
+
+ $content
+
-EOF;
-
- $html = ob_get_clean();
-
+HTML;
} // Génération du CERFA
elseif ($f->type_facture == CERFA)
{
@@ -394,9 +515,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 b2f1b4d..4f5079f 100644
--- a/config.json
+++ b/config.json
@@ -1,6 +1,14 @@
{
"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 923b4eb..fc5ec68 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 81250a2..b72d912 100644
--- a/install.php
+++ b/install.php
@@ -1,8 +1,13 @@
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 66e7d75..f1fcbb9 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\Entities\Plugin;
+use Paheko\Plugins;
use Paheko\UserException;
use Paheko\Utils;
@@ -29,9 +29,12 @@ class Client
public function __construct()
{
- $plugin = new Plugin('facturation');
- $this->config['unique_client_name'] = $plugin->getConfig('unique_client_name') ?: false;
- $this->config['validate_cp'] = $plugin->getConfig('validate_cp') ?: false;
+ $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);
+ }
}
public function _checkFields(&$data)
@@ -102,8 +105,20 @@ class Client
{
$db = DB::getInstance();
- return $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation
+ $client = $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 e6f766e..1cdaaf5 100644
--- a/lib/Facture.php
+++ b/lib/Facture.php
@@ -6,9 +6,10 @@ 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
{
@@ -47,17 +48,52 @@ 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))
@@ -76,24 +112,11 @@ class Facture
switch($k)
{
case 'type_facture':
- if (!array_key_exists($datas[$k], $this->types)) {
+ $datas[$k] = (int) $datas[$k];
+
+ if (!array_key_exists($datas[$k], self::TYPES_NAMES)) {
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':
@@ -108,11 +131,11 @@ class Facture
}
break;
case 'date_emission':
- $datas[$k] = \DateTime::createFromFormat('!d/m/Y', $data)->format('Y-m-d');
+ $datas[$k] = $this->normalizeDate($data, $k);
break;
case 'date_echeance':
- $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'))
+ $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'))
{
throw new UserException("La date d'échéance est antérieure à la date d'émission ($data).");
}
@@ -134,12 +157,12 @@ class Facture
if (empty($r['designation']) && empty($r['prix']))
{
unset($datas[$k][$g]);
- unset($datas[$k]['prix']);
continue;
}
- elseif (! is_numeric($r['prix']) && empty($r['prix']))
+ elseif (!isset($r['prix']) || $r['prix'] === '')
{
- $datas[$k]['prix'] = 0;
+ $datas[$k][$g]['prix'] = 0;
+ $r['prix'] = 0;
}
elseif (empty($r['designation'])) {
throw new UserException("Une au moins des désignations est absente.");
@@ -251,7 +274,7 @@ class Facture
$type = ctype_digit($v) ? 'd' : 's';
return sprintf('%' . $match[1] . $type, $v);
}, $selpattern);
- $modele = '/^' . $prefixe . '\d+$/';
+ $modele = '/^' . preg_quote($prefixe, '/') . '\d+$/';
$numeros_filtres = array_filter($numeros, function($elem) use ($modele) {
return preg_match($modele, $elem);
}, 0);
@@ -292,6 +315,12 @@ 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)
{
@@ -320,15 +349,19 @@ class Facture
public function list($all = true): DynamicList
{
- $id_field = \Paheko\Users\DynamicFields::getNameFieldsSQL('u');
- $plugin_name = preg_replace('/^.*\/(\w+)\/$/', '${1}', \Paheko\PLUGIN_ADMIN_URL);
- $plugin = \Paheko\Plugins::get($plugin_name);
+ $db = DB::getInstance();
+ $id_field = DynamicFields::getNameFieldsSQL('u');
+ $plugin = Plugins::get('facturation');
+ $user_fields = DynamicFields::getInstance()->listAssocNames();
// adresse et ville peuvent être redéfinies dans la configuration du plugin
- $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; }
+ $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';
$columns = [
// Sélectionner cette colonne, mais ne pas la mettre dans la liste des colonnes
@@ -357,7 +390,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 CASE %s WHEN "" THEN "** ABSENT **" ELSE %s END ELSE c.nom END', $id_field, $id_field),
+ 'select' => sprintf('CASE WHEN receveur_membre THEN COALESCE(NULLIF(%s, \'\'), \'** ABSENT **\') ELSE c.nom END', $id_field),
],
'receveur_adresse' => [
// l'adresse peut être redéfinie dans la configuration du plugin
@@ -393,7 +426,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 AS u ON f.receveur_membre = 1 AND u.id = f.receveur_id
+ LEFT JOIN users_view 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) {
@@ -404,7 +437,7 @@ class Facture
$list = new DynamicList($columns, $tables, $where);
$list->orderBy('date_emission', true);
- $currency = Config::getInstance()->monnaie;
+ $currency = Config::getInstance()->currency;
$list->setModifier(function ($row) use ($currency) {
// Remplir la colonne virtuelle
@@ -460,7 +493,10 @@ class Facture
}
else // Si c'est un membre de l'asso
{
- throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients");
+ if (!\Paheko\Users\Users::exists((int) $id))
+ {
+ throw new UserException("Ce membre n'existe pas.");
+ }
}
$r = (array)DB::getInstance()->get('SELECT *, strftime(\'%s\', date_emission) AS date_emission,
@@ -492,7 +528,10 @@ class Facture
}
else // Si c'est un membre de l'asso
{
- throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients");
+ if (!\Paheko\Users\Users::exists((int) $id))
+ {
+ throw new UserException("Ce membre n'existe pas.");
+ }
}
return DB::getInstance()->test('plugin_facturation_factures', 'receveur_membre = ? AND receveur_id = ?', $base, $id);
diff --git a/plugin.ini b/plugin.ini
index 1916e45..16ef07f 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="zou ; adapté par Jean-Christophe Engel"
+author="Sur la base de Jean-Christophe Engel, amélioré par Jordan"
url="https://gitea.zaclys.com/lesanges/facturation"
-version="0.17.2"
+version="0.27"
menu=true
restrict_section="accounting"
restrict_level="read"
diff --git a/templates/_form.tpl b/templates/_form.tpl
index 4ab076e..31168c7 100644
--- a/templates/_form.tpl
+++ b/templates/_form.tpl
@@ -63,7 +63,7 @@
Document adressé à :
{if !empty($clients)}
- {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default=1 source=$doc}
+ {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default="membre" 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 ed7feeb..6b1ff1c 100644
--- a/templates/_js.tpl
+++ b/templates/_js.tpl
@@ -45,9 +45,15 @@
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;
+ });
}
}
@@ -76,9 +82,11 @@
}
const selclient = document.querySelector('#f_client');
- selclient.addEventListener("change", () => {
- modifierContact(selclient, '#f_contact_list', '#f_nom_contact');
- });
+ if (selclient) {
+ selclient.addEventListener("change", () => {
+ modifierContact(selclient, '#f_contact_list', '#f_nom_contact');
+ });
+ }
} ());
@@ -86,12 +94,18 @@
// 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 2c30760..da8a48a 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_USERS, $session::ACCESS_ADMIN)}
+ {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
{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 903f809..b9f20c9 100644
--- a/templates/config.tpl
+++ b/templates/config.tpl
@@ -38,14 +38,27 @@
{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=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}
+ {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}
@@ -60,7 +73,6 @@
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 3f9c761..62096a2 100644
--- a/templates/facture_supprimer.tpl
+++ b/templates/facture_supprimer.tpl
@@ -16,7 +16,7 @@
- {csrf_field key="delete_doc_"|cat:$doc.id}
+ {csrf_field key=$csrf_key}
diff --git a/upgrade.php b/upgrade.php
index f47ac27..ff81e15 100644
--- a/upgrade.php
+++ b/upgrade.php
@@ -2,7 +2,6 @@
namespace Paheko;
use Paheko\Plugin\Facturation\Facture;
-use Paheko\Entities\Files\File;
$db = DB::getInstance();
$old_version = $plugin->oldVersion();
@@ -25,11 +24,9 @@ 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)
{
- if(!$plugin->setConfig($k, $v))
- {
- throw new UserException('Erreur dans la conversion de la configuration pour la clé : '.$k);
- }
+ $plugin->setConfigProperty($k, $v);
}
+ $plugin->save();
$db->exec('DROP TABLE `plugin_facturation_config`;');
}
@@ -59,13 +56,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
);
@@ -81,13 +78,13 @@ EOT
// 0.6.0 -
if (version_compare($old_version, '0.6.0', '<'))
{
- define('DEVIS', 0);
- define('FACT', 1);
- define('CERFA', 2);
- define('COTIS', 3);
+ defined('DEVIS') || define('DEVIS', 0);
+ defined('FACT') || define('FACT', 1);
+ defined('CERFA') || define('CERFA', 2);
+ defined('COTIS') || define('COTIS', 3);
$facture = new Facture;
$r = $db->first('SELECT id, total FROM plugin_facturation_factures;');
- if (strpos($r->total,'.'))
+ if ($r && strpos((string) $r->total, '.') !== false)
{
// 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
@@ -137,19 +136,16 @@ 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', '<'))
{
- define('DEVIS', 0);
- define('FACT', 1);
- define('CERFA', 2);
- define('COTIS', 3);
+ defined('DEVIS') || define('DEVIS', 0);
+ defined('FACT') || define('FACT', 1);
+ defined('CERFA') || define('CERFA', 2);
+ defined('COTIS') || define('COTIS', 3);
$facture = new Facture;
$db->exec(<<setConfig('ttc', false);
+ $plugin->setConfigProperty('ttc', false);
+ $plugin->save();
}
// 0.8.1 - Signal menu item
@@ -217,10 +214,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,
@@ -261,10 +258,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,
@@ -293,13 +290,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,
@@ -322,3 +319,64 @@ 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();
+}
From fffbfcac93947bd50fae88fdfde97be3a8271b50 Mon Sep 17 00:00:00 2001
From: Jordan Pelletier
Date: Thu, 2 Jul 2026 13:38:15 +0200
Subject: [PATCH 2/5] =?UTF-8?q?Correction=20erreurs,=20s=C3=A9curisation?=
=?UTF-8?q?=20code,=20personnalisation=20pr=C3=A9sentation=20(Jordan)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG | 10 +
admin/_facture_common.php | 114 +++--
admin/_inc.php | 38 +-
admin/client_supprimer.php | 2 +-
admin/config.php | 34 +-
admin/facture_supprimer.php | 7 +-
admin/pdf.php | 733 +++++++++++++++++++-------------
config.json | 10 +-
data/schema.sql | 16 +-
install.php | 9 +-
lib/Client.php | 25 +-
lib/Facture.php | 111 +++--
plugin.ini | 4 +-
templates/_form.tpl | 2 +-
templates/_js.tpl | 20 +-
templates/clients.tpl | 2 +-
templates/config.tpl | 20 +-
templates/facture_supprimer.tpl | 2 +-
upgrade.php | 134 ++++--
19 files changed, 822 insertions(+), 471 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index e4e5dae..d6a0e9f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,13 @@
+0.25
+- Options d'affichage du rendu facture (Jason)
+0.19
+- Réparation des schémas partiellement migrés (Jason)
+0.18
+- Personnalisation du rendu des factures (Jason)
+0.17
+- Ajout archivage factures
+0.15
+- Ajout champ contact à la fiche client
0.12
- Ajout Mollie à la table moyens de paiement
0.11
diff --git a/admin/_facture_common.php b/admin/_facture_common.php
index 9fbb19c..2be8c6b 100644
--- a/admin/_facture_common.php
+++ b/admin/_facture_common.php
@@ -4,30 +4,8 @@ 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 Exception('blabla illegal call'); // Fix: exception type?
+ throw new \LogicException('Appel interne invalide du formulaire de document.');
} else {
$target = ($target === 'new' ? true:false);
}
@@ -35,7 +13,6 @@ 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();
@@ -69,21 +46,18 @@ 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 ($client, &$data, $form)
+ function () use (&$data, $form)
{
try
{
- if ( count(f('designation')) !== count(f('prix')) )
- {
- throw new UserException('Nombre de désignations et de prix reçus différent.');
- }
+ $type = (int) f('type');
$data = [
'numero' => f('numero_facture'),
@@ -93,37 +67,51 @@ $form->runIf(f('save') && !$form->hasErrors(),
'archivee' => f('archivee') == 1?1:0,
'moyen_paiement' => f('moyen_paiement'),
'nom_contact' => f('nom_contact'),
- 'toto' => 0
+ 'numero_commande' => f('numero_commande') ?: null,
+ 'reference_acheteur' => f('reference_acheteur') ?: null,
];
- $data['type_facture'] = f('type');
- if (in_array(f('type'), [DEVIS, FACT]))
+
+ $data['type_facture'] = $type;
+
+ if (in_array($type, [DEVIS, FACT], true))
{
- foreach(f('designation') as $k=>$value)
+ $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)
{
- if (empty($value) && f('prix')[$k] != null) {
- throw new UserException("Il manque la désignation de la ligne " . $k+1 . " !!");
+ $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) . " !!");
}
- 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) {
+ 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 === '')) {
continue;
}
+ $prix_ligne = Utils::moneyToInteger($prix_ligne);
$data['contenu'][$k]['designation'] = $value;
- $data['contenu'][$k]['prix'] = Utils::moneyToInteger(f('prix')[$k]);
- $data['toto'] += Utils::moneyToInteger(f('prix')[$k]);
+ $data['contenu'][$k]['prix'] = $prix_ligne;
+ $total += $prix_ligne;
}
- $data['total'] = $data['toto'];
- unset($data['toto']);
+
+ $data['total'] = $total;
+
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 ( f('type') == CERFA )
+ elseif ($type === CERFA)
{
$data['moyen_paiement'] = f('moyen_paiement_cerfa');
$data['contenu'] = [
@@ -131,8 +119,12 @@ $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;
@@ -157,18 +149,20 @@ $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 ($radio, $fields, $facture, $form)
+ function () use (&$data, &$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);
@@ -183,8 +177,12 @@ $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'],
@@ -264,7 +262,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;
@@ -284,9 +282,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;
- $doc['numero_commande'] = $f->numero_commande;
- $doc['reference_acheteur'] = $f->reference_acheteur;
+ $doc['nom_contact'] = $f->nom_contact ?? null;
+ $doc['numero_commande'] = $f->numero_commande ?? null;
+ $doc['reference_acheteur'] = $f->reference_acheteur ?? null;
/* modif DD -- CERFA -------------------------------------- */
if ( $f->type_facture == CERFA ) {
$doc['total'] = $f->total;
@@ -346,11 +344,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', 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('users', iterator_to_array(Users\Users::iterateAssocByCategory()));
$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')]);
+$tpl->assign('number_pattern', PATTERNS_LIST[$plugin->getConfig('pattern')] ?? PATTERNS_LIST['']);
if ($target) {
$tpl->display(PLUGIN_ROOT . '/templates/facture_ajouter.tpl');
diff --git a/admin/_inc.php b/admin/_inc.php
index f11fc22..b0f7f07 100644
--- a/admin/_inc.php
+++ b/admin/_inc.php
@@ -22,6 +22,42 @@ 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;
@@ -63,7 +99,7 @@ $tpl->register_function('money_fac', function (array $params)
$current_value = htmlspecialchars($current_value, ENT_QUOTES, 'UTF-8');
}
- $currency = Config::getInstance()->get('monnaie');
+ $currency = Config::getInstance()->get('currency');
return sprintf('%s ', $name, $current_value, $currency);
}
);
diff --git a/admin/client_supprimer.php b/admin/client_supprimer.php
index 64a3395..2e8244f 100644
--- a/admin/client_supprimer.php
+++ b/admin/client_supprimer.php
@@ -12,7 +12,7 @@ $id = (int) qg('id');
$c = $client->get($id);
-if (!$client)
+if (!$c)
{
throw new UserException("Ce client n'existe pas.");
}
diff --git a/admin/config.php b/admin/config.php
index 638cd56..e6f57fd 100644
--- a/admin/config.php
+++ b/admin/config.php
@@ -11,6 +11,17 @@ $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'));
@@ -22,6 +33,14 @@ $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'));
@@ -36,8 +55,21 @@ $form->runIf('save', function () use ($plugin) {
$tpl->assign('ok', qg('ok') !== null);
-$tpl->assign('conf', $plugin->getConfig());
+$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('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 4711ddb..7640466 100644
--- a/admin/facture_supprimer.php
+++ b/admin/facture_supprimer.php
@@ -12,11 +12,13 @@ $id = (int) qg('id');
$f = $facture->get($id);
-if (!$client)
+if (!$f)
{
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)
{
@@ -28,7 +30,8 @@ $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 d953414..6b32848 100644
--- a/admin/pdf.php
+++ b/admin/pdf.php
@@ -4,11 +4,27 @@ 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((string) $value, ENT_QUOTES, 'UTF-8');
+};
+$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))
@@ -18,47 +34,42 @@ if (!$f = $facture->get($id))
$moyen_paiement = $facture->getMoyenPaiement($f->moyen_paiement);
-// Récupération infos membre
-try
+// Récupération infos membre/client dans un objet normalisé pour le rendu
+if ($f->receveur_membre)
{
- 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 **";
- }
+ $user = Users\Users::get($f->receveur_id);
- // 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; }
+ if (!$user) {
+ throw new UserException("Ce membre n'existe plus.");
+ }
- 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,
+ ];
}
-catch(UserException $e)
+else
{
- $form->addError($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;
}
// Formatage dates
@@ -74,307 +85,417 @@ if (isset($f->date_echeance))
// Génération factures, devis et cotisation
if ($f->type_facture != CERFA)
{
- 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 ":'');
- }
+ $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') ?: '€';
$total = Utils::money_format($f->total, ',', ' ');
+ $ttc = $plugin->getConfig('ttc') ? 'TTC' : 'HT';
- // 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';
-
- // Génération du contenu de la facture
- ob_start();
- echo <<
- Contenu - $doc
-
-
-
-
-
-
-
- Désignations
-
-
- Prix
-
-
-
-
-EOF;
-
- $i = 1;
- foreach($f->contenu as $k=>$v)
- {
- echo '';
- echo str_replace("\n", ' ', $v['designation']);
- echo ' ';
- echo Utils::money_format($v['prix'], ',', ' ') .' €';
- echo ' ';
- $i++;
- }
-
- echo <<
-
-
- Total Net à payer
- $total € ({$ttc})
-
-
-
-
- Détails
-
-
- $echeance
- $reglee
- Moyen de paiement : $moyen_paiement
-
- $footer
-
-
-
-EOF;
- if ($f->type_facture == DEVIS) {
- echo <<Bon pour accord, date et signature
-EOF;
- }
-
- $content = ob_get_clean();
-
+ 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 // Reçu de cotisation
- {
- $lieu = $plugin->getConfig('ville_asso');
+ else {
+ $adresse = $config->get('org_address_public') ?: $config->get('org_address') ?: '';
+ }
+
+ $logo = '';
+ if ($plugin->getConfig('logo') && ($logo_url = $config->fileURL('logo'))) {
+ $logo = sprintf(' ', $h($logo_url));
+ }
+
+ $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')) . '
';
+
+ $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(
+ '%s %s %s ',
+ $br($v['designation']),
+ $h(Utils::money_format($v['prix'], ',', ' ')),
+ $h($currency)
+ );
+ }
+
+ $payment_terms = trim((string) ($plugin->getConfig('payment_terms') ?: ''));
+ $invoice_note = trim((string) ($plugin->getConfig('invoice_note') ?: ''));
+ $status = '';
+
+ 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));
+ }
+
+ $approval = $f->type_facture == DEVIS ? 'Bon pour accord Date, 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 = <<
+
+
+
+ Désignation
+ Montant
+
+
+
+ $rows
+
+
+
+
+
+ $echeance_html
+ $terms
+ $moyen_html
+ $status
+
+
+ Total $ttc
+ $total {$h($currency)}
+ Net à payer
+
+
+$note
+$approval
+
+HTML;
+ }
+ else {
+ $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') ?? '');
- 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,
+ $content = <<
+
À {$h($lieu)}, le {$h($emission)},
Bonjour,
-
-
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;
+ 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'))}
+ 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.
+
+
+HTML;
}
- //-- Layout du document
+ $html_title = $h($doc . '_' . $emission);
+ $doc_html = $h($doc);
+ $txtemis = $h($emission_label . ' ' . $emission);
+ $txtdest = $h($recipient_label);
- ob_start();
- echo <<
-
+
- {$doc}_{$emission}
+ $html_title
-
- $txtemis
-
-
-
-
- $logo
- $asso
- $receveur
-
-
-
- $content
+
+
+
+
+
Émetteur
+ $asso_details
+
+
+
$txtdest
+ $receveur
+
+
+ $content
+
-EOF;
-
- $html = ob_get_clean();
-
+HTML;
} // Génération du CERFA
elseif ($f->type_facture == CERFA)
{
@@ -394,9 +515,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 b2f1b4d..4f5079f 100644
--- a/config.json
+++ b/config.json
@@ -1,6 +1,14 @@
{
"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 923b4eb..fc5ec68 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 81250a2..b72d912 100644
--- a/install.php
+++ b/install.php
@@ -1,8 +1,13 @@
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 66e7d75..f1fcbb9 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\Entities\Plugin;
+use Paheko\Plugins;
use Paheko\UserException;
use Paheko\Utils;
@@ -29,9 +29,12 @@ class Client
public function __construct()
{
- $plugin = new Plugin('facturation');
- $this->config['unique_client_name'] = $plugin->getConfig('unique_client_name') ?: false;
- $this->config['validate_cp'] = $plugin->getConfig('validate_cp') ?: false;
+ $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);
+ }
}
public function _checkFields(&$data)
@@ -102,8 +105,20 @@ class Client
{
$db = DB::getInstance();
- return $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation
+ $client = $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 e6f766e..1cdaaf5 100644
--- a/lib/Facture.php
+++ b/lib/Facture.php
@@ -6,9 +6,10 @@ 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
{
@@ -47,17 +48,52 @@ 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))
@@ -76,24 +112,11 @@ class Facture
switch($k)
{
case 'type_facture':
- if (!array_key_exists($datas[$k], $this->types)) {
+ $datas[$k] = (int) $datas[$k];
+
+ if (!array_key_exists($datas[$k], self::TYPES_NAMES)) {
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':
@@ -108,11 +131,11 @@ class Facture
}
break;
case 'date_emission':
- $datas[$k] = \DateTime::createFromFormat('!d/m/Y', $data)->format('Y-m-d');
+ $datas[$k] = $this->normalizeDate($data, $k);
break;
case 'date_echeance':
- $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'))
+ $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'))
{
throw new UserException("La date d'échéance est antérieure à la date d'émission ($data).");
}
@@ -134,12 +157,12 @@ class Facture
if (empty($r['designation']) && empty($r['prix']))
{
unset($datas[$k][$g]);
- unset($datas[$k]['prix']);
continue;
}
- elseif (! is_numeric($r['prix']) && empty($r['prix']))
+ elseif (!isset($r['prix']) || $r['prix'] === '')
{
- $datas[$k]['prix'] = 0;
+ $datas[$k][$g]['prix'] = 0;
+ $r['prix'] = 0;
}
elseif (empty($r['designation'])) {
throw new UserException("Une au moins des désignations est absente.");
@@ -251,7 +274,7 @@ class Facture
$type = ctype_digit($v) ? 'd' : 's';
return sprintf('%' . $match[1] . $type, $v);
}, $selpattern);
- $modele = '/^' . $prefixe . '\d+$/';
+ $modele = '/^' . preg_quote($prefixe, '/') . '\d+$/';
$numeros_filtres = array_filter($numeros, function($elem) use ($modele) {
return preg_match($modele, $elem);
}, 0);
@@ -292,6 +315,12 @@ 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)
{
@@ -320,15 +349,19 @@ class Facture
public function list($all = true): DynamicList
{
- $id_field = \Paheko\Users\DynamicFields::getNameFieldsSQL('u');
- $plugin_name = preg_replace('/^.*\/(\w+)\/$/', '${1}', \Paheko\PLUGIN_ADMIN_URL);
- $plugin = \Paheko\Plugins::get($plugin_name);
+ $db = DB::getInstance();
+ $id_field = DynamicFields::getNameFieldsSQL('u');
+ $plugin = Plugins::get('facturation');
+ $user_fields = DynamicFields::getInstance()->listAssocNames();
// adresse et ville peuvent être redéfinies dans la configuration du plugin
- $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; }
+ $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';
$columns = [
// Sélectionner cette colonne, mais ne pas la mettre dans la liste des colonnes
@@ -357,7 +390,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 CASE %s WHEN "" THEN "** ABSENT **" ELSE %s END ELSE c.nom END', $id_field, $id_field),
+ 'select' => sprintf('CASE WHEN receveur_membre THEN COALESCE(NULLIF(%s, \'\'), \'** ABSENT **\') ELSE c.nom END', $id_field),
],
'receveur_adresse' => [
// l'adresse peut être redéfinie dans la configuration du plugin
@@ -393,7 +426,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 AS u ON f.receveur_membre = 1 AND u.id = f.receveur_id
+ LEFT JOIN users_view 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) {
@@ -404,7 +437,7 @@ class Facture
$list = new DynamicList($columns, $tables, $where);
$list->orderBy('date_emission', true);
- $currency = Config::getInstance()->monnaie;
+ $currency = Config::getInstance()->currency;
$list->setModifier(function ($row) use ($currency) {
// Remplir la colonne virtuelle
@@ -460,7 +493,10 @@ class Facture
}
else // Si c'est un membre de l'asso
{
- throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients");
+ if (!\Paheko\Users\Users::exists((int) $id))
+ {
+ throw new UserException("Ce membre n'existe pas.");
+ }
}
$r = (array)DB::getInstance()->get('SELECT *, strftime(\'%s\', date_emission) AS date_emission,
@@ -492,7 +528,10 @@ class Facture
}
else // Si c'est un membre de l'asso
{
- throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients");
+ if (!\Paheko\Users\Users::exists((int) $id))
+ {
+ throw new UserException("Ce membre n'existe pas.");
+ }
}
return DB::getInstance()->test('plugin_facturation_factures', 'receveur_membre = ? AND receveur_id = ?', $base, $id);
diff --git a/plugin.ini b/plugin.ini
index 1916e45..16ef07f 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="zou ; adapté par Jean-Christophe Engel"
+author="Sur la base de Jean-Christophe Engel, amélioré par Jordan"
url="https://gitea.zaclys.com/lesanges/facturation"
-version="0.17.2"
+version="0.27"
menu=true
restrict_section="accounting"
restrict_level="read"
diff --git a/templates/_form.tpl b/templates/_form.tpl
index 4ab076e..31168c7 100644
--- a/templates/_form.tpl
+++ b/templates/_form.tpl
@@ -63,7 +63,7 @@
Document adressé à :
{if !empty($clients)}
- {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default=1 source=$doc}
+ {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default="membre" 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 ed7feeb..6b1ff1c 100644
--- a/templates/_js.tpl
+++ b/templates/_js.tpl
@@ -45,9 +45,15 @@
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;
+ });
}
}
@@ -76,9 +82,11 @@
}
const selclient = document.querySelector('#f_client');
- selclient.addEventListener("change", () => {
- modifierContact(selclient, '#f_contact_list', '#f_nom_contact');
- });
+ if (selclient) {
+ selclient.addEventListener("change", () => {
+ modifierContact(selclient, '#f_contact_list', '#f_nom_contact');
+ });
+ }
} ());
@@ -86,12 +94,18 @@
// 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 2c30760..da8a48a 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_USERS, $session::ACCESS_ADMIN)}
+ {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
{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 903f809..b9f20c9 100644
--- a/templates/config.tpl
+++ b/templates/config.tpl
@@ -38,14 +38,27 @@
{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=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}
+ {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}
@@ -60,7 +73,6 @@
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 3f9c761..62096a2 100644
--- a/templates/facture_supprimer.tpl
+++ b/templates/facture_supprimer.tpl
@@ -16,7 +16,7 @@
- {csrf_field key="delete_doc_"|cat:$doc.id}
+ {csrf_field key=$csrf_key}
diff --git a/upgrade.php b/upgrade.php
index f47ac27..ff81e15 100644
--- a/upgrade.php
+++ b/upgrade.php
@@ -2,7 +2,6 @@
namespace Paheko;
use Paheko\Plugin\Facturation\Facture;
-use Paheko\Entities\Files\File;
$db = DB::getInstance();
$old_version = $plugin->oldVersion();
@@ -25,11 +24,9 @@ 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)
{
- if(!$plugin->setConfig($k, $v))
- {
- throw new UserException('Erreur dans la conversion de la configuration pour la clé : '.$k);
- }
+ $plugin->setConfigProperty($k, $v);
}
+ $plugin->save();
$db->exec('DROP TABLE `plugin_facturation_config`;');
}
@@ -59,13 +56,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
);
@@ -81,13 +78,13 @@ EOT
// 0.6.0 -
if (version_compare($old_version, '0.6.0', '<'))
{
- define('DEVIS', 0);
- define('FACT', 1);
- define('CERFA', 2);
- define('COTIS', 3);
+ defined('DEVIS') || define('DEVIS', 0);
+ defined('FACT') || define('FACT', 1);
+ defined('CERFA') || define('CERFA', 2);
+ defined('COTIS') || define('COTIS', 3);
$facture = new Facture;
$r = $db->first('SELECT id, total FROM plugin_facturation_factures;');
- if (strpos($r->total,'.'))
+ if ($r && strpos((string) $r->total, '.') !== false)
{
// 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
@@ -137,19 +136,16 @@ 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', '<'))
{
- define('DEVIS', 0);
- define('FACT', 1);
- define('CERFA', 2);
- define('COTIS', 3);
+ defined('DEVIS') || define('DEVIS', 0);
+ defined('FACT') || define('FACT', 1);
+ defined('CERFA') || define('CERFA', 2);
+ defined('COTIS') || define('COTIS', 3);
$facture = new Facture;
$db->exec(<<setConfig('ttc', false);
+ $plugin->setConfigProperty('ttc', false);
+ $plugin->save();
}
// 0.8.1 - Signal menu item
@@ -217,10 +214,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,
@@ -261,10 +258,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,
@@ -293,13 +290,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,
@@ -322,3 +319,64 @@ 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();
+}
From 3d966e4bf69618892eedc63d2d04357ee40e924a Mon Sep 17 00:00:00 2001
From: Jean-Christophe Engel
Date: Thu, 2 Jul 2026 14:20:57 +0200
Subject: [PATCH 3/5] =?UTF-8?q?Normalisation=20pr=C3=A9sentation=20Changel?=
=?UTF-8?q?og?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG | 97 --------------------------------
CHANGELOG.md | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 156 insertions(+), 97 deletions(-)
delete mode 100644 CHANGELOG
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index d6a0e9f..0000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,97 +0,0 @@
-0.25
-- Options d'affichage du rendu facture (Jason)
-0.19
-- Réparation des schémas partiellement migrés (Jason)
-0.18
-- Personnalisation du rendu des factures (Jason)
-0.17
-- Ajout archivage factures
-0.15
-- Ajout champ contact à la fiche client
-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
new file mode 100644
index 0000000..5acf4b5
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,156 @@
+# Changelog
+
+## v0.25 - 2026-05-15 - @Jason
+### Added
+- Options d'affichage du rendu facture
+
+## v0.19 - 2026-05-15 - @Jason
+### Fixed
+- Réparation des schémas partiellement migrés
+
+## v0.18 - 2026-05-15 - @Jason
+### Changed
+- améliorations proposées par Jason (jojodavo@gmail.com)
+### 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
From 061fb13e5dcfedd59b3d1eeda39b0bfe2a88464a Mon Sep 17 00:00:00 2001
From: Jean-Christophe Engel
Date: Thu, 2 Jul 2026 18:46:26 +0200
Subject: [PATCH 4/5] =?UTF-8?q?Conserver=20les=20tags=20html=20dans=20les?=
=?UTF-8?q?=20libell=C3=A9s?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
admin/pdf.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/admin/pdf.php b/admin/pdf.php
index 6b32848..49ff41d 100644
--- a/admin/pdf.php
+++ b/admin/pdf.php
@@ -9,7 +9,10 @@ f(['id' => 'required|numeric']);
$id = (int) qg('id');
$sign_tag = UserTemplate\Functions::signature();
$h = static function ($value): string {
- return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
+ return htmlspecialchars_decode(
+ htmlentities($value, ENT_NOQUOTES, 'UTF-8', false),
+ ENT_NOQUOTES
+ );
};
$br = static function ($value) use ($h): string {
return nl2br($h($value));
From a17d7b09cedabef6b1998fc219a536e5203a88aa Mon Sep 17 00:00:00 2001
From: Jean-Christophe Engel
Date: Thu, 2 Jul 2026 20:30:59 +0200
Subject: [PATCH 5/5] =?UTF-8?q?Mise-=C3=A0-jour=20plugin.ini=20et=20CHANGE?=
=?UTF-8?q?LOG.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 12 ++++++++----
plugin.ini | 2 +-
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5acf4b5..cbd5b07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,16 +1,20 @@
# Changelog
-## v0.25 - 2026-05-15 - @Jason
+## 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
+## v0.19 - 2026-05-15 - Jason Pelletier
### Fixed
- Réparation des schémas partiellement migrés
-## v0.18 - 2026-05-15 - @Jason
+## v0.18 - 2026-05-15 - Jason Pelletier
### Changed
-- améliorations proposées par Jason (jojodavo@gmail.com)
+- améliorations proposées par Jason Pelletier
### Added
- Personnalisation du rendu des factures
diff --git a/plugin.ini b/plugin.ini
index 16ef07f..b90c87f 100644
--- a/plugin.ini
+++ b/plugin.ini
@@ -2,7 +2,7 @@ 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"
url="https://gitea.zaclys.com/lesanges/facturation"
-version="0.27"
+version="0.27.2"
menu=true
restrict_section="accounting"
restrict_level="read"