Compare commits

..

No commits in common. "master" and "v0.17.2" have entirely different histories.

20 changed files with 541 additions and 958 deletions

87
CHANGELOG Normal file
View file

@ -0,0 +1,87 @@
0.12
- Ajout Mollie à la table moyens de paiement
0.11
- Changement mention finale pour devis
0.9
- Ajout possibilité choisir champs identité et adresse membre
0.8.8
- correction typo
0.8.7
- correction typo
0.8.6
- Correction erreur si pas de prix saisi
0.8.5
- Ajout numéro SIREN/SIRET pour les clients
- Ajout pagination liste factures
0.8.4
- Correction bug si identité définie par plusieurs champs
- Correction bug oubli choix receveur
- Mise-à-jour Changelog
0.8.3 :
- Mise-à-jour README
0.8.2 :
- Tri des membres en ordre alphabétique
- Élimination de plusieurs bugs
0.8.1 :
- reprise du code par jce (lesanges@zaclys.net ; https://git.roflcopter.fr/lesanges/paheko-plugin-facturation)
- Achèvement migration vers Paheko 1.3
0.7.4 :
- Corrections de dépréciations PHP, le plugin être compatible PHP8.2
- Quelques problèmes réglés de formulaires invalidables
- Nettoyage du code et factorization des facture_{ajouter,modifier}.php
0.7.3 :
- Règle le problème de formulaire bloqué que je croyais avoir résolu la dernière fois :)
0.7.2 :
- Petite correction du moyen de paiement qui n'était pas retenu à cause d'un doublon dans le formulaire.
0.7.1 :
- Ajout d'une case dans la config pour choisir si les factures sont TTC ou HT
- Le formulaire de document était cassé à cause d'un champs obligatoire invisible
0.7.0 :
- Modernisation des formulaires, de la fiche client
- Liste dynamiques des documents et des clients + export CSV/ODS
- Numérotation automatique des documents
- Ajout du bouton Dupliquer sur les documents
- Divers fixes
- Changements de la gestion des CERFA :
- organisation du champ `contenu` pour gérer les paramètres de CERFA sous la forme {"forme":"3","nature":"1","texte":"1"}
- ajout d'une table `plugin_facturation_txt_cerfa` pour stocker les textes explicatifs additionnels
- formulaires adaptés aux CERFA pour permettre le choix de la forme, de la nature, du mode de versement et du texte explicatif éventuel
0.6.0 :
- Génération des PDF par Garradin, on se débarasse de mPDF et l'installation du plugin devient standard
- Fix #32 (prix total affiché incorrect), en stockant les prix sous formes d'entiers à la manière de Garradin
- Possibilité de supprimer un document
- Fix #34, problème sélection membre
0.5.0 :
Mise à jour rapide du plugin pour Garradin 1.1.
La gestion de la signature dans les reçus fiscaux a été désactivés pour l'instant (changements de méthodes avec Garradin 1.1, flemme de m'y pencher pour le moment)
0.4.1 :
Release mineure qui corrige un problème qui apparaît, selon la configuration de PHP (?), lors de la migration depuis une version précédente.
0.4.0 :
Cette version a pour principal but d'être compatible avec Garradin 1.0 et de ne pas empêcher la mise à jour d'une ancienne installation comprenant une ancienne version du plugin.
Le formulaire de création/édition de document a été modifié pour simplifier l'expérience utilisateur : tout les types de documents se font depuis le même formulaire, suivi des évolutions graphiques/ergonomiques de Garradin.
0.3.0 :
- Ajouts des reçus fiscaux et des reçus sur des cotisations :
- Génération du cerfa pour reçu fiscaux, paramètres disponibles dans la config
- Ajout d'une page d'aide
- L'identité des destinataires membres est maintenant définie selon la configuration de Garradin, et non plus de manière statique sur la colonne "nom"
- Suppression de la classe Facturation\Config pour utiliser celle que propose Garradin
- Ajout du champs de numéro RNA
0.2.0 :
- Stockage des désignations des documents changé de serialize à json_decode
- Petites améliorations sur les formulaires

View file

@ -1,160 +0,0 @@
# Changelog
## v0.27.2 - 2026-07-02
### Changed
- Conserver les tags html dans les libellés
## v0.25 - 2026-05-15 - Jason Pelletier <jojodavo@gmail.com>
### Added
- Options d'affichage du rendu facture
## v0.19 - 2026-05-15 - Jason Pelletier <jojodavo@gmail.com>
### Fixed
- Réparation des schémas partiellement migrés
## v0.18 - 2026-05-15 - Jason Pelletier <jojodavo@gmail.com>
### Changed
- améliorations proposées par Jason Pelletier <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

View file

@ -4,8 +4,30 @@ namespace Paheko;
require_once __DIR__ . '/_inc.php'; 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'])) { if (!isset($target) || !in_array( $target, ['new', 'edit'])) {
throw new \LogicException('Appel interne invalide du formulaire de document.'); throw new Exception('blabla illegal call'); // Fix: exception type?
} else { } else {
$target = ($target === 'new' ? true:false); $target = ($target === 'new' ? true:false);
} }
@ -13,6 +35,7 @@ if (!isset($target) || !in_array( $target, ['new', 'edit'])) {
$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE); $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE);
use Paheko\DB; use Paheko\DB;
use stdClass;
use const \Paheko\Plugin\Facturation\PATTERNS_LIST; use const \Paheko\Plugin\Facturation\PATTERNS_LIST;
$db = DB::getInstance(); $db = DB::getInstance();
@ -46,18 +69,21 @@ if ( !$target ) {
throw new UserException("Ce document n'existe pas."); 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"); throw new UserException("Ce document est archivé, vous n'avez pas le droit de le modifier");
} }
// Traitement // Traitement
$data=[]; $data=[];
$form->runIf(f('save') && !$form->hasErrors(), $form->runIf(f('save') && !$form->hasErrors(),
function () use (&$data, $form) function () use ($client, &$data, $form)
{ {
try try
{ {
$type = (int) f('type'); if ( count(f('designation')) !== count(f('prix')) )
{
throw new UserException('Nombre de désignations et de prix reçus différent.');
}
$data = [ $data = [
'numero' => f('numero_facture'), 'numero' => f('numero_facture'),
@ -67,51 +93,37 @@ $form->runIf(f('save') && !$form->hasErrors(),
'archivee' => f('archivee') == 1?1:0, 'archivee' => f('archivee') == 1?1:0,
'moyen_paiement' => f('moyen_paiement'), 'moyen_paiement' => f('moyen_paiement'),
'nom_contact' => f('nom_contact'), 'nom_contact' => f('nom_contact'),
'numero_commande' => f('numero_commande') ?: null, 'toto' => 0
'reference_acheteur' => f('reference_acheteur') ?: null,
]; ];
$data['type_facture'] = f('type');
$data['type_facture'] = $type; if (in_array(f('type'), [DEVIS, FACT]))
if (in_array($type, [DEVIS, FACT], true))
{ {
$designations = (array) f('designation'); foreach(f('designation') as $k=>$value)
$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)
{ {
$prix_ligne = $prix[$k] ?? null; if (empty($value) && f('prix')[$k] != null) {
throw new UserException("Il manque la désignation de la ligne " . $k+1 . " !!");
if (empty($value) && $prix_ligne !== null && $prix_ligne !== '') {
throw new UserException("Il manque la désignation de la ligne " . ($k + 1) . " !!");
} }
elseif ($value != '' && ($prix_ligne === null || $prix_ligne === '')) { elseif ($value != '' && f('prix')[$k] == null) {
throw new UserException('Il manque le prix sur la ligne '. ($k + 1) . ' !!'); throw new UserException('Il manque le prix sur la ligne '. $k+1 . ' !!');
} } elseif (empty($value) && f('prix')[$k] == null) {
elseif (empty($value) && ($prix_ligne === null || $prix_ligne === '')) {
continue; continue;
} }
$prix_ligne = Utils::moneyToInteger($prix_ligne);
$data['contenu'][$k]['designation'] = $value; $data['contenu'][$k]['designation'] = $value;
$data['contenu'][$k]['prix'] = $prix_ligne; $data['contenu'][$k]['prix'] = Utils::moneyToInteger(f('prix')[$k]);
$total += $prix_ligne; $data['toto'] += Utils::moneyToInteger(f('prix')[$k]);
} }
$data['total'] = $data['toto'];
$data['total'] = $total; unset($data['toto']);
if (! isset($data['contenu'])) { if (! isset($data['contenu'])) {
throw new UserException("Aucune désignation ni aucun prix saisi !!"); throw new UserException("Aucune désignation ni aucun prix saisi !!");
} }
if (f('type') == FACT) {
$data['numero_commande'] = f('numero_commande');
$data['reference_acheteur'] = f('reference_acheteur');
}
} }
elseif ($type === CERFA) elseif ( f('type') == CERFA )
{ {
$data['moyen_paiement'] = f('moyen_paiement_cerfa'); $data['moyen_paiement'] = f('moyen_paiement_cerfa');
$data['contenu'] = [ $data['contenu'] = [
@ -119,12 +131,8 @@ $form->runIf(f('save') && !$form->hasErrors(),
'nature' => f('nature_don'), 'nature' => f('nature_don'),
'texte' => f('texte_don')]; 'texte' => f('texte_don')];
$data['total'] = Utils::moneyToInteger(f('total')); $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') if (f('base_receveur') == 'client')
{ {
$data['receveur_membre'] = 0; $data['receveur_membre'] = 0;
@ -149,20 +157,18 @@ $form->runIf(f('save') && !$form->hasErrors(),
}, $csrf_key); }, $csrf_key);
$form->runIf(f('select_cotis') && !$form->hasErrors(), $form->runIf(f('select_cotis') && !$form->hasErrors(),
function () use (&$step) function () use ($step)
{ {
$step = true; $step = true;
}, 'add_cotis_1'); }, 'add_cotis_1');
$form->runIf(f('add_cotis') && !$form->hasErrors(), $form->runIf(f('add_cotis') && !$form->hasErrors(),
function () use (&$data, &$radio, $fields, $facture, $form) function () use ($radio, $fields, $facture, $form)
{ {
$radio['type'] = f('cotisation'); $radio['type'] = f('cotisation');
try try
{ {
$num = (int) str_replace('cotis_', '', $radio['type']); $num = (int) str_replace('cotis_', '', $radio['type']);
$cotis = [];
foreach($fields as $field) foreach($fields as $field)
{ {
$cotis[$field] = f($field.'_'.$num); $cotis[$field] = f($field.'_'.$num);
@ -177,12 +183,8 @@ $form->runIf(f('add_cotis') && !$form->hasErrors(),
'receveur_membre' => 1, 'receveur_membre' => 1,
'receveur_id' => f('membre_cotis'), 'receveur_id' => f('membre_cotis'),
'date_emission' => f('date_emission'), 'date_emission' => f('date_emission'),
'date_echeance' => f('date_echeance') ?: f('date_emission'),
'reglee' => 1,
'archivee' => 0,
'moyen_paiement' => 'AU', 'moyen_paiement' => 'AU',
'total' => $r->paid_amount ?? $r->amount, 'total' => $r->paid_amount ?? $r->amount,
'nom_contact' => null,
'contenu' => ['id' => $cotis['id'], 'contenu' => ['id' => $cotis['id'],
'intitule' => $cotis['label'], 'intitule' => $cotis['label'],
'souscription' => $cotis['date'], 'souscription' => $cotis['date'],
@ -262,7 +264,7 @@ if ($target)
} // ... s'il est défini dans le document copié } // ... s'il est défini dans le document copié
elseif (isset($doc['type'])) { elseif (isset($doc['type'])) {
$radio['type'] = $doc['type']; $radio['type'] = $doc['type'];
} // ... ou par défaut } // ... ou par défaut
else else
{ {
$radio['type'] = FACT; $radio['type'] = FACT;
@ -282,9 +284,9 @@ else
$doc['date_emission'] = f('date_emission') ?: $f->date_emission; $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['date_echeance'] = f('date_echeance')?: $f->date_echeance; // Smarty m'a saoulé pour utiliser form_field|date_fr:---
$doc['nom_contact'] = $f->nom_contact ?? null; $doc['nom_contact'] = $f->nom_contact;
$doc['numero_commande'] = $f->numero_commande ?? null; $doc['numero_commande'] = $f->numero_commande;
$doc['reference_acheteur'] = $f->reference_acheteur ?? null; $doc['reference_acheteur'] = $f->reference_acheteur;
/* modif DD -- CERFA -------------------------------------- */ /* modif DD -- CERFA -------------------------------------- */
if ( $f->type_facture == CERFA ) { if ( $f->type_facture == CERFA ) {
$doc['total'] = $f->total; $doc['total'] = $f->total;
@ -344,11 +346,11 @@ $date->setTimestamp(time());
$tpl->assign('date', $date->format('d/m/Y')); $tpl->assign('date', $date->format('d/m/Y'));
$tpl->assign(compact('liste', 'radio', 'step', 'designations', 'prix', 'from_user', 'identite', 'csrf_key', 'doc')); $tpl->assign(compact('liste', 'radio', 'step', 'designations', 'prix', 'from_user', 'identite', 'csrf_key', 'doc'));
$tpl->assign('users', iterator_to_array(Users\Users::iterateAssocByCategory())); $tpl->assign('users', toArray($db->get('SELECT id, '.$identite.' FROM users WHERE id_category != -2 NOT IN (SELECT id FROM users_categories WHERE hidden = 1) ORDER BY ' .$identite. ';'), 'id', " "));
$tpl->assign('clients', $db->getAssoc('SELECT id, nom FROM plugin_facturation_clients;')); $tpl->assign('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('contacts', $db->getAssoc('SELECT id, nom_contact FROM plugin_facturation_clients;'));
$tpl->assign('require_number', $require_number); $tpl->assign('require_number', $require_number);
$tpl->assign('number_pattern', PATTERNS_LIST[$plugin->getConfig('pattern')] ?? PATTERNS_LIST['']); $tpl->assign('number_pattern', PATTERNS_LIST[$plugin->getConfig('pattern')]);
if ($target) { if ($target) {
$tpl->display(PLUGIN_ROOT . '/templates/facture_ajouter.tpl'); $tpl->display(PLUGIN_ROOT . '/templates/facture_ajouter.tpl');

View file

@ -22,42 +22,6 @@ const PATTERNS_LIST = [
'%06{id}' => 'Numéro unique du document sur 6 chiffres ("000042")', '%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; $client = new Client;
$facture = new Facture; $facture = new Facture;
@ -99,7 +63,7 @@ $tpl->register_function('money_fac', function (array $params)
$current_value = htmlspecialchars($current_value, ENT_QUOTES, 'UTF-8'); $current_value = htmlspecialchars($current_value, ENT_QUOTES, 'UTF-8');
} }
$currency = Config::getInstance()->get('currency'); $currency = Config::getInstance()->get('monnaie');
return sprintf('<td><nobr><input type="text" pattern="[0-9]*([.,][0-9]{1,2})?" inputmode="decimal" size="8" class="money" style="width: 60%%" onchange="updateSum();" name="%s" value="%s" /><b>%s</b></nobr></td>', $name, $current_value, $currency); return sprintf('<td><nobr><input type="text" pattern="[0-9]*([.,][0-9]{1,2})?" inputmode="decimal" size="8" class="money" style="width: 60%%" onchange="updateSum();" name="%s" value="%s" /><b>%s</b></nobr></td>', $name, $current_value, $currency);
} }
); );

View file

@ -12,7 +12,7 @@ $id = (int) qg('id');
$c = $client->get($id); $c = $client->get($id);
if (!$c) if (!$client)
{ {
throw new UserException("Ce client n'existe pas."); throw new UserException("Ce client n'existe pas.");
} }

View file

@ -11,17 +11,6 @@ $champsPaheko = DynamicFields::getInstance()->listAssocNames();
$champsPaheko = array('' => '- Choisir un champ -') + $champsPaheko; $champsPaheko = array('' => '- Choisir un champ -') + $champsPaheko;
$form->runIf('save', function () use ($plugin) { $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('rna_asso', trim(f('rna_asso')));
$plugin->setConfigProperty('siret_asso', trim(f('siret_asso'))); $plugin->setConfigProperty('siret_asso', trim(f('siret_asso')));
$plugin->setConfigProperty('ttc', (bool) f('ttc')); $plugin->setConfigProperty('ttc', (bool) f('ttc'));
@ -33,14 +22,6 @@ $form->runIf('save', function () use ($plugin) {
$plugin->setConfigProperty('logo', (bool)f('logo')); $plugin->setConfigProperty('logo', (bool)f('logo'));
$plugin->setConfigProperty('footer', f('footer')); $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('adresse_client', f('adresse_client'));
$plugin->setConfigProperty('code_postal_client', f('code_postal_client')); $plugin->setConfigProperty('code_postal_client', f('code_postal_client'));
@ -55,21 +36,8 @@ $form->runIf('save', function () use ($plugin) {
$tpl->assign('ok', qg('ok') !== null); $tpl->assign('ok', qg('ok') !== null);
$conf = $plugin->getConfig(); $tpl->assign('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('patterns', \Paheko\Plugin\Facturation\PATTERNS_LIST);
$tpl->assign('champsPaheko', $champsPaheko); $tpl->assign('champsPaheko', $champsPaheko);
$tpl->assign('invoice_themes', [
'modern' => 'Moderne',
'classic' => 'Classique',
'minimal' => 'Minimal',
]);
$tpl->display(PLUGIN_ROOT . '/templates/config.tpl'); $tpl->display(PLUGIN_ROOT . '/templates/config.tpl');

View file

@ -12,13 +12,11 @@ $id = (int) qg('id');
$f = $facture->get($id); $f = $facture->get($id);
if (!$f) if (!$client)
{ {
throw new UserException("Ce document n'existe pas."); throw new UserException("Ce document n'existe pas.");
} }
$csrf_key = 'delete_doc_' . $f->id;
$form->runIf(f('delete') && !$form->hasErrors(), $form->runIf(f('delete') && !$form->hasErrors(),
function () use ($facture, $f, $form) function () use ($facture, $f, $form)
{ {
@ -30,8 +28,7 @@ $form->runIf(f('delete') && !$form->hasErrors(),
{ {
$form->addError($e->getMessage()); $form->addError($e->getMessage());
} }
}, $csrf_key); });
$tpl->assign('doc', $f); $tpl->assign('doc', $f);
$tpl->assign(compact('csrf_key'));
$tpl->display(PLUGIN_ROOT . '/templates/facture_supprimer.tpl'); $tpl->display(PLUGIN_ROOT . '/templates/facture_supprimer.tpl');

View file

@ -4,30 +4,11 @@ namespace Paheko;
require_once __DIR__ . '/_inc.php'; require_once __DIR__ . '/_inc.php';
$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ); $session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ);
$users = new Users\Users;
f(['id' => 'required|numeric']); f(['id' => 'required|numeric']);
$id = (int) qg('id'); $id = (int) qg('id');
$sign_tag = UserTemplate\Functions::signature(); $sign_tag = UserTemplate\Functions::signature();
$h = static function ($value): string {
return htmlspecialchars_decode(
htmlentities($value, ENT_NOQUOTES, 'UTF-8', false),
ENT_NOQUOTES
);
};
$br = static function ($value) use ($h): string {
return nl2br($h($value));
};
$format_identifier = static function ($value) use ($h): string {
$value = preg_replace('/\s+/', '', (string) $value);
return $value !== '' ? $h(implode(' ', str_split($value, 3))) : '';
};
$detail_line = static function (string $label, $value) use ($h, $br): string {
if ($value === null || trim((string) $value) === '') {
return '';
}
return sprintf('<div><span>%s</span>%s</div>', $h($label), $br($value));
};
// Vérification que le document existe // Vérification que le document existe
if (!$f = $facture->get($id)) if (!$f = $facture->get($id))
@ -37,42 +18,47 @@ if (!$f = $facture->get($id))
$moyen_paiement = $facture->getMoyenPaiement($f->moyen_paiement); $moyen_paiement = $facture->getMoyenPaiement($f->moyen_paiement);
// Récupération infos membre/client dans un objet normalisé pour le rendu // Récupération infos membre
if ($f->receveur_membre) try
{ {
$user = Users\Users::get($f->receveur_id); if ($f->receveur_membre)
{
$c = $users->get($f->receveur_id);
// l'identité du membre peut être redéfinie dans la configuration des membres
$name_fields = \Paheko\Users\DynamicFields::getNameFields();
array_walk($name_fields, function(&$elem) use ($c) {
$elem = $c->$elem;
});
$nom_client = implode(" ", $name_fields);
if (preg_match('/^ +$/', $nom_client)) {
$nom_client = "** ABSENT **";
}
if (!$user) { // adresse, code postal et ville peuvent être redéfini(e)s dans la configuration du plugin
throw new UserException("Ce membre n'existe plus."); $adresse_client = $plugin->getConfig('adresse_client');
if ($adresse_client != null && $c->$adresse_client != null) { $c->adresse = $c->$adresse_client; }
$code_postal_client = $plugin->getConfig('code_postal_client');
if ($code_postal_client != null && $c->$code_postal_client != null) { $c->code_postal = $c->$code_postal_client; }
$ville_client = $plugin->getConfig('ville_client');
if ($ville_client != null && $c->$ville_client != null) { $c->ville = $c->$ville_client; }
foreach(['ville','code_postal','adresse'] as $v)
{
if($c->$v == '')
{
$c->$v = '[À RENSEIGNER DANS LA FICHE MEMBRE]';
}
}
}
else
{
$c = $client->get($f->receveur_id);
$nom_client = $c->nom;
} }
$nom_client = Users\DynamicFields::getNameFromArray($user) ?: '** ABSENT **';
$user_fields = Users\DynamicFields::getInstance()->listAssocNames();
$get_user_field = static function (?string $configured, string $fallback) use ($user, $user_fields) {
$field = $configured ?: $fallback;
return array_key_exists($field, $user_fields) ? ($user->$field ?? null) : null;
};
$c = (object) [
'nom' => $nom_client,
'adresse' => $get_user_field($plugin->getConfig('adresse_client'), 'adresse') ?: '[À RENSEIGNER DANS LA FICHE MEMBRE]',
'code_postal' => $get_user_field($plugin->getConfig('code_postal_client'), 'code_postal') ?: '[À RENSEIGNER]',
'ville' => $get_user_field($plugin->getConfig('ville_client'), 'ville') ?: '[À RENSEIGNER]',
'siret' => null,
'email' => Users\DynamicFields::getFirstEmailField() ? ($user->{Users\DynamicFields::getFirstEmailField()} ?? null) : null,
'telephone' => null,
];
} }
else catch(UserException $e)
{ {
$client_data = $client->get($f->receveur_id); $form->addError($e);
if (!$client_data) {
throw new UserException("Ce client n'existe plus.");
}
$nom_client = $client_data->nom;
$c = $client_data;
} }
// Formatage dates // Formatage dates
@ -88,417 +74,307 @@ if (isset($f->date_echeance))
// Génération factures, devis et cotisation // Génération factures, devis et cotisation
if ($f->type_facture != CERFA) if ($f->type_facture != CERFA)
{ {
$labels = [ switch ($f->type_facture)
FACT => ['Facture', 'Émise le', 'Adressée à', 'Échéance de paiement'], {
DEVIS => ['Devis', 'Émis le', 'Adressé à', 'Échéance du devis'], case FACT:
COTIS => ['Reçu de cotisation', 'Émis le', 'Adressé à', 'Date du reçu'], $doc = 'Facture n° '. $f->numero;
]; $txtemis = $doc . " - Émise le " . $emission;
[$document_label, $emission_label, $recipient_label, $due_label] = $labels[$f->type_facture] ?? $labels[FACT]; $txtdest = "Adressée à :";
$doc = sprintf('%s n° %s', $document_label, $f->numero); break;
$theme = $plugin->getConfig('invoice_theme') ?: 'modern'; case DEVIS:
$theme = in_array($theme, ['modern', 'classic', 'minimal'], true) ? $theme : 'modern'; $doc = 'Devis n° '. $f->numero;
$accent = $plugin->getConfig('invoice_accent_color') ?: '#4656A5'; $txtemis = $doc . " - Émis le " . $emission;
$accent = preg_match('/^#[0-9A-Fa-f]{6}$/', $accent) ? $accent : '#4656A5'; $txtdest = "Adressé à :";
$show_address_borders = $plugin->getConfig('invoice_show_address_borders') !== false; break;
$show_email = $plugin->getConfig('invoice_show_email') !== false; case COTIS:
$show_web = $plugin->getConfig('invoice_show_web') !== false; $doc = 'Reçu de cotisation n° '. $f->numero;
$address_border_class = $show_address_borders ? 'invoice-address-borders' : 'invoice-no-address-borders'; $txtemis = $doc . " - Émis le " . $emission;
$currency = $config->get('currency') ?: '€'; $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') . "<br>" .
$plugin->getConfig('cp_asso') . " " .
$plugin->getConfig('ville_asso');
}
else if ($config->get('org_address') != null)
{
$adresse = str_replace("\n", '<br>', $config->get('org_address'));
}
else {
$adresse = "";
}
$logo='';
if ($plugin->getConfig('logo')) {
$logo = '<img id="logo" src="' . $config->fileURL('logo') . '" />';
}
$asso =
// 'Émis par :<br><br>'.
'<b>'.$config->get('org_name')."</b><br>".
$adresse ."<br>".
(($t = $plugin->getConfig('rna_asso'))?"RNA : $t<br>":'').
(($t = $plugin->getConfig('siret_asso'))?"SIRET : " . implode(' ', str_split($t, 3)) . "<br>":'').
(($t = $config->get('email_asso'))?"Email : $t<br>":'').
(($t = $config->get('site_asso'))?"Site web : $t<br>":'');
$receveur =
$txtdest.'<br>'.
'<b>'.$nom_client.'</b><br>'.
(($t = $f->nom_contact)?"Contact : $t<br>":'').
$c->adresse."<br>".
$c->code_postal.' '.$c->ville."<br>".
(($t = $c->siret)?"SIREN/SIRET : " . implode(' ', str_split($t, 3)) . "<br>":'').
(($t = $c->email)?"Email : $t<br>":'').
(($t = $c->telephone)?"Tel : $t<br>":'');
if ($f->type_facture == FACT) {
$receveur .=
(($t = $f->numero_commande)?"Commande N° : $t<br>":'').
(($t = $f->reference_acheteur)?"Référence : $t<br>":'');
}
$total = Utils::money_format($f->total, ',', ' '); $total = Utils::money_format($f->total, ',', ' ');
$ttc = $plugin->getConfig('ttc') ? 'TTC' : 'HT';
if ($plugin->getConfig('rue_asso') && $plugin->getConfig('cp_asso') && $plugin->getConfig('ville_asso')) { // Devis et facture
$adresse = trim(($plugin->getConfig('numero_rue_asso') ? $plugin->getConfig('numero_rue_asso') . ' ' : '') . $plugin->getConfig('rue_asso')) if ($f->type_facture != COTIS)
. "\n" . $plugin->getConfig('cp_asso') . ' ' . $plugin->getConfig('ville_asso'); {
} $echeance = ($f->type_facture?'Échéance de paiement':'Échéance du devis')." : ".$echeance;
else { if ($f->type_facture == FACT) {
$adresse = $config->get('org_address_public') ?: $config->get('org_address') ?: ''; $reglee = !$f->reglee?'Cette facture est en attente de règlement.':'Cette facture a été réglée.';
} } else {
$reglee = "";
}
$footer = str_replace("\n", '<br>', $plugin->getConfig('footer') ?? '[Pied de page à configurer]');
$ttc = $plugin->getConfig('ttc') ? 'TTC':'HT';
$logo = ''; // Génération du contenu de la facture
if ($plugin->getConfig('logo') && ($logo_url = $config->fileURL('logo'))) { ob_start();
$logo = sprintf('<img class="logo" src="%s" alt="" />', $h($logo_url)); echo <<<EOF
} <div class="h2">
<span>Contenu</span> - $doc
</div>
<hr>
$asso_details = '<strong>' . $h($config->get('org_name')) . '</strong>'; <table class="contenu">
$asso_details .= $adresse ? '<p>' . $br($adresse) . '</p>' : ''; <thead>
$asso_details .= ($t = $plugin->getConfig('rna_asso')) ? $detail_line('RNA', $t) : ''; <tr>
$asso_details .= ($t = $plugin->getConfig('siret_asso')) ? '<div><span>SIRET</span>' . $format_identifier($t) . '</div>' : ''; <th>
$asso_details .= $show_email && ($t = $config->get('org_email')) ? $detail_line('Email', $t) : ''; Désignations
$asso_details .= ($t = $config->get('org_phone')) ? $detail_line('Tel.', $t) : ''; </th>
$asso_details .= $show_web && ($t = $config->get('org_web')) ? $detail_line('Web', $t) : ''; <th>
$brand_summary = '<div class="brand-summary">' . $logo . '<strong>' . $h($config->get('org_name')) . '</strong></div>'; Prix
</th>
</tr>
</thead>
<tbody>
EOF;
$receveur = '<strong>' . $h($nom_client) . '</strong>'; $i = 1;
$receveur .= ($t = ($f->nom_contact ?? null)) ? $detail_line('Contact', $t) : ''; foreach($f->contenu as $k=>$v)
$receveur .= '<p>' . $br($c->adresse ?? '') . '<br>' . $h(trim(($c->code_postal ?? '') . ' ' . ($c->ville ?? ''))) . '</p>'; {
$receveur .= ($t = $c->siret ?? null) ? '<div><span>SIREN/SIRET</span>' . $format_identifier($t) . '</div>' : ''; echo '<tr><td>';
$receveur .= $show_email && ($t = $c->email ?? null) ? $detail_line('Email', $t) : ''; echo str_replace("\n", '<br>', $v['designation']);
$receveur .= ($t = $c->telephone ?? null) ? $detail_line('Tel.', $t) : ''; echo '</td><td>';
$receveur .= $f->type_facture == FACT && ($t = ($f->numero_commande ?? null)) ? $detail_line('Commande', $t) : ''; echo Utils::money_format($v['prix'], ',', ' ') .' €';
$receveur .= $f->type_facture == FACT && ($t = ($f->reference_acheteur ?? null)) ? $detail_line('Référence', $t) : ''; echo '</td></tr>';
$i++;
if ($f->type_facture != COTIS) {
$rows = '';
foreach($f->contenu as $v) {
$rows .= sprintf(
'<tr><td class="designation">%s</td><td class="amount">%s %s</td></tr>',
$br($v['designation']),
$h(Utils::money_format($v['prix'], ',', ' ')),
$h($currency)
);
} }
$payment_terms = trim((string) ($plugin->getConfig('payment_terms') ?: '')); echo <<<EOF
$invoice_note = trim((string) ($plugin->getConfig('invoice_note') ?: '')); </tbody>
$status = ''; <tfoot>
<tr>
<td><b>Total</b><br>Net à payer</td>
<td><b>$total </b><br>({$ttc})</td>
</tr>
</tfoot>
</table>
<footer>
<div class="h2"><span>Détails</span></div>
<hr>
if ($plugin->getConfig('invoice_show_status') && $f->type_facture == FACT) { $echeance <br>
$status_class = $f->reglee ? 'paid' : 'pending'; $reglee
$status_label = $f->reglee ? 'Facture réglée' : 'En attente de règlement'; Moyen de paiement : $moyen_paiement
$status = sprintf('<p class="status %s">%s</p>', $status_class, $h($status_label)); <p>
$footer
</p>
</footer>
EOF;
if ($f->type_facture == DEVIS) {
echo <<<EOF
<p><b>Bon pour accord, date et signature<b></p>
EOF;
} }
$approval = $f->type_facture == DEVIS ? '<section class="approval"><strong>Bon pour accord</strong><span>Date, nom et signature</span></section>' : ''; $content = ob_get_clean();
$note = $invoice_note !== '' ? '<p class="invoice-note">' . $br($invoice_note) . '</p>' : '';
$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 = <<<HTML
<section class="items">
<table>
<thead>
<tr>
<th>Désignation</th>
<th class="amount">Montant</th>
</tr>
</thead>
<tbody>
$rows
</tbody>
</table>
</section>
<section class="summary">
<div class="details">
$echeance_html
$terms
$moyen_html
$status
</div>
<div class="total">
<span>Total $ttc</span>
<strong>$total {$h($currency)}</strong>
<em>Net à payer</em>
</div>
</section>
$note
$approval
<footer>$footer</footer>
HTML;
} }
else { else // Reçu de cotisation
$lieu = $plugin->getConfig('ville_asso') ?: ''; {
$lieu = $plugin->getConfig('ville_asso');
$intitule = $f->contenu['intitule']; $intitule = $f->contenu['intitule'];
$souscription = date('d/m/Y', strtotime($f->contenu['souscription']));
$expiration = $f->contenu['expiration'] == '1970-01-01'
? "le jour même, s'agissant d'une cotisation ponctuelle"
: 'le ' . date('d/m/Y', strtotime($f->contenu['expiration']));
$footer = $br($plugin->getConfig('footer') ?? '');
$content = <<<HTML $souscription = date('d/m/Y', strtotime($f->contenu['souscription']));
<section class="letter">
<p>À {$h($lieu)}, le {$h($emission)},</p> 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 = <<<EOF
<div class="h2">
<span>Reçu de votre cotisation</span> - $doc
</div>
<hr>
<div class="contenuTexte">
<p>À $lieu, le $emission,</p>
<p>Bonjour,</p> <p>Bonjour,</p>
<p>Nous accusons réception de votre cotisation « {$h($intitule)} » reçue le {$h($emission)} et nous vous en remercions.</p>
<p>Nous reconnaissons que vous avez acquitté la somme de <strong>{$h($total)} {$h($currency)}</strong>. Votre adhésion sera donc effective à compter du {$h($souscription)} jusqu'à {$h($expiration)}.</p> <p>Nous accusons réception de votre cotisation « $intitule » reçue le $emission et nous vous en remercions.</p>
<p>Nous vous prions de recevoir nos meilleures salutations.</p> <p>Nous reconnaissons que vous avez acquitté la somme de {$total} .<br>111
<p><strong>Pour {$h($config->get('org_name'))}</strong></p> Votre adhésion sera donc effective à compter du $souscription jusquau $expiration.</p>
<p class="legal">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.</p> <br>
</section>
<footer>$footer</footer> <p>Nous vous prions de recevoir, chère adhérente, cher adhérent, nos meilleures salutations,</p>
HTML; <br>
<p>-représentant·e de l'asso-</p>
<br>
<p><i>Nous vous rappelons que la cotisation nest pas soumise à la TVA et quelle ne donne pas lieu à la délivrance dune facture. Elle nouvre 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.</i></p>
</div>
EOF;
} }
$html_title = $h($doc . '_' . $emission); //-- Layout du document
$doc_html = $h($doc);
$txtemis = $h($emission_label . ' ' . $emission);
$txtdest = $h($recipient_label);
$html = <<<HTML ob_start();
echo <<<EOF
<!DOCTYPE html> <!DOCTYPE html>
<html class="invoice-theme-$theme $address_border_class"> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>$html_title</title> <title>{$doc}_{$emission}</title>
<style> <style>
@page { size: A4 portrait; margin: 14mm; } @page {
* { box-sizing: border-box; } size: A4 portrait;
margin: 0;
}
body { body {
margin: 0; padding: 4mm;
font-family: Helvetica, Arial, sans-serif; font-family: Helvetica, Arial, sans;
font-size: 10pt; font-size: 10pt;
line-height: 1.45; background: white;
color: #20242A;
background: #fff;
} }
.invoice {
display: block; .titre {
padding-bottom: 28mm;
page-break-after: avoid;
break-after: avoid-page;
}
.hero {
display: table;
width: 100%;
padding-bottom: 10mm;
border-bottom: 2px solid $accent;
}
.brand, .document-title {
display: table-cell;
vertical-align: top;
}
.brand { width: 46%; }
.document-title { width: 54%; }
.brand-summary {
display: inline-block;
max-width: 72mm;
text-align: center; text-align: center;
font-size: 8pt;
margin-bottom: 6mm;
margin-top: 0mm;
} }
.logo { .h2 {
display: block; margin: 20px 20px 0px 20px;
max-width: 34mm;
max-height: 24mm;
margin: 0 auto 4mm;
} }
.brand-summary strong { .h2 span {
display: block; font-weight: bold;
font-size: 10pt; font-size: 14pt;
line-height: 1.3;
} }
.document-title { hr {
text-align: right; margin: 5px 0px 15px 0px;
border: none;
border-top: 1px solid;
} }
.document-title h1 { .adressage {
margin: 0 0 2mm; font-size: 11pt;
color: $accent; margin: auto;
font-size: 19pt;
font-weight: 700;
line-height: 1.15;
letter-spacing: 0;
white-space: nowrap;
} }
.document-title p { .adressage td {
margin: 0; width: 40%;
color: #566070;
}
.addresses {
display: table;
width: 100%;
margin: 10mm 0 8mm;
}
.card {
display: table-cell;
width: 50%;
padding: 6mm;
vertical-align: top; vertical-align: top;
border: 1px solid #D9DEE8;
} }
.card + .card { border-left: 0; } .adressage td#logo {
.card h2 { width: 20%;
margin: 0 0 4mm; vertical-align: top;
color: $accent;
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0;
} }
.card strong { .adressage img#logo {
display: block; height : 3cm;
margin-bottom: 2mm;
font-size: 12pt;
} }
.card p { margin: 2mm 0; }
.card div { .contenuTexte {
margin-top: 1.5mm; padding: 0 6mm;
} }
.card span {
display: inline-block; .contenu {
min-width: 27mm;
margin-right: 3mm;
color: #687386;
}
.invoice-no-address-borders .card {
padding: 0;
border: 0;
}
.invoice-no-address-borders .card + .card {
padding-left: 10mm;
}
.items table {
width: 100%; width: 100%;
}
.contenu td, .contenu th {
vertical-align: top;
padding: 8px 10px;
margin : 0px;
border-collapse: collapse; border-collapse: collapse;
} }
.items th {
padding: 4mm 3mm; .contenu tr th, .contenu tr td {
color: #fff; text-align: right;
background: $accent; width: 16%;
}
.contenu tr td:nth-child(1), .contenu tr th:nth-child(1) {
text-align: left; text-align: left;
font-weight: 700; width: 84%;
} }
.items td {
padding: 4mm 3mm; .contenu thead tr, .contenu tfoot tr {
border-bottom: 1px solid #E5E8EF; background-color: #CCE;
vertical-align: top; border: 10px solid black;
} }
.items tr:nth-child(even) td { .contenu tbody tr:nth-child(even) {
background: #F7F8FB; background-color: #DDF !important;
} }
.amount { .contenu tfoot {
width: 34mm; display: table-row-group;
text-align: right;
white-space: nowrap;
}
.summary {
display: table;
width: 100%;
margin-top: 8mm;
}
.details, .total {
display: table-cell;
vertical-align: top;
}
.details {
width: 64%;
color: #3B4350;
}
.details div {
margin-bottom: 2mm;
}
.details span {
display: inline-block;
min-width: 42mm;
margin-right: 4mm;
color: #687386;
}
.total {
width: 36%;
padding: 4mm 5mm;
color: #fff;
background: $accent;
text-align: right;
}
.total span, .total em {
display: block;
font-style: normal;
font-size: 9.5pt;
}
.total strong {
display: block;
margin: 1mm 0;
font-size: 17pt;
line-height: 1.15;
}
.status {
display: inline-block;
margin: 2mm 0 0;
padding: 1.5mm 3mm;
border: 1px solid #D9DEE8;
font-weight: 700;
}
.status.paid { color: #1F6B3A; }
.status.pending { color: #9A4D00; }
.invoice-note, .approval, .letter {
margin-top: 8mm;
}
.approval {
min-height: 25mm;
padding-top: 4mm;
border-top: 1px solid #D9DEE8;
}
.approval strong, .approval span {
display: block;
}
.legal {
margin-top: 8mm;
color: #566070;
font-size: 9pt;
} }
footer { footer {
position: fixed;
right: 0;
bottom: 0; bottom: 0;
left: 0; margin: 14mm 0;
margin: 0; width: inherit;
padding-top: 6mm; font-size: 9pt;
color: #566070;
font-size: 8.5pt;
text-align: center;
border-top: 1px solid #D9DEE8;
}
.invoice-theme-classic .hero {
border-bottom-width: 1px;
}
.invoice-theme-classic .items th,
.invoice-theme-classic .total {
background: #2F343D;
}
.invoice-theme-minimal .hero,
.invoice-theme-minimal .card,
.invoice-theme-minimal footer {
border-color: #E6E8ED;
}
.invoice-theme-minimal .items th,
.invoice-theme-minimal .total {
color: #20242A;
background: #F0F2F6;
}
@media screen {
body {
padding: 14mm;
}
footer {
right: 14mm;
bottom: 14mm;
left: 14mm;
}
}
@media print {
footer {
right: 0;
bottom: 0;
left: 0;
}
} }
</style> </style>
</head> </head>
<body> <body>
<main class="invoice"> <p class="titre">
<header class="hero"> $txtemis
<section class="brand"> </p>
$brand_summary
</section> <table class="adressage">
<section class="document-title"> <tr>
<h1>$doc_html</h1> <td id="logo">$logo</td>
<p>$txtemis</p> <td>$asso</td>
</section> <td>$receveur</td>
</header> </tr>
<section class="addresses"> </table>
<div class="card"> <br>
<h2>Émetteur</h2> $content
$asso_details
</div>
<div class="card">
<h2>$txtdest</h2>
$receveur
</div>
</section>
$content
</main>
</body> </body>
</html> </html>
HTML; EOF;
$html = ob_get_clean();
} // Génération du CERFA } // Génération du CERFA
elseif ($f->type_facture == CERFA) elseif ($f->type_facture == CERFA)
{ {
@ -518,9 +394,9 @@ elseif ($f->type_facture == CERFA)
$t['objet2'] = $plugin->getConfig('objet_2'); $t['objet2'] = $plugin->getConfig('objet_2');
$t['nom'] = $nom_client; $t['nom'] = $nom_client;
$t['adresse'] = $c->adresse ?? ''; $t['adresse'] = $c->adresse;
$t['cp'] = $c->code_postal ?? ''; $t['cp'] = $c->code_postal;
$t['ville'] = $c->ville ?? ''; $t['ville'] = $c->ville;
$t['total'] = '***'.Utils::money_format($f->total).'***'; $t['total'] = '***'.Utils::money_format($f->total).'***';
$t['total_lettre'] = numfmt_create('fr_FR', \NumberFormatter::SPELLOUT)->format($f->total/100). ' euros'; $t['total_lettre'] = numfmt_create('fr_FR', \NumberFormatter::SPELLOUT)->format($f->total/100). ' euros';

View file

@ -1,14 +1,6 @@
{ {
"footer": "[EXEMPLE]\nAssociation exonérée des impôts commerciaux\nEn cas de retard de paiement, indemnité forfaitaire légale pour frais de recouvrement : 40,00 €\n[Coordonnées bancaires]\nAssociation enregistrée en préfecture de XXX au numéro YYY", "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}", "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, "ttc": false,
"validate_cp": true "validate_cp": true
} }

View file

@ -3,13 +3,13 @@ CREATE TABLE IF NOT EXISTS plugin_facturation_factures (
type_facture INTEGER NOT NULL DEFAULT 0, type_facture INTEGER NOT NULL DEFAULT 0,
numero TEXT NOT NULL UNIQUE, numero TEXT NOT NULL UNIQUE,
receveur_membre INTEGER NOT NULL, -- bool receveur_membre INTEGER NOT NULL, -- bool
receveur_id INTEGER NOT NULL, receveur_id INTEGER NOT NULL,
date_emission TEXT NOT NULL, -- CHECK (date(date_emission) IS NOT NULL AND date(date_emission) = date_emission), 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), date_echeance TEXT NOT NULL, -- CHECK (date(date_echeance) IS NOT NULL AND date(date_echeance) = date_echeance),
reglee INTEGER DEFAULT 0, -- bool reglee INTEGER DEFAULT 0, -- bool
archivee INTEGER DEFAULT 0, -- bool archivee INTEGER DEFAULT 0, -- bool
moyen_paiement TEXT NOT NULL, moyen_paiement TEXT NOT NULL,
contenu TEXT NOT NULL, contenu TEXT NOT NULL,
total INTEGER DEFAULT 0, total INTEGER DEFAULT 0,
nom_contact TEXT, nom_contact TEXT,
numero_commande TEXT, numero_commande TEXT,
@ -20,10 +20,10 @@ CREATE TABLE IF NOT EXISTS plugin_facturation_factures (
CREATE TABLE IF NOT EXISTS plugin_facturation_clients ( CREATE TABLE IF NOT EXISTS plugin_facturation_clients (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
nom TEXT NOT NULL, nom TEXT NOT NULL,
adresse TEXT NOT NULL, adresse TEXT NOT NULL,
code_postal TEXT NOT NULL, code_postal TEXT NOT NULL,
ville TEXT NOT NULL, ville TEXT NOT NULL,
siret TEXT, 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 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, telephone TEXT,

View file

@ -1,13 +1,8 @@
<?php <?php
namespace Paheko; namespace Paheko;
use Paheko\Entities\Files\File;
use Paheko\Plugin\Facturation\Facture;
$db = DB::getInstance(); $db = DB::getInstance();
$db->import(dirname(__FILE__) . "/data/schema.sql"); $db->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();
}

View file

@ -4,7 +4,7 @@ namespace Paheko\Plugin\Facturation;
use Paheko\DB; use Paheko\DB;
use Paheko\DynamicList; use Paheko\DynamicList;
use Paheko\Plugins; use Paheko\Entities\Plugin;
use Paheko\UserException; use Paheko\UserException;
use Paheko\Utils; use Paheko\Utils;
@ -29,12 +29,9 @@ class Client
public function __construct() public function __construct()
{ {
$plugin = Plugins::get('facturation'); $plugin = new Plugin('facturation');
$this->config['unique_client_name'] = $plugin->getConfig('unique_client_name') ?: false;
if ($plugin) { $this->config['validate_cp'] = $plugin->getConfig('validate_cp') ?: false;
$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) public function _checkFields(&$data)
@ -105,20 +102,8 @@ class Client
{ {
$db = DB::getInstance(); $db = DB::getInstance();
$client = $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation return $db->first('SELECT *, strftime(\'%s\', date_creation) AS date_creation
FROM plugin_facturation_clients WHERE id = ? LIMIT 1;', (int)$id); 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() public function listAll()

View file

@ -6,10 +6,9 @@ use DateTime;
use Paheko\Config; use Paheko\Config;
use Paheko\DB; use Paheko\DB;
use Paheko\DynamicList; use Paheko\DynamicList;
use Paheko\Plugins;
use Paheko\UserException; use Paheko\UserException;
use Paheko\Users\DynamicFields;
use Paheko\Utils; use Paheko\Utils;
use Paheko\Services\Services_User;
class Facture class Facture
{ {
@ -48,52 +47,17 @@ class Facture
'accounts' => [], 'accounts' => [],
'label' => 'Facture', 'label' => 'Facture',
'help' => ''], '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() 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 // 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) // et implique que toutes les données soient présentes (pas possible de faire un update partiel)
public function _checkFields(&$datas) 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) foreach($datas as $k=>$data)
{ {
if (!in_array($k, $this->keys)) if (!in_array($k, $this->keys))
@ -112,11 +76,24 @@ class Facture
switch($k) switch($k)
{ {
case 'type_facture': case 'type_facture':
$datas[$k] = (int) $datas[$k]; if (!array_key_exists($datas[$k], $this->types)) {
if (!array_key_exists($datas[$k], self::TYPES_NAMES)) {
throw new UserException("$k est de type non-attendue ($data)."); 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; break;
case 'receveur_membre': case 'receveur_membre':
case 'reglee': case 'reglee':
@ -131,11 +108,11 @@ class Facture
} }
break; break;
case 'date_emission': case 'date_emission':
$datas[$k] = $this->normalizeDate($data, $k); $datas[$k] = \DateTime::createFromFormat('!d/m/Y', $data)->format('Y-m-d');
break; break;
case 'date_echeance': case 'date_echeance':
$datas[$k] = $this->normalizeDate($data, $k); $datas[$k] = \DateTime::createFromFormat('!d/m/Y', $data)->format('Y-m-d');
if (isset($datas['date_emission']) && DateTime::createFromFormat('!Y-m-d', $datas[$k])->format('U') < DateTime::createFromFormat('!Y-m-d', $datas['date_emission'])->format('U')) if (DateTime::createFromFormat('!Y-m-d', $datas[$k])->format('U') < DateTime::createFromFormat('!Y-m-d', $datas['date_emission'])->format('U'))
{ {
throw new UserException("La date d'échéance est antérieure à la date d'émission ($data)."); throw new UserException("La date d'échéance est antérieure à la date d'émission ($data).");
} }
@ -157,12 +134,12 @@ class Facture
if (empty($r['designation']) && empty($r['prix'])) if (empty($r['designation']) && empty($r['prix']))
{ {
unset($datas[$k][$g]); unset($datas[$k][$g]);
unset($datas[$k]['prix']);
continue; continue;
} }
elseif (!isset($r['prix']) || $r['prix'] === '') elseif (! is_numeric($r['prix']) && empty($r['prix']))
{ {
$datas[$k][$g]['prix'] = 0; $datas[$k]['prix'] = 0;
$r['prix'] = 0;
} }
elseif (empty($r['designation'])) { elseif (empty($r['designation'])) {
throw new UserException("Une au moins des désignations est absente."); throw new UserException("Une au moins des désignations est absente.");
@ -274,7 +251,7 @@ class Facture
$type = ctype_digit($v) ? 'd' : 's'; $type = ctype_digit($v) ? 'd' : 's';
return sprintf('%' . $match[1] . $type, $v); return sprintf('%' . $match[1] . $type, $v);
}, $selpattern); }, $selpattern);
$modele = '/^' . preg_quote($prefixe, '/') . '\d+$/'; $modele = '/^' . $prefixe . '\d+$/';
$numeros_filtres = array_filter($numeros, function($elem) use ($modele) { $numeros_filtres = array_filter($numeros, function($elem) use ($modele) {
return preg_match($modele, $elem); return preg_match($modele, $elem);
}, 0); }, 0);
@ -315,12 +292,6 @@ class Facture
$r->contenu = json_decode($r->contenu, true); $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); $r->date_emission = \DateTime::createFromFormat('!Y-m-d', $r->date_emission);
if ($r->date_echeance) if ($r->date_echeance)
{ {
@ -349,19 +320,15 @@ class Facture
public function list($all = true): DynamicList public function list($all = true): DynamicList
{ {
$db = DB::getInstance(); $id_field = \Paheko\Users\DynamicFields::getNameFieldsSQL('u');
$id_field = DynamicFields::getNameFieldsSQL('u'); $plugin_name = preg_replace('/^.*\/(\w+)\/$/', '${1}', \Paheko\PLUGIN_ADMIN_URL);
$plugin = Plugins::get('facturation'); $plugin = \Paheko\Plugins::get($plugin_name);
$user_fields = DynamicFields::getInstance()->listAssocNames();
// adresse et ville peuvent être redéfinies dans la configuration du plugin // adresse et ville peuvent être redéfinies dans la configuration du plugin
$adresse_client = $plugin ? $plugin->getConfig('adresse_client') : null; $adresse_client = $plugin->getConfig('adresse_client');
$adresse_client = $adresse_client ?: 'adresse'; if ($adresse_client == null) { $adresse = 'u.adresse'; } else { $adresse = 'u.' . $adresse_client; }
$adresse = array_key_exists($adresse_client, $user_fields) ? 'u.' . $db->quoteIdentifier($adresse_client) : 'NULL'; $ville_client = $plugin->getConfig('ville_client');
if ($ville_client == null) { $ville = 'u.ville'; } else { $ville = 'u.' . $ville_client; }
$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 = [ $columns = [
// Sélectionner cette colonne, mais ne pas la mettre dans la liste des colonnes // Sélectionner cette colonne, mais ne pas la mettre dans la liste des colonnes
@ -390,7 +357,7 @@ class Facture
'receveur' => [ 'receveur' => [
'label' => 'Receveur', 'label' => 'Receveur',
// l'identité du membre peut être redéfinie dans la configuration des membres // l'identité du membre peut être redéfinie dans la configuration des membres
'select' => sprintf('CASE WHEN receveur_membre THEN COALESCE(NULLIF(%s, \'\'), \'** ABSENT **\') ELSE c.nom END', $id_field), 'select' => sprintf('CASE WHEN receveur_membre THEN CASE %s WHEN "" THEN "** ABSENT **" ELSE %s END ELSE c.nom END', $id_field, $id_field),
], ],
'receveur_adresse' => [ 'receveur_adresse' => [
// l'adresse peut être redéfinie dans la configuration du plugin // l'adresse peut être redéfinie dans la configuration du plugin
@ -426,7 +393,7 @@ class Facture
$tables = 'plugin_facturation_factures AS f $tables = 'plugin_facturation_factures AS f
INNER JOIN plugin_facturation_paiement AS mp ON mp.code = f.moyen_paiement INNER JOIN plugin_facturation_paiement AS mp ON mp.code = f.moyen_paiement
LEFT JOIN users_view AS u ON f.receveur_membre = 1 AND u.id = f.receveur_id LEFT JOIN users AS u ON f.receveur_membre = 1 AND u.id = f.receveur_id
LEFT JOIN plugin_facturation_clients AS c ON f.receveur_membre = 0 AND c.id = f.receveur_id'; LEFT JOIN plugin_facturation_clients AS c ON f.receveur_membre = 0 AND c.id = f.receveur_id';
if ($all) { if ($all) {
@ -437,7 +404,7 @@ class Facture
$list = new DynamicList($columns, $tables, $where); $list = new DynamicList($columns, $tables, $where);
$list->orderBy('date_emission', true); $list->orderBy('date_emission', true);
$currency = Config::getInstance()->currency; $currency = Config::getInstance()->monnaie;
$list->setModifier(function ($row) use ($currency) { $list->setModifier(function ($row) use ($currency) {
// Remplir la colonne virtuelle // Remplir la colonne virtuelle
@ -493,10 +460,7 @@ class Facture
} }
else // Si c'est un membre de l'asso else // Si c'est un membre de l'asso
{ {
if (!\Paheko\Users\Users::exists((int) $id)) throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients");
{
throw new UserException("Ce membre n'existe pas.");
}
} }
$r = (array)DB::getInstance()->get('SELECT *, strftime(\'%s\', date_emission) AS date_emission, $r = (array)DB::getInstance()->get('SELECT *, strftime(\'%s\', date_emission) AS date_emission,
@ -528,10 +492,7 @@ class Facture
} }
else // Si c'est un membre de l'asso else // Si c'est un membre de l'asso
{ {
if (!\Paheko\Users\Users::exists((int) $id)) throw new UserException("Woopsie, g pô encore implémenté l'usage des users de l'asso comme clients");
{
throw new UserException("Ce membre n'existe pas.");
}
} }
return DB::getInstance()->test('plugin_facturation_factures', 'receveur_membre = ? AND receveur_id = ?', $base, $id); return DB::getInstance()->test('plugin_facturation_factures', 'receveur_membre = ? AND receveur_id = ?', $base, $id);

View file

@ -1,8 +1,8 @@
name="Facturation" name="Facturation"
description="Permet d'éditer des factures et devis à ses membres ainsi qu'à une base de clients supplémentaire." description="Permet d'éditer des factures et devis à ses membres ainsi qu'à une base de clients supplémentaire."
author="Sur la base de Jean-Christophe Engel, amélioré par Jordan" author="zou ; adapté par Jean-Christophe Engel"
url="https://gitea.zaclys.com/lesanges/facturation" url="https://gitea.zaclys.com/lesanges/facturation"
version="0.27.2" version="0.17.2"
menu=true menu=true
restrict_section="accounting" restrict_section="accounting"
restrict_level="read" restrict_level="read"

View file

@ -63,7 +63,7 @@
<dt><label>Document adressé à :</label></dt> <dt><label>Document adressé à :</label></dt>
{if !empty($clients)} {if !empty($clients)}
<dd> <dd>
{input type="radio" name="base_receveur" value="membre" label="Un·e membre" default="membre" source=$doc} {input type="radio" name="base_receveur" value="membre" label="Un·e membre" default=1 source=$doc}
{input type="radio" name="base_receveur" value="client" label="Un·e client·e" source=$doc} {input type="radio" name="base_receveur" value="client" label="Un·e client·e" source=$doc}
</dd> </dd>
{/if} {/if}

View file

@ -45,15 +45,9 @@
function changeTypeSaisie(type) function changeTypeSaisie(type)
{ {
g.toggle(['.type_client', '.type_membre'], false); 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) { if (type) {
g.toggle('.type_' + type, true); g.toggle('.type_' + type, true);
document.querySelectorAll('.type_' + type + ' input, .type_' + type + ' select, .type_' + type + ' textarea').forEach((e) => {
e.disabled = false;
});
} }
} }
@ -82,11 +76,9 @@
} }
const selclient = document.querySelector('#f_client'); const selclient = document.querySelector('#f_client');
if (selclient) { selclient.addEventListener("change", () => {
selclient.addEventListener("change", () => { modifierContact(selclient, '#f_contact_list', '#f_nom_contact');
modifierContact(selclient, '#f_contact_list', '#f_nom_contact'); });
});
}
} ()); } ());
@ -94,18 +86,12 @@
// Hide type specific parts of the form // Hide type specific parts of the form
function hideAllTypes() { function hideAllTypes() {
g.toggle('[data-types]', false); 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 // Toggle parts of the form when a type is selected
function selectType(v) { function selectType(v) {
hideAllTypes(); hideAllTypes();
g.toggle('[data-types~=t' + v + ']', true); 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); g.toggle('[data-types=all-but-advanced]', v != 0);
// Disable required form elements, or the form won't be able to be submitted // Disable required form elements, or the form won't be able to be submitted
$('[data-types=all-but-advanced] input[required]').forEach((e) => { $('[data-types=all-but-advanced] input[required]').forEach((e) => {

View file

@ -25,7 +25,7 @@
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)} {if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
{linkbutton shape="delete" href="client_supprimer.php?id=%d"|args:$row.id label="Supprimer"} {linkbutton shape="delete" href="client_supprimer.php?id=%d"|args:$row.id label="Supprimer"}
{/if} {/if}
{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)} {if $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN)}
{linkbutton shape="edit" href="client_modifier.php?id=%d"|args:$row.id label="Modifier"} {linkbutton shape="edit" href="client_modifier.php?id=%d"|args:$row.id label="Modifier"}
{/if} {/if}
{linkbutton shape="user" href="client.php?id=%d"|args:$row.id label="Fiche client"} {linkbutton shape="user" href="client.php?id=%d"|args:$row.id label="Fiche client"}

View file

@ -38,27 +38,14 @@
{input type="checkbox" name="logo" value="1" source=$conf label="Imprimer le logo de l'association"} {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} {input type="textarea" class="full-width" rows="5" name="footer" source=$conf label="Pied de document — informations légales" required=true}
</dl> </dl>
<fieldset>
<legend>Présentation des factures</legend>
<dl>
{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"}
</dl>
</fieldset>
<fieldset> <fieldset>
<legend> <legend>
Choisir les champs à faire figurer sur la facture ou le devis pour l'adresse d'un membre Choisir les champs à faire figurer sur la facture ou le devis pour l'adresse d'un membre
</legend> </legend>
<dl> <dl>
{input type="select" name="adresse_client" label="Adresse" required=false options=$champsPaheko source=$conf} {input type="select" name="adresse_client" label="Adresse" required=true options=$champsPaheko source=$conf}
{input type="select" name="code_postal_client" label="Code postal" required=false 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=false options=$champsPaheko source=$conf} {input type="select" name="ville_client" label="Ville" required=true options=$champsPaheko source=$conf}
</dl> </dl>
</fieldset> </fieldset>
</fieldset> </fieldset>
@ -73,6 +60,7 @@
F = Facture, D = Devis F = Facture, D = Devis
</dd> </dd>
</dl> </dl>
<i>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 ! </i>
</fieldset> </fieldset>
<p class="submit"> <p class="submit">

View file

@ -16,7 +16,7 @@
</fieldset> </fieldset>
<p class="submit"> <p class="submit">
{csrf_field key=$csrf_key} {csrf_field key="delete_doc_"|cat:$doc.id}
<input type="submit" name="delete" value="Supprimer &rarr;" /> <input type="submit" name="delete" value="Supprimer &rarr;" />
</p> </p>

View file

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