facturation/admin/pdf.php

698 lines
19 KiB
PHP

<?php
namespace Paheko;
require_once __DIR__ . '/_inc.php';
$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_READ);
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))
{
throw new UserException("Ce document n'existe pas.");
}
$moyen_paiement = $facture->getMoyenPaiement($f->moyen_paiement);
// Récupération infos membre/client dans un objet normalisé pour le rendu
if ($f->receveur_membre)
{
$user = Users\Users::get($f->receveur_id);
if (!$user) {
throw new UserException("Ce membre n'existe plus.");
}
$nom_client = Users\DynamicFields::getNameFromArray($user) ?: '** ABSENT **';
$user_fields = Users\DynamicFields::getInstance()->listAssocNames();
$get_user_field = static function (?string $configured, string $fallback) use ($user, $user_fields) {
$field = $configured ?: $fallback;
return array_key_exists($field, $user_fields) ? ($user->$field ?? null) : null;
};
$c = (object) [
'nom' => $nom_client,
'adresse' => $get_user_field($plugin->getConfig('adresse_client'), 'adresse') ?: '[À RENSEIGNER DANS LA FICHE MEMBRE]',
'code_postal' => $get_user_field($plugin->getConfig('code_postal_client'), 'code_postal') ?: '[À RENSEIGNER]',
'ville' => $get_user_field($plugin->getConfig('ville_client'), 'ville') ?: '[À RENSEIGNER]',
'siret' => null,
'email' => Users\DynamicFields::getFirstEmailField() ? ($user->{Users\DynamicFields::getFirstEmailField()} ?? null) : null,
'telephone' => null,
];
}
else
{
$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
$emission = $f->date_emission->format('d/m/Y');
if (isset($f->date_echeance))
{
$echeance = $f->date_echeance->format('d/m/Y');
}
// -- Création du PDF
// Génération factures, devis et cotisation
if ($f->type_facture != CERFA)
{
$labels = [
FACT => ['Facture', 'Émise le', 'Adressée à', 'Échéance de paiement'],
DEVIS => ['Devis', 'Émis le', 'Adressé à', 'Échéance du devis'],
COTIS => ['Reçu de cotisation', 'Émis le', 'Adressé à', 'Date du reçu'],
];
[$document_label, $emission_label, $recipient_label, $due_label] = $labels[$f->type_facture] ?? $labels[FACT];
$doc = sprintf('%s n° %s', $document_label, $f->numero);
$theme = $plugin->getConfig('invoice_theme') ?: 'modern';
$theme = in_array($theme, ['modern', 'classic', 'minimal'], true) ? $theme : 'modern';
$accent = $plugin->getConfig('invoice_accent_color') ?: '#4656A5';
$accent = preg_match('/^#[0-9A-Fa-f]{6}$/', $accent) ? $accent : '#4656A5';
$show_address_borders = $plugin->getConfig('invoice_show_address_borders') !== false;
$show_email = $plugin->getConfig('invoice_show_email') !== false;
$show_web = $plugin->getConfig('invoice_show_web') !== false;
$address_border_class = $show_address_borders ? 'invoice-address-borders' : 'invoice-no-address-borders';
$currency = $config->get('currency') ?: '€';
$total = Utils::money_format($f->total, ',', ' ');
$ttc = $plugin->getConfig('ttc') ? 'TTC' : 'HT';
if ($plugin->getConfig('rue_asso') && $plugin->getConfig('cp_asso') && $plugin->getConfig('ville_asso')) {
$adresse = trim(($plugin->getConfig('numero_rue_asso') ? $plugin->getConfig('numero_rue_asso') . ' ' : '') . $plugin->getConfig('rue_asso'))
. "\n" . $plugin->getConfig('cp_asso') . ' ' . $plugin->getConfig('ville_asso');
}
else {
$adresse = $config->get('org_address_public') ?: $config->get('org_address') ?: '';
}
$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') ?? '');
$content = <<<HTML
<section class="letter">
<p>À {$h($lieu)}, le {$h($emission)},</p>
<p>Bonjour,</p>
<p>Nous accusons réception de votre cotisation « {$h($intitule)} » reçue le {$h($emission)} et nous vous en remercions.</p>
<p>Nous reconnaissons que vous avez acquitté la somme de <strong>{$h($total)} {$h($currency)}</strong>. Votre adhésion sera donc effective à compter du {$h($souscription)} jusqu'à {$h($expiration)}.</p>
<p>Nous 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;
}
$html_title = $h($doc . '_' . $emission);
$doc_html = $h($doc);
$txtemis = $h($emission_label . ' ' . $emission);
$txtdest = $h($recipient_label);
$html = <<<HTML
<!DOCTYPE html>
<html class="invoice-theme-$theme $address_border_class">
<head>
<meta charset="UTF-8">
<title>$html_title</title>
<style>
@page { size: A4 portrait; margin: 14mm; }
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Helvetica, Arial, sans-serif;
font-size: 10pt;
line-height: 1.45;
color: #20242A;
background: #fff;
}
.invoice {
display: block;
padding-bottom: 28mm;
page-break-after: avoid;
break-after: avoid-page;
}
.hero {
display: table;
width: 100%;
padding-bottom: 10mm;
border-bottom: 2px solid $accent;
}
.brand, .document-title {
display: table-cell;
vertical-align: top;
}
.brand { width: 46%; }
.document-title { width: 54%; }
.brand-summary {
display: inline-block;
max-width: 72mm;
text-align: center;
}
.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;
}
.items th {
padding: 4mm 3mm;
color: #fff;
background: $accent;
text-align: left;
font-weight: 700;
}
.items td {
padding: 4mm 3mm;
border-bottom: 1px solid #E5E8EF;
vertical-align: top;
}
.items tr:nth-child(even) td {
background: #F7F8FB;
}
.amount {
width: 34mm;
text-align: right;
white-space: nowrap;
}
.summary {
display: table;
width: 100%;
margin-top: 8mm;
}
.details, .total {
display: table-cell;
vertical-align: top;
}
.details {
width: 64%;
color: #3B4350;
}
.details div {
margin-bottom: 2mm;
}
.details span {
display: inline-block;
min-width: 42mm;
margin-right: 4mm;
color: #687386;
}
.total {
width: 36%;
padding: 4mm 5mm;
color: #fff;
background: $accent;
text-align: right;
}
.total span, .total em {
display: block;
font-style: normal;
font-size: 9.5pt;
}
.total strong {
display: block;
margin: 1mm 0;
font-size: 17pt;
line-height: 1.15;
}
.status {
display: inline-block;
margin: 2mm 0 0;
padding: 1.5mm 3mm;
border: 1px solid #D9DEE8;
font-weight: 700;
}
.status.paid { color: #1F6B3A; }
.status.pending { color: #9A4D00; }
.invoice-note, .approval, .letter {
margin-top: 8mm;
}
.approval {
min-height: 25mm;
padding-top: 4mm;
border-top: 1px solid #D9DEE8;
}
.approval strong, .approval span {
display: block;
}
.legal {
margin-top: 8mm;
color: #566070;
font-size: 9pt;
}
footer {
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>
<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>
HTML;
} // Génération du CERFA
elseif ($f->type_facture == CERFA)
{
$doc = 'Reçu de don n°'. $f->numero;
$url = WWW_URL;
$libelles = $facture->listTextesCerfa(false);
$t['numero'] = $f->numero;
$t['org_name'] = $config->get('org_name');
$t['n_rue_asso'] = $plugin->getConfig('numero_rue_asso');
$t['rue_asso'] = $plugin->getConfig('rue_asso');
$t['cp_asso'] = $plugin->getConfig('cp_asso');
$t['ville_asso'] = $plugin->getConfig('ville_asso');
$t['objet0'] = $plugin->getConfig('objet_0');
$t['objet1'] = $plugin->getConfig('objet_1');
$t['objet2'] = $plugin->getConfig('objet_2');
$t['nom'] = $nom_client;
$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';
$t['d'] = ($f->date_emission->format('d'));
$t['m'] = ($f->date_emission->format('m'));
$t['Y'] = ($f->date_emission->format('Y'));
$t['forme'] = $f->contenu['forme'];
$t['nature'] = $f->contenu['nature'];
$t['texte'] = $libelles[$f->contenu['texte']];
$t['art200'] = $t['art238'] = $t['art885'] = '';
if($plugin->getConfig('droit_art200')){
$t['art200'] = 'X';
}
if($plugin->getConfig('droit_art238bis')){
$t['art238'] = 'X';
}
if($plugin->getConfig('droit_art885-0VbisA')){
$t['art885'] = 'X';
}
// forme du don
switch ($t['forme']){
case '1':
$t['frm'] = 'left: 15mm;';
break;
case '2':
$t['frm'] = 'left: 57.3mm;';
break;
case '3':
$t['frm'] = 'left: 115.2mm;';
break;
case '4':
$t['frm'] = 'left: 175.2mm;';
}
// nature du don
switch ($t['nature']){
case '1':
$t['nat'] = 'left: 15mm;';
break;
case '2':
$t['nat'] = 'left: 57.3mm;';
break;
case '3':
$t['nat'] = 'left: 115.2mm;';
}
// moyen de paiement
switch ($f->moyen_paiement){
case 'ES':
$t['pos'] = 'left: 15mm;';
break;
case 'CH':
$t['pos'] = 'left: 57.3mm;';
break;
default:
$t['pos'] = 'left: 115.2mm;';
}
$t['d2'] = ($f->date_echeance->format('d'));
$t['m2'] = ($f->date_echeance->format('m'));
$t['Y2'] = ($f->date_echeance->format('Y'));
ob_start();
echo <<<EOF
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{$doc}_{$emission}</title>
<style>
@page {
size: A4 portrait;
margin: 0;
padding: 0;
}
body {
font-family: Helvetica, Arial, sans;
font-size: 10pt;
background: white;
padding: 0;
margin: 0;
}
.page div {
position: absolute;
}
.page {
padding: 0;
margin: 0;
break-after: always;
width: 210mm;
height: 297mm;
background-size: cover;
background-position: -5mm -4.8mm;
}
#p1 {
background-image: url('{$url}p/facturation/cerfa-1.png');
}
#p2 {
background-image: url('{$url}p/facturation/cerfa-2.png');
position: relative;
}
</style>
</head>
<body>
<div class="page" id="p1">
<div style="top: 10mm; left: 170mm;">{$t['numero']}</div>
<div style="top: 35mm; left: 20mm;">{$t['org_name']}</div>
<div style="top: 46mm; left: 20mm;">{$t['n_rue_asso']}</div>
<div style="top: 46mm; left: 40mm;">{$t['rue_asso']}</div>
<div style="top: 51.5mm; left: 37mm;">{$t['cp_asso']}</div>
<div style="top: 51.5mm; left: 75mm;">{$t['ville_asso']}</div>
<div style="top: 62mm; left: 18mm;">{$t['objet0']}</div>
<div style="top: 66.5mm; left: 18mm;">{$t['objet1']}</div>
<div style="top: 70.8mm; left: 18mm;">{$t['objet2']}</div>
<div style="top: 128.5mm; left: 15mm;">X</div>
</div>
<div class="page" id="p2">
<div style="top: 18mm; left: 18mm;">{$t['nom']}</div>
<div style="top: 32mm; left: 18mm;">{$t['adresse']}</div>
<div style="top: 37mm; left: 40mm;">{$t['cp']}</div>
<div style="top: 37mm; left: 80mm;">{$t['ville']}</div>
<div style="top: 63mm; left: 87mm;">{$t['total']}</div>
<div style="top: 73mm; left: 58mm;">{$t['total_lettre']}</div>
<div style="top: 82mm; left: 69mm;">{$t['d']}</div>
<div style="top: 82mm; left: 82mm;">{$t['m']}</div>
<div style="top: 82mm; left: 99mm;">{$t['Y']}</div>
<div style="top: 96mm; left: 53mm;">{$t['art200']}</div>
<div style="top: 96mm; left: 103mm;">{$t['art238']}</div>
<div style="top: 96mm; left: 153.0mm;">{$t['art885']}</div>
<div style="top: 113mm; {$t['frm']}">X</div>
<div style="top: 136mm; {$t['nat']}">X</div>
<div style="top: 142mm; left: 25mm;">{$t['texte']}</div>
<div style="top: 158.2mm; {$t['pos']}">X</div>
<div style="top: 239mm; left: 139mm;">{$t['d2']}</div>
<div style="top: 239mm; left: 148mm;">{$t['m2']}</div>
<div style="top: 239mm; left: 156mm;">{$t['Y2']}</div>
<div style="top: 243mm; left: 137mm;">{$sign_tag}</div>
</div>
</body>
</html>
EOF;
$html = ob_get_clean();
} // End if cerfa
if(qg('d') !== null)
{
$filename = 'Print';
if (preg_match('!<title>(.*)</title>!U', $html, $match)) {
$filename = str_replace(" ", "_", trim($match[1]));
}
header('Content-type: application/pdf');
header(sprintf('Content-Disposition: attachment; filename="%s.pdf"', Utils::safeFileName($filename)));
Utils::streamPDF($html);
}
else
{
echo $html;
}