Correction erreurs, sécurisation code, personnalisation présentation (Jordan)

This commit is contained in:
Jordan Pelletier 2026-07-02 13:38:15 +02:00 committed by Jean-Christophe Engel
parent 717fc5320f
commit fffbfcac93
19 changed files with 822 additions and 471 deletions

View file

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