Projet : web/cabaret/ (site PHP SiteBuilder / Smarty — VPS test.informatux.com)
Montage : projet monté en SSHFS sur le poste local (/home/patrice/VPS/informatux_test/cabaret/) — les commandes git sont lentes car elles opèrent sur un système de fichiers réseau ; utiliser git update-index --add <fichiers> + git commit plutôt que git add . ou git status (timeout quasi-systématique)
Objectif : Intégration de la facturation électronique Factur-X EN 16931 (CII)
Date de mise en place : mai–juin 2026
Git : repo dans web/cabaret/ — les modifications sont à committer sur le Synology perso
backcabaret/inc/class/sbuiadmin-facturx.phpClasse sbfacturx extends sanitize — créée from scratch, aucune dépendance externe sauf mPDF 8.
| Méthode | Rôle |
|---------|------|
| fetchOrderData($order_id) | Charge commande + client + produits depuis la BDD (tables sb_shop_order, sb_shop_order_detail, sb_account, sb_shop_product, sb_config) |
| _parseProducts() | Décode le JSON TVA stocké dans sb_shop_product.tva (champs tva1/tva2/tva3 avec libelle, nom, taux, montant_ht) |
| _computeTotals() | Regroupe la TVA par taux, calcule base HT / montant TVA / total TTC. Absorbe un écart de ≤ 0,02 € dans le groupe le plus chargé (pratique comptable standard) |
| buildHTML() | Construit le HTML 2 pages (CSS inline, logo, en-tête, lignes produits, TVA, RIB, conditions) compatible mPDF |
| buildXML() | Génère le XML CII EN 16931 complet (ExchangedDocument, SupplyChainTradeTransaction, vendeur, acheteur, lignes, TVA, totaux) |
| generatePDF() | mPDF 8 (PDFA=true, PDFAauto=true) → embed XML Factur-X via mise à jour incrémentale PDF |
| _embedXMLInPDF() | Post-traitement PDF pur PHP : ajoute EmbeddedFile stream, FileSpec, XMP metadata (PDF/A-3b + Factur-X extension), OutputIntent ICC (sRGB), nouveau Catalog avec /AF et /Names |
| _extractCatalogDict() | Extraction du Catalog par bracket-counting (robuste aux dicts imbriqués mPDF 8) |
| _findIccProfile() | Cherche un profil ICC sRGB sur le système (7 chemins) ou dans le répertoire de la classe |
| stream($order_id) | Génère et envoie la facture au navigateur sans sauvegarde serveur (headers Content-Type, Content-Disposition inline) |
| sendToPennylane($order_id) | Envoi vers Pennylane API v2 via import PDF Factur-X (POST /customer_invoices/e_invoices/imports), met à jour pennylane_sent_at sur HTTP 201 |
| _numberToWords($n) | Conversion entier → lettres français (jusqu'à 999 999) |
Format : {PREFIX}{YYYYMMDD}{ID sur 6 chiffres} — PREFIX lu depuis sb_config.config = 'tva-prefix' (défaut : FAC)
vendor/mpdf804/vendor/autoload.phpbackcabaret/inc/sbuiadmin-header.phpLigne modifiée (L.64) : ajout de 'facturx' dans le tableau $sbuiadmin_classes
// Avant
$sbuiadmin_classes = array('sql', 'sanitize', 'users', 'account', 'mail', 'csv', 'medias', 'form', 'page', 'pagination', 'upgrade');
// Après
$sbuiadmin_classes = array('sql', 'sanitize', 'users', 'account', 'mail', 'csv', 'medias', 'form', 'page', 'pagination', 'upgrade', 'facturx');
La classe est donc disponible dans tout le back-office sans include_once supplémentaire.
backcabaret/datas/modules/shop.phpDeux nouveaux case ajoutés dans le switch d'actions :
case "invoice" (L.907–911)Téléchargement direct de la facture depuis le back-office admin :
case "invoice":
$order_id = intval($_GET['id'] ?? 0);
if (!$order_id) die('Commande invalide');
(new sbfacturx())->stream($order_id);
break;
case "sendinvoice" (L.913–942)Envoi de la facture par email au client depuis le back-office :
1 (validée)sys_get_temp_dir() pour la pièce jointe$sbaccount->sendEmail('order-send-invoice', $datas_inv) avec l'array attachment@unlink)?sendinvoice=1 (succès) ou 0 (échec)backcabaret/datas/modules/tpls/shop.tpl{if $order.status == '1'}
<i class="fa fa-file-pdf-o"></i>
<a href="{$module_url}&a=invoice&id={$order.id}" target="_blank">Télécharger la facture</a>
{/if}
Affiché uniquement pour les commandes validées (status = 1).
<a class="glyphicon glyphicon-envelope"
onclick="return confirm('Souhaitez-vous envoyer la facture à ce client ?')"
href="{$module_url}&a=sendinvoice&id={$order.id}"
title="Envoyer la facture par email"></a>
{if $smarty.get.sendinvoice == '1'}
[message succès]
{elseif $smarty.get.sendinvoice == '0'}
[message échec]
{/if}
datas/modules/account/account.phpcase "invoice" dans l'espace client (L.349–358)case "invoice": // Facture PDF Factur-X
$invoice_oid = intval($_GET['id']);
if (!$invoice_oid) die();
// Vérification ownership : la commande appartient bien au client connecté
$own_q = "SELECT t1.id FROM $t_order AS t1
LEFT JOIN $t_acct AS t2 ON (t1.client_uid = t2.uid)
WHERE t1.id = '$invoice_oid'
AND t2.email = '" . $sbsql->escape_string($sbuiadmin_user_email) . "'";
if (!$sbsql->assoc($sbsql->query($own_q))) die('Accès refusé');
include_once(SB_ADMIN_DIR . 'inc/class/sbuiadmin-facturx.php');
(new sbfacturx())->stream($invoice_oid);
break;
Sécurité : vérification ownership avant de streamer — un client ne peut télécharger que ses propres factures.
Note : la classe est ici chargée avecinclude_oncecar le front n'a pas accès au header back-office.
URL d'accès front : https://[domaine]/account/?ac=invoice&id={order_id}
datas/modules/account/tpls/otpls/account/orders.tpl{if $order.status == '1'}
<table class="table">
<tbody>
<tr>
<td>Votre facture</td>
<td style="text-align: right;">
<a href="{$smarty.const.SB_URL}account/?ac=invoice&id={$order.id}" target="_blank">
<i class="fa fa-download"></i> Télécharger votre facture
</a>
</td>
</tr>
</tbody>
</table>
{/if}
Affiché uniquement si la commande est validée (status = 1).
datas/modules/account/tpls/emails/default_client.phpTemplate email client mis à jour pour supporter l'envoi de facture (cas order-send-invoice).
La facture est transmise en pièce jointe via le paramètre attachment de sendEmail().
files/Les PDFs générés à la volée lors des tests sont stockés dans :
files/2026/05/ — tests de mai 2026 (format YYYYMMDD-prenom-nom-N-timestamp.pdf)files/2026/06/ — tests de juin 2026Ces fichiers sont générés uniquement lors du streaming brouillon — en production normale, le PDF n'est pas sauvegardé sur le serveur.
| Fichier | Type | Nature de la modification |
|---------|------|--------------------------|
| backcabaret/inc/class/sbuiadmin-facturx.php | Créé | Classe complète Factur-X EN 16931 |
| backcabaret/inc/sbuiadmin-header.php | Modifié | Ajout 'facturx' dans $sbuiadmin_classes (L.64) |
| backcabaret/datas/modules/shop.php | Modifié | Ajout case "invoice" et case "sendinvoice" (L.907–942) |
| backcabaret/datas/modules/tpls/shop.tpl | Modifié | Boutons téléchargement + envoi email + messages retour |
| backcabaret/datas/modules/tpls/shop_bar.tpl | Modifié | (mineur — vérifier au commit) |
| datas/modules/account/account.php | Modifié | Ajout case "invoice" avec vérification ownership (L.349–358) |
| datas/modules/account/tpls/otpls/account/orders.tpl | Modifié | Ajout bouton téléchargement facture (L.212–227) |
| datas/modules/account/tpls/emails/default_client.php | Modifié | Support template email order-send-invoice |
| datas/modules/boutique/inc/functions.php | Modifié | (vérifier — présence dans les fichiers récents) |
46c6029 — 2026-06-25)Ajout de l'affichage des remises/avoirs sur la facture Factur-X pour toutes les commandes ayant un code promo.
sbuiadmin-facturx.php| Méthode | Modification |
|---------|-------------|
| fetchOrderData() | Extraction du promo_code / promo_type / promo_description / promo_price depuis sb_shop_order_detail → stockés dans $this->order |
| _parseProducts() | Transmission des champs promo dans le tableau produit |
| _computeTotals() | Calcul de promo_ttc et promo_ht + répartition proportionnelle par groupe TVA ; retournés dans le tableau de totaux |
| _htmlPage1() | Ligne colorée (fond rose) après les produits : label, description, code, montant HT négatif |
| _htmlPage2() | Ligne "Remise" en TTC dans le tableau des totaux (entre Total HT et TVA) |
| buildXML() | SpecifiedTradeAllowanceCharge EN 16931 + AllowanceTotalAmount dans les totaux monétaires |
| Type | Libellé | Calcul promo_ttc | Affichage facture |
|------|---------|-------------------|-------------------|
| 1 | Remise fixe produit | promo_price (TTC) | Montant HT négatif |
| 2 | Remise fixe panier | promo_price (TTC) | Montant HT négatif |
| 3 | Remise % | products_ttc × promo_price / 100 | Montant + pourcentage entre parenthèses |
| 4 | Livraison offerte | Pas de calcul (transport = 0) | Ligne "Livraison offerte — code: XXX" sans montant |
| 5 | Remise client | promo_price (TTC) | Montant HT négatif |
| 6 | E-carte cadeau | promo_price (TTC) | Montant HT négatif |
| 7 | Remise cumulée | promo_price (TTC) | Montant HT négatif |
Note type 3 :
promo_priceen BDD = valeur du pourcentage (ex.10pour 10%), pas un montant euro.
Arrondis : la répartition HT est proportionnelle à la base HT de chaque groupe TVA ; les écarts ≤ 0,02 € sont absorbés par le mécanisme existant.
86da3f9 — 2026-06-26)backcabaret/datas/modules/shop.php — nouveau case "sendpennylane"Instancie sbfacturx, appelle sendToPennylane($order_id), puis redirige vers la liste des commandes avec :
sendpennylane=1&plid={id_pennylane} — succèssendpennylane=0&plid=HTTP {code} — {détail} — erreurbackcabaret/datas/modules/tpls/shop.tpl<a class="glyphicon glyphicon-cloud-upload"> avec confirmation JS avant envoialert-success si sendpennylane=1 (affiche l'id Pennylane), alert-danger si sendpennylane=0 (affiche le détail de l'erreur)La méthode
sendToPennylane()danssbuiadmin-facturx.phpétait déjà commitée (token lu depuissb_config.pennylane-api-key, endpoint POSTapi/external/v2/customer_invoices, 1 ligne par groupe TVA, mise à jourpennylane_sent_atsur HTTP 201).
548e655 — 2026-06-26)| Fichier | Bug | Correction |
|---------|-----|-----------|
| datas/modules/contact/contact.php | SMTPSecure = sbGetConfigContact(...) > 0 → toujours false | Valeur string directe ('tls'/'ssl') |
| datas/modules/contact/contact.php | $PHPMailer->Send() non vérifié → succès affiché même si l'envoi échoue | Check $status, affiche $PHPMailer->ErrorInfo en cas d'échec |
| datas/modules/contact/contact.php | $responseData->error (variable indéfinie si reCAPTCHA échoue) | Corrigé en $response->{'error-codes'}[0] |
| backcabaret/inc/class/sbuiadmin-account.php | Même bug SMTPSecure > 0 | Même correction |
Le formulaire de contact affichait systématiquement "message envoyé" même en cas d'erreur SMTP — le client ne recevait rien sans message d'erreur côté front.
263eadb — 2026-06-26)Le formulaire de contact passe par inc/functions.php (rendu via shortcode dans les blocs CMS), pas par contact.php ni contact_ajax.php. Trois bugs corrigés dans les 3 fichiers concernés :
| Fichier | Bug | Correction |
|---------|-----|-----------|
| inc/functions.php | SMTPSecure > 0 → toujours false | Valeur string directe |
| inc/functions.php | FROM = email visiteur (domaine non vérifié Brevo) → rejet silencieux | FROM = email_smtp_username (expéditeur vérifié) ; Reply-To = email visiteur |
| inc/functions.php | Send() non vérifié → succès affiché même si échec | Check $status, affiche $phpemail->ErrorInfo si échec |
| contact_ajax.php | Idem SMTPSecure + mail() natif PHP + erreur silencieuse | Passage à sendMailer() + check retour |
| contact.php | Idem SMTPSecure + erreur silencieuse | sendMailer() + check retour |
| sbuiadmin-account.php | Idem SMTPSecure > 0 | Même correction |
Testé et validé : email reçu via
smtp-relay.sendinblue.comsurinformatux@e.email.
Penser à remettre l'adresse email du client en destination une fois les tests terminés (backcabaret/index.php?p=contact).
da689ef — 2026-06-26)sbuiadmin-facturx.php| Méthode | Modification |
|---------|-------------|
| fetchOrderData() | Requête supplémentaire sur sb_shop_payment pour récupérer title via order.payment (id) — stocké dans $this->order['payment_title'] ; fallback 'Carte bancaire' si id absent |
| _htmlPage2() | Section "Règlement" : utilise $o['payment_title'] au lieu de la chaîne codée en dur 'Carte bancaire' |
| buildXML() | Note <ram:IncludedNote SubjectCode="PMT"> : utilise $o['payment_title'] au lieu de 'carte bancaire' |
Demande HTTP (admin ou client)
│
▼
sbfacturx::stream($order_id)
│
├─ fetchOrderData() ← BDD (commande + client + produits + TVA JSON)
│
├─ _parseProducts() ← Décode tva1/tva2/tva3 par produit
│
├─ _computeTotals() ← Regroupe TVA par taux, calcule totaux
│
├─ generatePDF()
│ ├─ buildHTML() ← HTML 2 pages (CSS inline, mPDF-compatible)
│ ├─ mPDF 8 ← HTML → PDF binaire (PDFA-3b)
│ ├─ buildXML() ← XML CII EN 16931
│ └─ _embedXMLInPDF() ← Mise à jour incrémentale PDF (xref, Catalog, XMP, ICC)
│
└─ Output navigateur ← Content-Type: application/pdf (inline, sans sauvegarde)
327351b — 2026-06-29)backcabaret/datas/modules/shop.phpAjout du champ priceht (float) dans le formulaire add/edit produit, section PRIX, juste avant "Prix (sans réduction)".
| Point modifié | Détail |
|---|---|
| Sanitize POST | Lecture $_POST['priceht'] |
| INSERT | Colonne priceht ajoutée dans la requête |
| UPDATE | ,priceht = '$priceht' ajouté |
| Chargement edit | $priceht = $assoc['priceht'] |
| Reset formulaire | $priceht initialisé à vide (add et edit) |
| Input formulaire | Champ texte obligatoire, affiché avant "Prix (sans réduction)" |
La colonne
priceht FLOATa été créée directement en BDD par le client. TVA non modifiée — en attente du feu vert client.
b1fcb8e — 2026-06-29)backcabaret/datas/modules/shop.php| Point corrigé | Détail |
|---|---|
| INSERT adddiscount manquait uid | Ajout de uid = '0' — distingue les promos génériques des avoirs (uid > 0) |
| Message d'erreur doublon opaque | Détection Duplicate entry MySQL → message "Ce code promo existe déjà." |
| Listing promos (a=discount) | Requête WHERE uid = '0' OR uid IS NULL — les anciens enregistrements (avant colonne uid) réapparaissent |
L'ajout de la colonne
uid(commit67a2af3) avait rendu invisibles tous les codes promos créés avant cette date (uid = NULL). Le filtreOR uid IS NULLles récupère sans toucher aux données.
6f5c032 — 2026-06-29)| Fichier | Modification |
|---------|-------------|
| backcabaret/inc/class/sbuiadmin-facturx.php | sendToPennylane() : ajout pennylane_id = '$pl_id' dans l'UPDATE sur HTTP 201 |
| backcabaret/cron/pennylane_weekly.php | Même ajout dans la boucle d'envoi cron |
La colonne
pennylane_ida été créée directement en BDD par le client dansCa07bar8_sb_shop_order. Script ponctueltmp/pl_getid.phputilisé pour rétrospectivement retrouver et insérer en BDD l'ID Pennylane de la commande 23607 (envoyée avant l'ajout du save automatique), supprimé après usage.
d8b98fe — 2026-06-29)| Fichier | Corrections |
|---------|------------|
| backcabaret/inc/class/sbuiadmin-facturx.php | sendToPennylane() entièrement corrigé + nouvelle méthode _getPennylaneCustomerId() |
| backcabaret/cron/pennylane_weekly.php | Mêmes corrections + fonction getPennylaneCustomerId() |
| tmp/pl_test.php | Supprimé après tests |
| Avant (incorrect) | Après (correct) |
|---|---|
| invoice_lines_attributes | invoice_lines |
| customer_attributes: {name, emails_attributes} | customer_id (entier Pennylane) |
| raw_currency_unit_price: float | raw_currency_unit_price: "string" |
| invoice_number: "FACT-..." présent | Absent (interdit sur brouillon, assigné à la finalisation) |
| section_rank, rank dans les lignes | Supprimés |
| $resp_data['invoice']['id'] | $resp_data['id'] |
_getPennylaneCustomerId()Recherche paginée dans l'API Pennylane (GET /customers, 50 par page, max 500) avec correspondance partielle insensible à la casse. Si le client est introuvable, l'envoi échoue avec un message explicite invitant à créer le client dans Pennylane.
Commandes #23608 (237,15 €) et #23604 (93,00 €) envoyées en brouillon → HTTP 201, pennylane_sent_at mis à jour en BDD.
5b0a1a9 — 2026-06-29)backcabaret/inc/class/sbuiadmin-facturx.phpClasse CSS .conds : font-size passé de 7pt à 6pt.
Concerne les trois blocs texte en bas de facture :
Texte toujours lisible à 6pt mais visuellement plus discret, laissant plus de place au contenu facturation.
9892d34 — 2026-07-01)backcabaret/datas/modules/shop.phpSuppression des 3 champs de saisie manuelle tva1_montant_ht, tva2_montant_ht, tva3_montant_ht dans le formulaire add/edit produit (section TVA).
Les montants HT par taux seront dorénavant calculés automatiquement depuis le champ priceht du produit (ajouté au commit 327351b), et non plus saisis manuellement.
Les variables PHP de traitement POST (
$tva1_montant_ht, etc.) et la structure JSON en BDD (tva.tva1.montant_ht) sont conservées en l'état — le calcul automatique est implémenté danssbuiadmin-facturx.php(voir §21).
9892d34 — 2026-07-01)backcabaret/inc/class/sbuiadmin-facturx.php| Point modifié | Détail |
|---|---|
| fetchOrderData() — requête $q2 | Ajout de t2.priceht dans le SELECT |
| _parseProducts() — filtre TVA | Condition !isset($t['montant_ht']) \|\| $t['montant_ht'] === '' remplacée par empty($t['taux']) — une ligne TVA est active si elle a libellé et taux |
| _parseProducts() — calcul pu_ht | Si montant_ht est présent en base (anciens produits) → utilisé tel quel (rétrocompatibilité). Sinon → pu_ht = priceht du produit |
Règle en production : un produit = un seul taux TVA actif (une seule des tva1/tva2/tva3 renseignée), donc pu_ht = priceht sans ambiguïté. Pour les rares anciens produits multi-TVA avec montant_ht en JSON, les valeurs stockées sont conservées.
893890e — 2026-07-01)Refonte complète de l'envoi Pennylane : le PDF Factur-X généré est envoyé directement à Pennylane via POST /customer_invoices/e_invoices/imports (multipart/form-data). Pennylane extrait automatiquement toutes les lignes et montants depuis le XML EN 16931 embarqué dans le PDF — plus aucun calcul manuel de lignes TVA côté PHP.
backcabaret/inc/class/sbuiadmin-facturx.php| Point modifié | Détail |
|---|---|
| fetchOrderData() | Ajout t2.firstname, t2.lastname dans le SELECT commande + compte client |
| $this->order | Ajout client_firstname et client_lastname (décodage HTML entities) |
| _getPennylaneCustomerId() | Retourne désormais ['id' => int, 'type' => string] au lieu d'un simple int — permet de choisir le bon endpoint PUT (individual_customers vs company_customers) |
| _updatePennylaneCustomer() | Nouvelle méthode — PUT individual_customers/{id} ou company_customers/{id} avec first_name, last_name, billing_address (objet imbriqué avec address, postal_code, city, country_alpha2), emails |
| sendToPennylane() | Entièrement réécrite : suppression du bloc invoice_lines manuel → generatePDF() → fichier tmp → POST /customer_invoices/e_invoices/imports multipart → suppression tmp → parse id réponse HTTP 201 |
Endpoint utilisé : https://app.pennylane.com/api/external/v2/customer_invoices/e_invoices/imports
Payload : file (CURLFile PDF) + invoice_options (JSON : {"customer_id": 42})
Réponse succès : HTTP 201 {"id": 24590380871680, "url": "..."}
Note : l'endpoint e-invoice import ne supporte pas le champ
draft— testé et confirmé (HTTP 400NotExistPropertyDefinition). Champ retiré. En recette, supprimer manuellement la facture créée dans Pennylane après test.
backcabaret/cron/pennylane_weekly.phpRefonte complète — réduit de ~400 à ~155 lignes. Les fonctions buildPennylaneLines(), getPennylaneCustomerId(), updatePennylaneCustomer() supprimées. Le cron bootstrappe maintenant la classe et délègue entièrement :
// Bootstrap (constantes + adaptateur PDO→$sbsql + stub sanitize + include)
define('SBUIADMIN_PATH', true);
define('SB_PATH', dirname(dirname(__DIR__)) . '/');
define('_AM_DB_PREFIX', $prefix);
define('_AM_SITE_URL', $site_url);
$sbsql = new class($pdo) { ... }; // query/assoc/toarray/escape_string
require_once __DIR__ . '/../inc/class/sbuiadmin-facturx.php';
// Boucle
$result = (new sbfacturx())->sendToPennylane($oid);
Commande test envoyée depuis le back-office → HTTP 201, Pennylane id 24590380871680, pennylane_sent_at mis à jour en BDD.
8b0e32d — 2026-07-01)backcabaret/cron/pennylane_weekly.phpAjout de unset($facturx) + gc_collect_cycles() après chaque envoi dans la boucle. mPDF contient des références cycliques internes qui ne sont pas libérées immédiatement par le comptage de références PHP — le GC cyclique explicite maintient une empreinte mémoire stable quel que soit le nombre de commandes à traiter (utile notamment lors de la première exécution en production si un backlog important s'est accumulé).
735a952 — 2026-07-01)backcabaret/datas/modules/shop.phpHeader du tableau produits : ajout de 'Prix HT' avant la colonne prix TTC, et renommage explicite de "Prix $text_tva" en 'Prix TTC'.
backcabaret/datas/modules/tpls/shop.tplAjout d'une <td> affichant $product.priceht (formaté avec séparateur décimal et symbole monnaie) avant la colonne "Prix TTC" dans le tableau des produits.
735a952 — 2026-07-01)backcabaret/datas/modules/shop.php$query_0 (commandes) : ajout de t1.pennylane_sent_at dans le SELECT$allorders : propagation du champ pennylane_sent_atbackcabaret/datas/modules/tpls/shop.tplDans la cellule "Date Commande", affichage conditionnel sous la date :
{if $order.pennylane_sent_at}
<br><small style="color: green;">Envoyé Pennylane le {$order.pennylane_sent_at|date_format:"%d/%m/%Y"}</small>
{/if}
Aucune colonne supplémentaire — l'information s'affiche en vert en petite taille uniquement si pennylane_sent_at est renseigné.
cb363a2 — 2026-07-01)Jusqu'ici, seul l'envoi manuel (sendinvoice back-office) ou le téléchargement (front/back) donnaient accès à la facture Factur-X. Le mail de confirmation de commande envoyé après un paiement réussi ne contenait pas la facture.
vendor/monetico/monetico-return.phpLe mail order-after-payment n'est en réalité envoyé que depuis la notification serveur-à-serveur de Monetico (Phase 2 — cas payetest et paiement du switch sur code-retour), pas depuis le retour navigateur du client (boutique.php, case paymenty/paymentr/paymenta → sbGetPaymentReturn()), qui n'envoie jamais l'email quand $smarty = true (garde volontaire pour éviter un double envoi).
Dans les deux cas payetest et paiement, juste avant $sbaccount->sendEmail("order-after-payment", $after_payment_datas) :
sbfacturx::fetchOrderData() + generatePDF(), même pattern que sendinvoice dans shop.php)sys_get_temp_dir()/facture_{oid}.pdf)$after_payment_datas['attachment']Le tout est enveloppé dans un try/catch : une erreur de génération de facture n'empêche pas l'envoi du mail de confirmation (dégradation gracieuse) ; l'erreur est tracée dans logs/monetico_facturx_errors.txt.
Le mail admin (
order-after-paymentcôté admin) n'a volontairement pas la pièce jointe — comportement existant conservé danssendEmail()(sbuiadmin-account.php).
La notification serveur-à-serveur Monetico n'arrivait jamais sur monetico-return.php (confirmé via traces de debug temporaires — aucune n'était atteinte, pas même la toute première ligne du fichier). Cause : l'URL de l'interface retour commerçant configurée dans le back-office Monetico était incorrecte. Corrigée par Patrice en https://test.informatux.com/cabaret/vendor/monetico/monetico-return.php.
Sans cette URL correcte, aucun mail de confirmation de commande (
order-after-payment) n'a jamais dû partir, indépendamment de la facture — bug préexistant, pas une régression de cette fonctionnalité.
Paiement test Monetico → email de confirmation reçu avec la facture Factur-X en pièce jointe. Validé par Patrice le 2026-07-01.
cb363a2 — 2026-07-01)customers:all — envoi Pennylane opérationnel (commandes 23608 et 23604 envoyées en brouillon, HTTP 201)893890e — 2026-07-01)37dccb5 — 2026-06-26)2e393e6 — 2026-07-02, section 17 : 3 commandes réelles traitées avec --allow-recette --since=, 0 erreur)sendToPennylane() + cron hebdomadaire (commit ad259b8 — 2026-06-26)67ccc26 — 2026-06-25)a607758 — 2026-06-25)order-send-invoice testé — rendu OK avec pièce jointe PDFshop_bar.tpl et boutique/inc/functions.php vérifiés — aucune modification non committéesbuiadmin-cart.php L.338) (commit a8c6548 — 2026-06-25)46c6029 — 2026-06-25)6b4e06f — 2026-06-25)67a2af3 — 2026-06-25)d5d04fb — 2026-06-25)0c24ca4 — 2026-06-25)b1ccbaf — 2026-06-25)51ceff5 — 2026-06-25)47c7490 — 2026-06-25)c09f990 — 2026-06-25)bc1545d — 2026-06-25)89afbb4 — 2026-06-25)44f99a3 — 2026-06-25)backcabaret/datas/modules/tpls/shop.tpl (commit d5d04fb — 2026-06-25)Amélioration UX : le <select id="uid"> du formulaire d'ajout/modification d'avoir est maintenant un dropdown recherchable via Select2.
shop.tplBloc ajouté juste avant <!-- Page-Level Scripts --> :
{if $smarty.get.a == 'addavoir' || $smarty.get.a == 'editavoir'}
<link rel="stylesheet" href="{$smarty.const.SB_ADMIN_URL}inc/js/editor/ckeditor/plugins/ckawesome/resources/select2/select2.full.min.css">
<script src="{$smarty.const.SB_ADMIN_URL}inc/js/editor/ckeditor/plugins/ckawesome/resources/select2/select2.full.min.js"></script>
{/if}
Initialisation dans $(document).ready() :
{if $smarty.get.a == 'addavoir' || $smarty.get.a == 'editavoir'}
$('#uid').select2({
placeholder: "Choisissez un client",
allowClear: true,
width: '350px',
language: { noResults: function() { return "Aucun résultat"; } }
});
{/if}
Fichiers Select2 utilisés (déjà présents dans le projet) :
backcabaret/inc/js/editor/ckeditor/plugins/ckawesome/resources/select2/select2.full.min.jsbackcabaret/inc/js/editor/ckeditor/plugins/ckawesome/resources/select2/select2.full.min.cssPrérequis : SARL CABARET LE LIVE doit avoir un abonnement Pennylane actif.
cabaret-facturx)INSERT INTO sb_config (config, content)
VALUES ('pennylane-api-key', 'TON_TOKEN_ICI');
La méthode
sendToPennylane()danssbuiadmin-facturx.phplira cette valeur via une requête SQL directe sursb_config.
L'endpoint validé (2026-06-26) :POST https://app.pennylane.com/api/external/v2/customer_invoices
Suite à un email de Monetico demandant de vérifier l'implémentation du calcul MAC (section 4.3 de leur documentation).
| Fichier | Rôle |
|---------|------|
| vendor/monetico/monetico.class.php | Kit officiel Monetico v4.0 (2014) — classes MoneticoPaiement_Ept et MoneticoPaiement_Hmac |
| vendor/monetico/monetico.config.php | Configuration TPE (clé, numéro, code société, URLs) |
| vendor/monetico/monetico-return.php | Réception et validation des notifications Monetico (Phase 2) |
| datas/modules/boutique/boutique.php | Construction du formulaire de paiement (Phase 1, case "5") |
monetico.config.php)07828903.0cabaretlelhttps://p.monetico-services.com/https://p.monetico-services.com/test/monetico-return.php utilise tous les paramètres dynamiquement :
$MoneticoPaiement_bruteVars = getMethode(); // TOUS les POST/GET
unset($source['MAC']); // retire seulement MAC
ksort($source); // tri alphabétique obligatoire
return implode('*', $source); // séparateur *
Conforme à la section 4.3 — les futurs nouveaux paramètres Monetico seront automatiquement inclus.
monetico-return.php L.70 (commit a607758)// Avant (bugué) : anomalie générée quand la version CORRESPOND → MAC toujours KO
$anomalies .= $source["version"] == $oEpt->sVersion ? ":version" : null;
// Après (corrigé) : anomalie générée quand la version NE CORRESPOND PAS
$anomalies .= $source["version"] != $oEpt->sVersion ? ":version" : null;
backcabaret/datas/modules/shop.php (commit 67a2af3)| Correction | Détail |
|-----------|--------|
| Requête promos filtrée | Ajout WHERE uid = '0' — les avoirs (uid > 0) n'apparaissent plus dans le tableau des promos |
| Champ Client obligatoire | openSelect(..., true) → attribut required natif du framework sur le select client du formulaire addavoir |
Mécanisme :
sb_shop_discountsert aux deux entités. La distinction se fait viauid:0= promo générique,> 0= avoir lié à un client.
boutique.php (commit 6b4e06f)| Correction | Détail |
|-----------|--------|
| Suppression url_retour | Ce champ n'existe plus dans la doc v2.0 ; seuls url_retour_ok et url_retour_err sont reconnus. Retiré du calcul MAC Phase 1 ET du formulaire HTML. |
| Suppression utf8_encode() | Deprecated PHP 8.2+, double-encodait du UTF-8 déjà correct |
| contexte_commande enrichi | Ajout firstName dans billing et shipping, ajout country + email dans shipping (3DS v2 friction reduction) |
SHA1 et protocole 3.0 inchangés : la doc v2.0 (Fév. 2025) confirme explicitement RFC2104 HMAC SHA1 et version 3.0.
monetico.class.phpetmonetico-return.phpnon modifiés.
Les objets git créés par le serveur web (http) bloquent les push SSH de patrice si le hash du commit commence par les mêmes 2 caractères qu'un objet http existant. Contournement : varier légèrement le message de commit pour obtenir un hash différent.
sb_config WHERE config = 'pennylane-api-key'/customer_invoices → token fonctionnelhttps://app.pennylane.com/api/external/v2/customer_invoicesALTER TABLE Ca07bar8_sb_shop_order
ADD COLUMN pennylane_sent_at DATETIME NULL DEFAULT NULL AFTER comment;
Permet de tracer chaque envoi et d'éviter les doublons entre exécutions.
sendToPennylane($order_id) — sbuiadmin-facturx.phpMéthode implémentée (usage depuis le back-office, envoi manuel d'une commande) :
sb_config$this->totals['groups'] — promo et transport déjà distribués par _computeTotals())vat_rate Pennylane : 5.5% = FR_55, 10% = FR_100, 20% = FR_200, 2.1% = FR_21, 0% = FR_0https://app.pennylane.com/api/external/v2/customer_invoices['status' => 'ok', 'http' => 201, 'pennylane_id' => ...] sur succèspennylane_sent_at = NOW() en BDD sur HTTP 201backcabaret/cron/pennylane_weekly.phpScript PHP autonome (PDO direct, sans framework SiteBuilder — sql extends Smarty incompatible CLI).
Fonctionnement :
backcabaret/inc/admin/settings.txtsb_configstatus = 1 (payées) avec pennylane_sent_at IS NULL_computeTotals() via PDO, construit les lignes, poste vers Pennylanepennylane_sent_at uniquement sur HTTP 201 (les erreurs sont retentées à la prochaine exécution)cron/pennylane.log via crontab)Protection CLI : if (PHP_SAPI !== 'cli') { http_response_code(403); exit; }
crontab -e)0 10 * * 1 /usr/bin/php /home/patrice/VPS/informatux_test/cabaret/web/cabaret/backcabaret/cron/pennylane_weekly.php >> /home/patrice/VPS/informatux_test/cabaret/web/cabaret/backcabaret/cron/pennylane.log 2>&1
| Fichier | Nature |
|---------|--------|
| backcabaret/inc/class/sbuiadmin-facturx.php | sendToPennylane() implémentée |
| backcabaret/cron/pennylane_weekly.php | Créé — script cron autonome |
| BDD Ca07bar8_sb_shop_order | Colonne pennylane_sent_at ajoutée |
buildHTML() — pagination conditionnelle<pagebreak /> + header répété sur la page 2if (count($this->order['products']) >= 2) {
$html .= '<pagebreak />';
$html .= $header . $p2;
} else {
$html .= $p2;
}
generatePDF() — marges réduites| Paramètre | Avant | Après |
|---|---|---|
| margin_bottom | 30 | 20 |
| margin_footer | 12 | 8 |
Gain de 14 mm en bas de page — évite qu'une facture mono-produit génère une 2e page parasite.
1a39964 — 2026-07-02)Le comptable a remonté un écart de TTC sur la commande #23608 : Pennylane recalculait mal le total car le XML Factur-X déclarait la remise sous un taux de TVA unique (le taux "dominant" du panier, celui avec la base HT la plus haute), alors que la remise s'applique en réalité sur tout le panier — qui mélange spectacle 20% / repas 10% / boisson 5,5%.
Confirmé avec Patrice : les codes promo s'appliquent bien sur tout le panier (pas uniquement sur le spectacle) — la correction est donc technique (refléter fidèlement la répartition déjà calculée par _computeTotals()), pas un changement de règle métier.
sbuiadmin-facturx.php_computeTotals() : chaque groupe de TVA garde désormais sa propre part de remise dans $g['promo_ht'] (au lieu de ne connaître que le total remisé global).buildXML() : la remise n'est plus déclarée en une seule ligne SpecifiedTradeAllowanceCharge au taux dominant, mais en une ligne par taux de TVA concerné (boucle sur $t['groups'], une ligne dès que $g['promo_ht'] > 0), chacune avec son propre RateApplicablePercent et CategoryCode.Reprendre l'investigation du delta TTC paiement/facture sur les commandes antérieures #23607 et #23603 — probablement le même mécanisme (remise multi-taux mal représentée), à vérifier cas par cas car commandes antérieures au fix.
1851601 — 2026-07-02)Investigation du delta TTC paiement/facture sur les commandes #23607 (payé 416,00 €, facturé 413,47 €) et #23603 (payé 474,00 €, facturé 471,46 €) — écarts de ~2,53-2,54 €, trop importants pour être de l'arrondi.
Cause identifiée et confirmée par reconstruction exacte du calcul : dans le JSON tva de deux produits (pid 22 — T-IE — FORMULE ALL INCLUSIVE et pid 24 — T-ST-IE — FORMULE SHOW TIME), certains champs montant_ht étaient enregistrés avec une virgule comme séparateur décimal (ex. "11,44444") au lieu d'un point. floatval() en PHP tronque à la virgule (floatval("11,44444") = 11), ce qui faussait le HT recalculé de plusieurs euros à chaque fois qu'une commande contenait un de ces deux produits.
Le reste du catalogue (6 produits au total sur ce site) est indemne — seuls pid 22 et 24 étaient concernés, vraisemblablement une erreur de frappe lors de la création de ces fiches par copie de leurs équivalents "billet cadeau" (pid 18, 19), qui eux ont les mêmes montants correctement formatés en point.
9892d34, 01/07)Ce commit avait retiré les 3 champs "Montant HT" par taux du formulaire produit, au profit d'un calcul automatique depuis le Prix HT global quand montant_ht est absent. Problème : ce fallback applique le Prix HT entier à chaque taux de TVA — correct pour un produit à taux unique, mais faux pour un produit multi-taux (spectacle/repas/boisson) où le HT doit être réparti, pas dupliqué. Aucun produit actuel n'était impacté par ce cas (tous ont un montant_ht renseigné), mais le risque restait latent pour tout nouveau produit multi-taux créé sans ces champs.
sbuiadmin-facturx.php — _parseProducts() normalise virgule → point à la lecture de montant_ht (str_replace(',', '.', ...) avant floatval()). Corrige immédiatement les factures des produits déjà corrompus en base, sans migration de données.shop.php — réintégration des 3 champs "TVA X - Montant HT" dans le formulaire produit (indispensables pour les produits multi-taux, le Prix HT seul ne suffit pas à reconstituer la ventilation). Normalisation virgule → point à la sauvegarde. Ajout d'un avertissement non bloquant si la somme des montants HT TVA ne correspond pas au Prix HT (détection précoce de ce type d'erreur de saisie).Test synthétique (Reflection, données réelles des deux commandes) : les deux commandes recalculent désormais exactement le montant payé — #23603 → 474,00 €, #23607 → 416,00 €. Confirmé par Patrice après test en recette.
2e393e6 — 2026-07-02)Le cron (pennylane_weekly.php, mis en place le 2026-06-26, section 11) se bloquait automatiquement dès qu'il détectait l'environnement de recette (test.informatux.com), pour éviter qu'un lancement manuel de test n'envoie tout l'historique des commandes payées non transmises. Mais ça empêchait aussi toute validation réelle du cron avant sa mise en production.
--allow-recette : lève le blocage automatique (jamais actif via la crontab de production, qui tourne sans arguments — comportement de prod strictement inchangé).--since=YYYY-MM-DD : limite le traitement aux commandes payées datées à partir de cette date (AND date >= :since dans la requête SQL).Sans les deux options ensemble, le script se bloque exactement comme avant.
php backcabaret/cron/pennylane_weekly.php --allow-recette --since=2026-07-01
3 commandes de juillet traitées avec succès (#23610, #23612, #23613), HTTP 201 à chaque fois, pennylane_id reçu, 0 erreur, aucune commande antérieure à juillet touchée. Factures vérifiées correctes côté Pennylane par Patrice.
Activer la crontab en production (entrée déjà documentée en section 11) — le blocage recette reste actif pour tout lancement non maîtrisé sur cet environnement. Bloqué en attente de la migration vers l'environnement de production, elle-même en attente du feu vert du client.
daef2a6 — 2026-07-02)Une fonctionnalité mise en place précédemment permet de définir un "Titre billet PDF" sur une fiche produit, qui remplace le "Titre fiche web" sur le e-billet généré, avec une police spéciale (autographyregular). Ça fonctionnait sur les billets datés (BI) mais pas sur les billets cadeau (BCI) : le nom du spectacle apparaissait bien, mais sans la police.
Dans sbGenerateClientEboxPDF() (datas/modules/boutique/inc/functions.php), le titre est stocké dans deux variables différentes selon le type de billet :
$billet_name, injecté dans {BILLET_NAME} avec la police autographyregular.$billet_name reste vide, le titre est plutôt concaténé dans $validity ({BILLET_VALIDITY}), sans style de police.Dans la branche BCI de sbGenerateClientEboxPDF() :
autographyregular appliquée au nom du spectacle.#billet_validity, augmenté progressivement 14 → 17 → 20pt sur validation visuelle) appliquées au texte de réservation (ebillet_txt_cadeau, configurable, texte par défaut "Merci de contacter le 02 43 70 05 82...") et au texte "Billet valable du..." (datevaliditytext), qui n'avaient jusque-là aucun style particulier.Vérifié : le billet daté (BI) n'a pas de point équivalent à corriger (aucune ponctuation n'est ajoutée dans cette branche du code).
Testé et confirmé par Patrice en recette à chaque étape (application de la police, paliers de taille, suppression du point).
942a642 — 2026-07-02)Sur les billets datés (BI), quand un "Titre billet PDF" est renseigné sur la fiche produit, seul ce titre doit s'afficher. Quand il est vide, le titre doit être précédé de "1 place pour " (ou du texte configurable ebillet_txt_date s'il est renseigné en config) suivi du titre web.
Dans sbGenerateClientEboxPDF(), le texte configurable ebillet_txt_date était vérifié avant la présence du Titre billet PDF : dès que ce texte était configuré, il s'affichait systématiquement en préfixe, même quand un Titre billet PDF était renseigné (cas où aucun préfixe ne doit apparaître).
Inversion de l'ordre des conditions : la présence du Titre billet PDF est vérifiée en premier. Le texte configurable / "1 PLACE POUR " ne s'applique que si aucun Titre billet PDF n'est renseigné.
Testé et confirmé par Patrice en recette, avec et sans Titre billet PDF renseigné.
8e89098 — 2026-07-02)Après le fix de police du 2026-07-02 (section 20), Patrice a testé plusieurs polices "signature" pour trouver le meilleur rendu visuel sur le nom du spectacle et les textes du BCI :
| Police | Verdict |
|---|---|
| autographyregular (Autography.ttf) | Police d'origine |
| amsterdamsignature (Amsterdam-Signature.ttf) | Testée, écartée ("moins belle") |
| highspirited (High-Spirited.ttf) | Testée, plutôt bien — pas de variante Bold disponible (fichier à graisse unique, font-weight: 900 sans effet) |
| betterlett (Betterlett.ttf) | Retenue |
| betterlettswash (Betterlett-Swash.ttf) | Testée, écartée ("illisible") |
Ajustements visuels sur betterlett (BCI) : taille du nom du spectacle 9pt, texte réservation/validité 17pt (paliers testés : 12→14→17→20→22→23→17 selon les allers-retours), espace de 8pt ajouté entre le nom du spectacle et le texte de réservation qui se collaient (span → <div style="display:block; margin-top:8pt;">).
mPDF 6.1 (vendor/mpdf/, utilisé par sbGenerateClientEboxPDF()) ignore complètement le CSS @font-face du template pdf_page_1.html — ce bloc n'a aucun effet sur le rendu. Le vrai chargement de police passe par la config interne de mPDF :
vendor/mpdf/config_fonts.php → tableau $this->fontdata, une entrée par police ('R'/'I'/'B'/'BI' → nom de fichier .ttf)..ttf doit être physiquement présent dans vendor/mpdf/ttfonts/.Tout vendor/ est exclu du dépôt git (.gitignore). Ces deux éléments (config_fonts.php et les .ttf dans ttfonts/) ne sont donc jamais commités et doivent être répliqués manuellement sur tout nouvel environnement — y compris lors de la future migration en production. Sans ça, le code s'exécutera sans erreur mais la police retombera sur la police par défaut, silencieusement.
datas/modules/boutique/inc/functions.php — constante ADMIN_PDF_FONTS_URL (URL des polices reconstruite dynamiquement depuis SB_URL, plus d'URL en dur à corriger en prod pour le @font-face cosmétique) ; police active betterlett + tailles/espacement.inc/tpls_pdf/pdf_page_1.html — @font-face pour les 4 polices testées (cosmétique, sans effet réel côté mPDF, cf. ci-dessus).inc/tpls_pdf/fonts/*.ttf — copies de référence des polices testées.vendor/mpdf/config_fonts.php — entrées fontdata pour autographyregular, amsterdamsignature, highspirited, betterlett, betterlettswash.vendor/mpdf/ttfonts/*.ttf — copies physiques des 5 fichiers de police.betterlett retenue le 2026-07-02, puis remplacée par Lobster le 2026-07-03 (voir section 28).vendor/mpdf/config_fonts.php + vendor/mpdf/ttfonts/ à la checklist de migration prod~~ — ajouté à la mémoire project_prod_migration (point 4 de la checklist).c2535f7 — 2026-07-02)Une commande dont la facture a été envoyée à Pennylane ne doit plus pouvoir être supprimée depuis le back-office (cohérence comptable — éviter un décalage entre les commandes locales et les factures déjà enregistrées chez Pennylane).
shop.tpl — la croix de suppression (colonne Actions du tableau des commandes) est masquée dès que pennylane_sent_at est renseigné sur la commande, remplacée par une icône grisée non cliquable avec tooltip explicatif.shop.php (case delorder) — blocage équivalent côté serveur : la suppression est refusée avec message d'erreur si pennylane_sent_at est renseigné, même en cas d'appel direct de l'URL de suppression (la protection visuelle seule ne suffisait pas à empêcher un appel URL direct).Testé et confirmé par Patrice en recette (commande transmise à Pennylane vs commande non transmise).
3910a13 — 2026-07-03)Sur la page front "Nos prochains spectacles" (theme/cabaret/inc/page-spectacles.html), le lien du bouton ACHETER était codé en dur dans le template avec un test sur la date ({if $date.date > '2026-06-28'} → produit 22, sinon produit 6) — un contournement temporaire, pas une vraie donnée admin.
Nouvelle colonne sur sb_dates (créée directement en BDD par Patrice, comme priceht/pennylane_id) :
ALTER TABLE Ca07bar8_sb_dates
ADD COLUMN link VARCHAR(255) NULL DEFAULT NULL AFTER products;
Stocke l'id du produit choisi (pas une URL brute), en texte.
backcabaret/datas/modules/dates.php| Point modifié | Détail |
|---|---|
| Nouveau select "Lien produit du bouton ACHETER" (obligatoire) | Même source de données que le select existant "Produit concerné par le changement de prix" (SELECT reference, title, id FROM sb_shop_product WHERE active = '1') — la requête produits est remontée une fois, réutilisée par les deux selects |
| Validation serveur | Si aucun produit sélectionné ($link <= 0), l'ajout/la modification est bloqué avec message d'erreur explicite, formulaire réaffiché avec les valeurs saisies (rien n'est perdu) |
| INSERT/UPDATE | Colonne link ajoutée (stocke l'id produit) |
| Chargement édition | $link = intval($assoc['link']) |
inc/cmscustom.php — insert_sbGetDates()LEFT JOIN sb_shop_product AS p ON (p.id = d.link) ajouté à la requête, avec alias p.title AS link_product_title — nécessaire pour construire le slug SEO de l'URL produit côté front sans requête supplémentaire. Fonction partagée par 3 templates (page-spectacles.html, page-accueil.html, boutique_display_product.tpl) — modification additive uniquement (toutes les colonnes d.* d'origine conservées), sans impact sur les deux autres usages.
theme/cabaret/inc/page-spectacles.htmlLe bouton ACHETER :
$date.link est renseigné (produit choisi en admin) → boutique/product/{id}/{titre-slug}?d={date_id} construit dynamiquement (même format d'URL que le reste du site, ex. boutique_item.tpl)Testé et confirmé par Patrice en recette le 2026-07-03.
6fd0d59 — 2026-07-03)betterlett → LobsterNouveau choix de police définitif du client (remplace betterlett, retenue le 2026-07-02 — cf. section 22). Confirmé par Patrice le 2026-07-03.
| Fichier | Modification |
|---------|-------------|
| inc/tpls_pdf/pdf_page_1.html | Ajout @font-face pour Lobster (cosmétique, sans effet réel côté mPDF 6.1 — cf. mécanisme réel section 22) |
| inc/tpls_pdf/fonts/Lobster-Regular.ttf | Ajouté (copie de référence versionnée) |
| datas/modules/boutique/inc/functions.php | sbGenerateClientEboxPDF() — les 4 font-family: (nom spectacle BI/BCI, texte réservation, texte "Billet valable du...") basculés sur Lobster. Taille uniformisée à 17pt partout, y compris le nom du billet ({BILLET_NAME} et nom du spectacle BCI), auparavant à 9pt |
Non versionné (à répliquer manuellement en prod, cf. [[project_prod_migration]]) :
vendor/mpdf/config_fonts.php— entréelobsterajoutée (fontdata['lobster']→Lobster-Regular.ttfpour R/I/B/BI) — etvendor/mpdf/ttfonts/Lobster-Regular.ttf(copie physique). Vérifiés présents en recette, hors dépôt git (.gitignoresurvendor/).
logo-color-green-payment| Fichier | Modification |
|---------|-------------|
| theme/cabaret/style-custom.css | Nouvelle classe .logo-color-green-payment { color: #0DA300 !important; } — distincte de .logo-color-green (qui reste noir) |
| datas/modules/boutique/tpls/checkout/step5-afterpayment.tpl | Le <h1> "Votre paiement a été accepté" (cas paymenty) utilise désormais logo-color-green-payment au lieu de logo-color-green |
index.php — handler debug erreurs fatalesAjout d'un register_shutdown_function() optionnel affichant le détail d'une erreur fatale (var_dump de error_get_last()) — utile pour le débogage en environnement de test. Contrôlé par la variable $fatal_handler, codée à false par défaut : aucun effet en l'état, à activer ponctuellement en modifiant cette valeur.
0dd01b5 — 2026-07-03)Sur la commande #23635 (1 585,00 € payés), les frais de livraison La Poste (4,00 €) n'apparaissaient pas sur la facture Factur-X — total facturé 1 581,00 € au lieu de 1 585,00 €.
À l'insertion des lignes de commande (boutique.php), chaque ligne porte son propre transport :
transport_title = "E-BOX"/"E-PASS", transport_price = 0 (L.903-905)Or sbfacturx::fetchOrderData() prenait le premier transport_title non vide et s'arrêtait. Sur une commande mixte commençant par un e-billet (cas #23635 : billet cadeau E-BOX 0 € puis formule datée La Poste 4 €), la facture retenait "E-BOX / 0 €" et ignorait les frais réels.
backcabaret/inc/class/sbuiadmin-facturx.phpfetchOrderData() — extraction du transport réécrite : priorité à la première ligne avec transport_price > 0 (le transport payé), sinon premier titre non vide en fallback (commande 100 % dématérialisée → "E-BOX" à 0 €, comportement inchangé). Le transport restant unique par commande (dupliqué sur les lignes physiques), il n'est compté qu'une fois — cohérent avec le calcul du checkout.
Test technique sur #23635 (bootstrap PDO + Reflection) : transport = La Poste 4,00 €, totaux facture HT 1 458,49 + TVA 126,51 = 1 585,00 € — réconciliation exacte avec le montant payé. Commande non encore transmise à Pennylane (pennylane_sent_at NULL), aucun renvoi nécessaire. Testé et validé par Patrice en recette le 2026-07-03.
Point de vigilance connexe — corrigé le 2026-07-06 :
getOrderEmail()(sbuiadmin-cart.phpL.1034-1037) avait le motif inverse — il prenait le dernier transport non vide pour l'email de confirmation. Correct sur #23635 par chance d'ordre des lignes, mais fragile si une commande mixte a la ligne physique en premier. Voir section 32 pour le détail du fix.
434d8fe et 133985e — 2026-07-06)Demande client avant livraison : ajouter sur le formulaire "Créer un compte" (/account/) un select permettant de choisir entre Particulier et Professionnel.
datas/modules/account/tpls/otpls/form_login_register.tpl (commit 434d8fe)La logique métier (validation "nom d'entreprise obligatoire si Professionnel", email de confirmation différencié verify-signup-professionnel/verify-signup-particulier) existait déjà entièrement côté back-end (account.php). Seul le <select id="registergroup"> était présent en commentaire dans le template, remplacé par <input type="hidden" name="group" value="1"> qui forçait tous les comptes en Particulier.
| Point | Détail |
|---|---|
| Select réactivé | 2 options actives en base : Particulier (gid=1, sélectionné par défaut) et Professionnel (gid=2). Le 3e groupe actif (Partenaires, gid=3) est explicitement exclu du select ({if $group.gid != 3}) — usage interne, non destiné à l'auto-inscription |
| Persistance du choix | En cas d'erreur de validation, le select réaffiche l'option choisie par l'utilisateur ($smarty.post.group) |
| Champ "Nom de l'entreprise" | Placeholder "(facultatif)" retiré ; devient dynamiquement obligatoire (attribut required posé/retiré en JS) quand "Professionnel" est sélectionné |
datas/modules/account/account.php:1473 (commit 133985e)Découvert en testant le parcours complet : le lien de confirmation d'email renvoyait une erreur 500, pour tout type de compte (bug préexistant, sans lien avec le nouveau select).
Cause : $sbflood vaut null sur tout le site — son instanciation est commentée dans header.php (//$sbflood = new flood();), très probablement parce que le constructeur de la classe flood (backcabaret/inc/class/sbuiadmin-flood.php) instancie new Memcache(), extension absente du serveur (seule l'extension Memcached, avec un "d", est installée — vérifié via un script temporaire, class_exists('Memcache') = non, class_exists('Memcached') = oui). Le reste du code tolère $sbflood à null (simple accès de propriété, sans effet), mais case "activation" appelait $sbflood->floodCheck() sans garde — appel de méthode sur null = erreur fatale.
Fix minimal : if ($sbflood) $sbflood->floodCheck(); — ce seul point d'appel corrigé, rien d'autre touché (réactiver $sbflood dans header.php tel quel aurait fait planter toutes les pages du site ; remettre l'anti-flood en état de marche avec Memcached a été écarté pour cette livraison, jugé trop risqué juste avant la mise en prod).
Confirmé en conditions réelles : le lien de confirmation qui renvoyait 500 est repassé à 200, compte réellement activé en base (active=1, email_verified=1).
businessname non enregistré — datas/modules/account/account.php (requête INSERT ~L.1309) (commit 133985e)Découvert dans la foulée : la colonne businessname n'était jamais insérée dans sb_account à l'inscription — utilisée uniquement pour le contenu de l'email de confirmation. Les comptes Professionnel se retrouvaient donc avec un nom d'entreprise vide en base malgré une saisie correcte.
Fix : colonne businessname ajoutée à la requête INSERT, avec la même sanitization que les autres champs texte (escape_string + stopXSS + utf8_decode + htmlEntities).
Validé de façon isolée (requête reconstruite à l'identique, insertion + relecture + suppression immédiate d'une ligne de test — pas de dépendance au captcha Google réel du formulaire).
Testé et confirmé par Patrice en recette le 2026-07-06 (création de compte Professionnel réussie, ce qui a permis de découvrir les bugs #2 et #3 au moment de cliquer sur le lien de confirmation reçu par email).
6ed5b3d — 2026-07-06)Demande client : un compte Professionnel (gid=2) sans numéro de SIRET renseigné ne doit pas pouvoir passer commande.
datas/modules/boutique/boutique.php — case "checkout"Vérification (gid == 2 && !siret) ajoutée à chaque étape du tunnel de commande, pour bloquer aussi bien le parcours normal qu'un contournement par appel direct d'URL :
| Étape | Point d'insertion |
|---|---|
| account (1er clic sur "Commander" depuis le panier) | Juste après le contrôle "utilisateur connecté" — redirige vers le panier au lieu de "shipping" |
| shipping (étape 2) | Idem, avant le traitement des adresses |
| payment (étape 3) | Réutilise $user_info déjà chargé à cet endroit (pas de requête supplémentaire) |
| payinprogress (soumission réelle à la passerelle de paiement) | Nouveau elseif juste avant le traitement du paiement (Paypal/Paybox/Chèque/Stripe/Monetico) — dernier rempart avant tout échange d'argent |
Case "cart" (panier) : calcule $siret_missing et l'assigne à Smarty pour l'affichage, appliqué à toutes les sous-actions (vue, ajout, suppression, mise à jour, promo).
datas/modules/boutique/tpls/boutique_display_cart.tplsb-box sb-error-box que les autres erreurs du panier) avec lien direct vers account/?ac=informations, affiché quand $siret_missing est vrai.<button disabled> (avec tooltip) tant que le SIRET manque ; redevient un lien cliquable normal dès qu'il est renseigné.Testé de bout en bout avec un compte Professionnel de test (créé et supprimé proprement après usage, aucune donnée résiduelle) :
shipping, payment, payinprogress) redirigent vers le panier (302) même en accès direct par URL.Testé et confirmé par Patrice le 2026-07-06.
getOrderEmail() : frais de livraison absents sur l'email de confirmation de commande (commit dc8379d — 2026-07-06)Même bug que la section 27 (sbuiadmin-facturx.php, commit 0dd01b5), mais côté email de confirmation de commande plutôt que facture : getOrderEmail() (backcabaret/inc/class/sbuiadmin-cart.php ~L.1034-1037) prenait le dernier transport_title non vide rencontré dans la boucle sur les lignes de commande, au lieu du transport réellement payé. Sur une commande mixte e-billet + produit physique, si la ligne physique arrivait avant la ligne e-billet, l'email affichait "E-BOX / 0 €" au lieu des frais de livraison réels.
Même logique de priorité que sbfacturx::fetchOrderData() (section 27) : priorité à la ligne avec transport_price > 0, repli sur le premier titre non vide seulement si aucune ligne payante n'existe (commande 100% dématérialisée).
Simulation avec les données réelles de la commande #23635 (celle qui avait révélé le bug initial) :
À tester par Patrice directement en production après la migration (cf. mémoire project_prod_migration) — pas de test en recette pour ce fix, décision explicite de Patrice le 2026-07-06.
98ae890 — 2026-07-06)Demande client : afficher le nom de l'entreprise et le SIRET du client sur la facture Factur-X, dans le bloc adresse (celui situé sous le bloc N°/date d'émission/N° client).
backcabaret/inc/class/sbuiadmin-facturx.php| Point | Détail |
|---|---|
| fetchOrderData() | Ajout de t2.gid, t2.businessname, t2.siret à la requête SQL (jointure sb_account), exposés dans $this->order sous client_gid / client_businessname / client_siret |
| _htmlHeader() (bloc PDF) | Nom de l'entreprise en gras sous le nom du client, SIRET en gras en fin de bloc adresse. N'apparaît que si renseigné — transparent pour les comptes Particulier (aucun changement visuel pour eux) |
| buildXML() (XML CII, sur confirmation de Patrice) | SIRET acheteur ajouté dans <ram:BuyerTradeParty><ram:SpecifiedLegalOrganization>, même format que le vendeur (schemeID="0002", SIREN sur les 9 premiers chiffres du SIRET). N'apparaît que si client_siret est renseigné |
ReflectionProperty/ReflectionMethod, aucun accès aux données clients réelles) : Professionnel → nom entreprise + SIRET affichés ; Particulier → bloc identique à avant.fetchOrderData() ne fait que des SELECT, aucune écriture) : sans SIRET → identique à avant ; avec SIRET → SpecifiedLegalOrganization ajouté, XML validé bien formé (DOMDocument::loadXML).Validé visuellement par Patrice le 2026-07-06 (PDF envoyé au comptable). Validation finale du comptable en attente — à consigner ici une fois reçue.
9abe6f5 — 2026-07-06)En cliquant sur le nuage (envoi manuel back-office) pour la commande #23649 (client « Pat TEST ent. », compte Professionnel de test créé pendant cette session), Patrice a obtenu : "Erreur Pennylane : Client « Pat TEST ent. » introuvable dans Pennylane. Créer le client dans Pennylane avant l'envoi."
sendToPennylane() recherchait le client uniquement par nom (GET /customers) et bloquait avec ce message si aucune correspondance — aucune création n'était jamais tentée. Ce n'était jamais apparu jusqu'ici car tous les clients réels ayant eu une facture envoyée (#23608, #23610, #23612, #23613) existaient déjà dans Pennylane (probablement saisis par le comptable en amont). "Pat TEST ent." est le premier compte entièrement nouveau à passer par ce circuit — un vrai nouveau client Professionnel commandant avant que le comptable ait eu le temps de le créer manuellement aurait été bloqué de la même façon.
backcabaret/inc/class/sbuiadmin-facturx.phpNouvelle méthode _createPennylaneCustomer($api_key, $customer_type) : si la recherche par nom échoue, tente une création automatique avant d'abandonner :
POST individual_customers (first_name, last_name, billing_address, emails)POST company_customers (name = businessname du compte, reg_no = SIRET, billing_address, emails)billing_address (adresse/CP/ville/pays) est obligatoire côté API Pennylane — retourne null proprement (sans appel API inutile) si l'adresse du compte est incomplète, plutôt que d'envoyer une requête vouée à échouer. Le message d'erreur ne s'affiche désormais que si la création automatique échoue aussi.
Le cron hebdomadaire (pennylane_weekly.php) bénéficie du même fix automatiquement, puisqu'il délègue entièrement à sendToPennylane().
Champs du payload vérifiés contre la documentation officielle Pennylane (postindividualcustomer/postcompanycustomer). Pas de test technique de mon côté contre l'API réelle (compte de production utilisé par le comptable — éviter d'y créer un faux client de test sans le faire moi-même). Testé et confirmé par Patrice en cliquant sur le nuage de la commande #23649.
148cae9 — 2026-07-08)Patrice voulait pouvoir masquer certaines dates du front (page nos-spectacles et tableau "prochains spectacles" de l'accueil) sans les supprimer — ex. dates passées ou publiées par erreur.
backcabaret/datas/modules/dates.phpactive : lu depuis $_POST, inséré/mis à jour en BDD, chargé en édition — même pattern que active sur pages.php/faq.php.pages/faq qui démarrent désactivées) — pour éviter qu'une date fraîchement créée reste invisible par oubli de case à cocher.(inactif) sur les produits désactivés pour les distinguer. Le select "Produit concerné par le changement de prix" reste filtré sur les produits actifs uniquement (non demandé).backcabaret/datas/modules/tpls/dates.tplIcône statut dans la colonne Actions du tableau admin (glyphicon-eye-open, vert si actif / rouge si non actif) — même pattern que produits/pages/FAQ.
inc/cmscustom.php (insert_sbGetDates)Filtre d.active = '1' ajouté à la requête — s'applique à toutes les dates récupérées par cette fonction, donc à la fois à nos-spectacles et au tableau de la page d'accueil (même fonction, même requête).
Nouvelle colonne sur sb_dates (à exécuter par Patrice, comme link/priceht/pennylane_id) :
ALTER TABLE Ca07bar8_sb_dates
ADD COLUMN active TINYINT(1) NOT NULL DEFAULT '1' AFTER complet;
DEFAULT '1' pour que les dates existantes restent visibles sans action requise.
Testé et validé par Patrice en recette le 2026-07-08 ("Nickel").
6dadeb7 — 2026-07-08)Sur la commande #23790, les frais de livraison apparaissaient bien sur la facture Factur-X mais pas dans la modal "détail de la commande" du tableau admin (shop&a=orders). Repéré par Patrice directement en production, après la migration.
Même défaut que celui déjà corrigé sur la facture (sbuiadmin-facturx.php, commit 0dd01b5, section 27) et sur l'email de confirmation (getOrderEmail(), commit dc8379d, section 34) : chaque ligne sb_shop_order_detail porte son propre transport_title/transport_price — les lignes e-billet dématérialisées portent "E-BOX"/"E-PASS" à 0 €, la ligne produit physique porte le vrai tarif payé.
Dans shop.tpl (boucle de construction de la modal), la variable transport_title/transport_price était écrasée à chaque ligne portant un titre non vide, sans priorité — donc c'est la dernière ligne de la commande qui l'emportait. Sur une commande mixte où l'e-billet arrive après la ligne physique, les frais réels étaient écrasés par 0 € et le bloc "Frais de livraison" retombait à zéro dans la modal.
backcabaret/datas/modules/tpls/shop.tplMême correction de priorité que les deux fix précédents : la première ligne avec transport_price > 0 gagne et se verrouille (nouvelle variable transport_locked), sinon repli sur le premier titre non vide (commande 100 % dématérialisée).
Corrigé directement en production (accès direct confirmé post-migration), cache Smarty compilé vidé pour forcer la recompilation. Testé et confirmé fonctionnel par Patrice sur la commande #23790. Fix reporté à l'identique sur la copie recette pour le commit.
3544ecd — 2026-07-09)Retour du comptable suite à l'envoi de la facture d'Anne-Françoise Chauveau Meisch (commande #23779) : le « ç » n'était pas reconnu, affiché littéralement comme ç dans le bloc informations client (facture + remontée Pennylane) — ex. Anne-Françoise au lieu de Anne-Françoise.
Double encodage HTML sur le nom client :
firstname/lastname sont encodés en entités HTML (htmlEntities()) puis concaténés en nickname, stocké encodé une fois dans sb_account.username.boutique.php:797), ce username déjà encodé est relu puis repassé dans displayText(..., entities=1), qui l'encode une seconde fois avant stockage dans sb_shop_order.client_name (ex. & devient &).sbuiadmin-facturx.php ne décodait qu'une seule fois — il restait donc une entité résiduelle visible sur la facture.Vérification faite également sur le bloc adresse/ville de la facture (demande explicite de Patrice) : ces champs proviennent directement de sb_account (pas d'un instantané de commande) et ne sont encodés qu'une seule fois (le passage par un formulaire HTML lors d'une modif de profil auto-corrige un éventuel ré-encodage, contrairement au nom recalculé en PHP pur au checkout) — pas de bug de ce type constaté sur adresse/ville.
backcabaret/inc/class/sbuiadmin-facturx.phpNouvelle méthode privée _decode() : décode les entités HTML en boucle jusqu'à stabilisation (idempotent — sans effet sur un champ déjà décodé ou simplement encodé une fois). Remplace tous les appels html_entity_decode() de fetchOrderData() et _parseProducts() : nom, prénom, adresse, code postal, ville, pays, raison sociale, SIRET, titre paiement, titre transport, description promo, titre produit.
Le même anti-pattern (relecture d'une valeur déjà encodée puis ré-encodage avant stockage) existe ailleurs et pourrait produire le même symptôme visuel, sans lien avec la facture :
client_shipping (adresse de livraison snapshotée sur la commande, boutique.php:799)account.php:1648-1649)Déployé en production le 2026-07-09 (copie directe de sbuiadmin-facturx.php, diff vérifié identique au commit recette avant copie). Testé et confirmé par Patrice : "les factures sont testées, pour le bloc information, c'est ok". Dossier clos.
8dfbf43 — 2026-07-09, déployé en production le 2026-07-09)Retour du comptable (email, commande #23779 servant de test) : chaque article remonté sur Pennylane est enregistré dans le même compte 706, quel que soit son type. Demande d'utiliser :
707531 – BOISSONS 26/27707532 – TRAITEUR 26/27 (majuscule comme les autres)707534 – SPECTACLE IMPREVISIBLE EGERIEExploration des liens de doc référencés en tête de sbuiadmin-facturx.php (attention : le petit modèle utilisé par l'outil de fetch a halluciné un premier résultat avec un endpoint inexistant — vérifié et écarté en croisant avec llms.txt et les pages .md brutes, cohérentes avec l'endpoint réellement utilisé en prod).
Confirmé : l'endpoint déjà utilisé POST /customer_invoices/e_invoices/imports accepte un tableau optionnel invoice_options.invoice_lines[] :
"invoice_lines": [
{ "e_invoice_line_id": "3", "ledger_account_id": 123 }
]
e_invoice_line_id correspond exactement au <ram:LineID> déjà généré dans _xmlLine(). ledger_account_id est l'identifiant interne Pennylane du compte (pas le numéro comptable 707531) — il se résout via GET /ledger_accounts?filter=[{"field":"number","operator":"start_with","value":"707531"}] (seul opérateur documenté pour le champ number), scope requis : ledger_accounts:readonly ou :all.
backcabaret/inc/class/sbuiadmin-facturx.php :
_parseProducts() lit un nouveau champ compte par tranche TVA (tva1/tva2/tva3).buildXML() capture, pendant la génération des lignes, le mapping LineID → numéro de compte dans $this->lineAccounts (uniquement les tranches où un compte est renseigné)._resolveLedgerAccountId($api_key, $account_number) : résout le numéro en ledger_account_id via l'API, avec cache par instance (une facture peut avoir plusieurs lignes sur le même compte).sendToPennylane() construit invoice_options.invoice_lines à partir de ce mapping. Si un compte ne se résout pas, la ligne repart sans override plutôt que de bloquer tout l'envoi (repli sur le mapping par défaut Pennylane, donc 706 comme aujourd'hui).backcabaret/datas/modules/shop.php (admin produit) :
707531/707532/707534), à côté des champs Libellé/Nom/Taux/Montant HT existants.tva déjà existant (colonne sb_shop_product.tva) — aucune migration BDD.Logique validée par un test isolé (reproduction de l'algorithme hors framework, sans BDD) : mapping LineID→compte correct, produits sans compte non affectés. Syntaxe PHP vérifiée (php -l) sur les deux fichiers.
Le token existant (scopes customer_invoices:all + customers:all) ne permettait pas d'interroger /ledger_accounts (403 explicite : "Access to this resource requires scope ledger_accounts:readonly ledger_accounts:all").
Le comptable ne trouvant pas comment modifier les scopes d'un token existant (interface Pennylane en français), analyse de deux captures d'écran de l'assistant "Génération de Token API" Pennylane — identification de la case à cocher : ligne "Comptes comptables", colonne "Lecture seule". Rappel donné à Patrice de bien recocher aussi "Clients" et "Factures client" (scopes déjà utilisés) pour ne pas casser l'intégration existante en générant un nouveau token.
Nouveau token généré et renseigné en BDD (sb_config.pennylane-api-key) le 2026-07-09 — scope ledger_accounts désormais actif, vérifié en lecture seule via script temporaire HTTPS (créé, utilisé, supprimé immédiatement — même procédure que les vérifications précédentes, cf. section accès SSH refusé).
La résolution GET /ledger_accounts?filter=... fonctionne mais remonte plusieurs comptes avec le même numéro et le même libellé, tous enabled: true, IDs différents :
707531 → 7 doublons707532 → 10 doublons707534 → 10 doublonsAucun champ retourné par l'API ne permet de distinguer objectivement le "bon" compte parmi les doublons (le flag enabled est identique partout). Dates de création groupées sur quelques journées (fin octobre 2025, 30 juin 2026) — ressemble à des comptes recréés par erreur plutôt qu'un fonctionnement normal de Pennylane.
Décision finale du comptable (2026-07-09) : il ne voit qu'un seul compte 707531/707532/707534 dans son propre plan comptable Pennylane (les doublons ne sont visibles que via l'API, pas dans son interface). Consigne : "Prends le 1er ID et on verra". Le code prenait déjà le premier match exact retourné par l'API (_resolveLedgerAccountId(), break au premier trouvé) — comportement inchangé, juste documenté explicitement en commentaire pour que ce choix soit tracé comme une décision et non un oubli. Aucune modification de code nécessaire sur ce point.
Commit recette effectué le 2026-07-09 (commit 8dfbf43) à la demande de Patrice, pour figer l'état du code pendant l'attente du comptable (facilite les diffs si modifications ultérieures) — la fonctionnalité n'est pas validée pour autant, voir statut du test ci-dessous. Ne pas déployer en production tant que le test réel et la validation du comptable ne sont pas faits.
Commande de test ("Pat TEST ent.", compte informatux45@gmail.com) avec le produit "BILLET CADEAU - ALL INCLUSIVE" — les 3 tranches TVA du produit ont déjà les comptes renseignés (707531/707532/707534, cf. libellés LIVE085/086/087). Test effectué via un script temporaire HTTPS (bootstrap complet du site + appel direct à sendToPennylane()), créé/utilisé/supprimé immédiatement (copie conservée hors webroot dans /home/patrice/VPS/informatux_test/_pennylane_test_script.php.txt pour réutilisation rapide, jamais laissée déployée entre deux tests).
Résultat : bloqué sur un scope manquant. Le nouveau token (généré pour obtenir le scope ledger_accounts) a perdu le scope customers:all — confirmé par un test de diagnostic direct (reproduction de l'appel POST company_customers) :
{"status": 403, "error": "Access to this resource requires scope \"customers:all\"."}
Exactement le risque anticipé avant la génération du nouveau token (un nouveau token ne reprend pas automatiquement les scopes de l'ancien).
Liste des 3 scopes nécessaires, transmise à Patrice pour le comptable : | Scope Pennylane | Choix | |---|---| | Clients | Lecture et écriture | | Factures client | Lecture et écriture | | Comptes comptables | Lecture seule |
Statut au 2026-07-09 (matin) : en attente du comptable pour régénérer le token avec les 3 scopes ci-dessus en une seule fois. Reprise du test dès que le token est mis à jour en BDD.
Comptable a régénéré un nouveau token ("normalement tous les droits nécessaires"). Retest complet via un nouveau script temporaire HTTPS (même procédure : copié dans le webroot recette, utilisé, supprimé immédiatement) sur la commande #23649 :
| Vérification | Résultat |
|---|---|
| POST company_customers (scope Clients) | HTTP 201 — création OK |
| sendToPennylane(23649) (scope Factures client) | HTTP 201 — nouvel envoi OK (pennylane_id 25591424708608) |
| GET /ledger_accounts?number=707531 (scope Comptes comptables) | HTTP 200 — les 7 doublons connus remontent bien |
Les 3 scopes nécessaires sont confirmés fonctionnels. Les 3 lignes de la facture de test sont bien remontées côté Pennylane avec les bons libellés produit (BOISSONS 26/27, TRAITEUR 26-27, SPECTACLE IMPREVISIBLE EGERIE).
Point non vérifiable par API : l'endpoint GET /customer_invoices/{id}/invoice_lines n'expose pas le ledger_account_id effectivement assigné à chaque ligne — impossible de confirmer par ce biais que la ventilation (707531/707532/707534 au lieu du 706 par défaut) a réellement eu lieu. Seule une vérification visuelle dans l'interface Pennylane par Patrice/le comptable peut le confirmer définitivement. Demande transmise au comptable par Patrice, en attente de retour.
Décision explicite de Patrice : basculer le code en production dès maintenant, sans attendre le retour du comptable sur la ventilation effective des comptes ("on verra quand le comptable nous fera son retour si c'est ok ou pas") — dérogation assumée à la règle habituelle d'attente de validation fonctionnelle complète avant mise en prod.
8dfbf43 vers /home/patrice/VPS/cabaret/web/ (backcabaret/datas/modules/shop.php et backcabaret/inc/class/sbuiadmin-facturx.php), après diff préalable confirmant l'absence de toute divergence non expliquée entre prod et l'état pré-commit de la recette.Retour du comptable sur la facture de test déployée en prod (FACT-20260706023649) : 2 lignes sur 3 correctement ventilées (BOISSONS → 707531, TRAITEUR → 707532), mais la ligne SPECTACLE IMPREVISIBLE EGERIE (TVA 5,5%) est restée sur le compte 706 par défaut au lieu de 707534.
Diagnostic effectué (script temporaire, même procédure que d'habitude) :
fetchOrderData() : ligne 1 = TRAITEUR/707532, ligne 2 = BOISSONS/707531, ligne 3 = SPECTACLE/707534 — aucun bug de décalage dans buildXML()/sendToPennylane().GET /ledger_accounts?number=707534 interrogé isolément à 3 reprises : HTTP 200 à chaque fois, résolution normale — pas de scope manquant ni de rate-limit reproductible.GET /ledger_entries/{id}) : bloquée, scope ledger_entries non inclus dans le token actuel (non demandé au comptable, jugé non prioritaire pour l'instant).Conclusion à ce stade : cause non déterministe identifiée avec les données disponibles ; hypothèse principale = raté ponctuel (aléa réseau/API) sur l'appel de résolution pour cette ligne précise au moment de l'envoi, plutôt qu'un bug systématique reproductible.
Nouveau test envoyé pour trancher : nouvel envoi réel déclenché sur la même commande #23649 (nouveau doublon, pennylane_id 25606483181568, même numéro de facture FACT-20260706023649) — demande transmise au comptable pour vérifier si la ligne 5,5% est cette fois bien sur 707534.
Le comptable a confirmé que le nouveau test (pennylane_id 25606483181568) est correct : les 3 lignes, y compris la TVA 5,5% (SPECTACLE IMPREVISIBLE EGERIE → 707534), sont bien ventilées sur les bons comptes. L'anomalie du premier test était donc bien un aléa ponctuel (raté isolé sur un des 3 appels de résolution), pas un bug reproductible — aucune correction de code nécessaire.
Vérifications complémentaires avant clôture :
pennylane_weekly.php (cron hebdomadaire) délègue entièrement à sbfacturx::sendToPennylane() — la ventilation comptable est donc héritée automatiquement par le cron, sans aucune modification à faire dessus.8dfbf43, déjà en prod) : rien à committer/pousser de plus, le diagnostic n'a utilisé que des scripts temporaires hors dépôt, supprimés après usage.Dossier définitivement clos.
Document mis à jour le 2026-07-09 — Claude Code (Informatux) — commit 8dfbf43, déployé en production le 2026-07-09, validé par le comptable le 2026-07-09, dossier clos