Publipostage : Custom widget pour enregistrer facilement les PDFs

Pré-requis : avoir une table avec une colonne formule qui crée du code html dynamique, cf Générer des PDF personnalisés / Publipostage / Widget Markdown ou Publipostage avec données de plusieurs tables.

Enregistrer un PDF pour chaque vue, en indiquant le nom du fichier

Le custom widget ouvre la boîte de dialogue d’impression pour enregistrer en PDF les documents générés grâce à du publipostage, en les nommant de la manière souhaitée.

Nov-30-2025 20-10-08

Utilisation
Dans votre table qui contient le html :

  • ajoutez une colonne de type booléen, pour sélectionner les lignes que vous souhaitez imprimer

Dans votre page :

  • Ajoutez une vue personnalisée basée sur la table qui contient le html, choisir la vue « URL personnalisée » avec le lien suivant : https://maluhialoha.github.io/grist-cw-html-to-pdf/

  • Dans le panneau de création, autorisez l’accès au document, et indiquez la colonne de votre table qui contient le html, ainsi que la colonne de sélection créée précédemment

  • (optionnel) Dans le custom widget, indiquez le nom que vous souhaitez donner aux documents. Vous pouvez utiliser des valeurs dynamiques, en utilisant les identifiants de colonnes entourés d’accolades (ex : Facture de {Nom} - {Ville})

  • Cliquez sur « Enregistrer ».

  • La boîte de dialogue d’impression va s’ouvrir pour chaque document à télécharger. Il faut choisir « Enregistrer en PDF » et valider l’enregistrement dans le dossier souhaité.

Document d’exemple ici :

PS : au départ je voulais faire un téléchargement direct, sans passer par la boîte de dialogue : j’ai testé les librairies htmltopdf et htmltocanva mais le html est très mal rendu (problèmes d’espaces notamment)…

Enregistrer toutes les vues dans un même PDF

Vous pouvez utiliser ce custom widget : Imprimer en masse - publipostage

En choisissant « Enregistrer en PDF » quand la boîte de dialogue s’ouvre.

1 « J'aime »

Code du widget

Le code est disponible ici : GitHub - maluhialoha/grist-cw-html-to-pdf
Il est servi à cette adresse : https://maluhialoha.github.io/grist-cw-html-to-pdf/, pour être utilisé directement comme custom widget (cf section précédente).

Pour information ci-dessous la v0 du code, sans la partie de mappage des colonnes :
html :

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 10px;
      background: #f5f5f5;
      line-height: 1.4;
    }

    .container {
      max-width: 800px;
      margin: 0 auto;
      background: white;
      padding: 15px;
      border-radius: 6px;
      box-shadow: 0 1px 4px rgba(0,0,0,0.1);
    }

    h1 {
      color: #333;
      margin-bottom: 15px;
      font-size: 18px;
    }

    .form-group {
      margin-bottom: 15px;
    }

    label {
      display: block;
      margin-bottom: 8px;
      color: #555;
      font-weight: 500;
      font-size: 14px;
    }

    input[type="text"] {
      width: 100%;
      padding: 8px 12px;
      border: 1px solid #ddd;
      border-radius: 3px;
      font-size: 13px;
      display: block;
      margin-bottom: 10px;
    }

    input[type="text"]:focus {
      outline: none;
      border-color: #667eea;
    }

    button {
      background: #667eea;
      color: white;
      border: none;
      padding: 8px 20px;
      border-radius: 3px;
      font-size: 14px;
      cursor: pointer;
      font-weight: 500;
      transition: background 0.2s;
    }

    button:hover {
      background: #5568d3;
    }

    details {
      margin-top: 12px;
      border: 1px solid #e0e0e0;
      border-radius: 3px;
      padding: 8px 10px;
      background: #fafafa;
    }

    summary {
      cursor: pointer;
      font-weight: 600;
      color: #333;
      font-size: 13px;
      user-select: none;
    }

    summary:hover {
      color: #667eea;
    }

    details[open] summary {
      margin-bottom: 8px;
      padding-bottom: 6px;
      border-bottom: 1px solid #e0e0e0;
    }

    ol, ul {
      padding-left: 20px;
      color: #555;
      font-size: 13px;
    }

    li {
      margin-bottom: 4px;
    }

    code {
      background: #f0f0f0;
      padding: 2px 6px;
      border-radius: 3px;
      font-family: 'Courier New', monospace;
      font-size: 13px;
    }

    pre {
      display: none;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>📄 Téléchargement PDFs</h1>

    <div class="form-group">
      <label for="namePattern">Nom que vous souhaitez donner aux documents :</label>
      <input type="text" id="namePattern" placeholder="Ex: Dossier {Nom} {Prenom}">
      <button id="printBtn">Télécharger</button>
    </div>
  </div>

  <script src="script.js"></script>
</body>
</html>

js - il faut remplacer formule_avec_style par l’id de la colonne qui contient votre html :

grist.ready({ requiredAccess: 'full' });

const output = document.querySelector("#output");
const printBtn = document.querySelector("#printBtn");
const namePattern = document.querySelector("#namePattern");
let tableData = [];

// Récupérer la table
grist.onRecords(table => {
  tableData = table
});

// Bouton d'impression
printBtn.addEventListener('click', async () => {
  if (tableData.length === 0) {
    alert("Aucune donnée à télécharger");
    return;
  }

  // Vérifier que la colonne "Télécharger" existe
  if (tableData.length > 0 && !('Telecharger' in tableData[0])) {
    alert("La colonne 'Télécharger' n'existe pas dans la table");
    return;
  }

  const rowsToDownload = tableData.filter(row => row.Telecharger === true);
  
  if (rowsToDownload.length === 0) {
    alert("Aucune ligne avec Télécharger = True");
    return;
  }

  const pattern = namePattern.value.trim();
  
  // Générer le nom du fichier
  function generateFileName(row, index) {
    if (!pattern) {
      return `${index + 1}`;
    }
    
    const variables = pattern.match(/\{([^}]+)\}/g);
    
    if (!variables) {
      return `${pattern} - ${index + 1}`;
    }
    
    let fileName = pattern;
    for (let variable of variables) {
      const columnId = variable.slice(1, -1).trim();
      
      if (!(columnId in row)) {
        throw new Error(`La colonne "${columnId}" n'existe pas`);
      }
      
      const value = (row[columnId] || '').toString().trim();
      fileName = fileName.replace(variable, value);
    }
    
    fileName = fileName.replace(/\s+/g, ' ').trim();
    
    return fileName;
  }

  // Ouvrir les fenêtres d'impression
  try {
    for (let i = 0; i < rowsToDownload.length; i++) {
      const row = rowsToDownload[i];
      const fileName = generateFileName(row, i);
      
      const printWindow = window.open('', '_blank');
      
      const htmlContent = `
        <!DOCTYPE html>
        <html>
        <head>
          <meta charset="UTF-8">
          <title>${fileName}</title>
          <style>
            body {
              font-family: Arial, sans-serif;
              padding: 20px;
              margin: 0;
            }
            @media print {
              body {
                padding: 10mm;
              }
            }
          </style>
        </head>
        <body>
          ${row.formule_avec_style || ''}
          <script>
            window.onload = function() {
              setTimeout(function() {
                window.print();
              }, 250);
            };
          <\/script>
        </body>
        </html>
      `;
      
      printWindow.document.write(htmlContent);
      printWindow.document.close();
      
    }
    
  } catch (error) {
    alert(error.message);
  }
});

Bonjour,

J’essaie de créer un modèle de publipostage mais je rencontre quelques difficultés.
En bref, le document comporte une table DEMANDES liée à un formulaire permettant à des gens de faire une demande (demande d’autorisation de faire x). Le formulaire, qui est assez long, comporte tout types de champs (texte, numérique, choix unique, choix multiple, date). Une deuxième table comporte la liste des communes du département (pour sélection via un menu déroulant).

Le but du publipostage serait d’avoir une fiche récapitulative pour chaque demande qui sera envoyée au demandeur signée si l’autorisation est délivrée.

Voici les difficultés que je rencontre :

  1. Pour les questions à choix multiples, le résultat s’affiche avec des parenthèses et des apostrophes, par exemple : (‹ Du 1er avril au 10 juin ›, ‹ Du 11 juin au 31 juillet ›)
    Est-ce qu’il est possible de « nettoyer » ce champs au niveau du publipostage ?

  2. Dans le formulaire, le demandeur peut inclure 1 à 5 communes dans sa demande. Il y a 5 menus déroulant pour sélectionner les communes, seul le premier est obligatoire. Est-ce qu’il serait possible d’avoir un tableau avec un nombre de lignes adapté au nombre de communes sélectionnées ?

J’espère que mes questions sont assez claires.
Merci pour vos explications sur ce fil qui m’ont déjà beaucoup aidée.

Bonjour, pourriez-vous créer un nouveau fil de discussion pour votre question svp ? (comme c’est un autre sujet que l’enregistrement de pdf)
Si vous pouvez inclure aussi une capture d’écran ou un exemple minimal pour la question des communes, ce serait top !
Merci d’avance et bonne journée :slight_smile: