Serienbriefe mit Google Docs/Suite erstellen

Zunächst einmal die Ziele, die ich mir für diesen Hack gesetzt habe.

  • Erstellen einer Serienbriefvorlage mit Platzhaltern (Format {{platzhalter}})
  • Google Sheet Tabellendokument mit den Empfängern und den zu ersetzenden Platzhaltern
  • Automatisches Erstellen und Ablegen der generierten Briefe als PDF in einem Ordner

Erstellen der Briefvorlage

Ich habe erst angefangen, meinen Brief in Google Docs zu schreiben, wie ich es in dem vorhergehenden Beitrag gemacht habe.

Google Slides bietet viel mehr Formatierungsoptionen und bietet somit eine clevere Alternative zum Erstellen von Dokumenten an. Dort kann man mit Formen und Illustrationen arbeiten, und es ist hier ebenfalls möglich, die Platzhalter austauschen zu lassen.

 

Erstellen eines Spreadsheets für die Platzhalter

Als Letztes erstellen wir unser Spreadsheets in dem sich die einzelnen Empfänger befinden und die Werte für die Platzhalter.

Zwei der Felder werden eine Bedeutung in meiner Logic haben:

  • template: Document-Id der Vorlage (so sind wir flexibel). Die Document-ID findet man in der Adressleiste des Browsers, wenn man die Google Docs-Vorlage aufruft.
  • filename: Dateiname, der erzeugt werden soll (wenn leer, wird keine Datei angelegt) z. B. Brief01.pdf, Brief02.pdf…
  • Der Rest sind die Platzhalter wie Name, Adresse, Datum, Anrede, die man dann als {{platzhalter}} ersetzt.

App-Script Magic

 

In Google Sheets im Bereich Erweiterungen >>> Apps Script aufrufen und den Code einfügen (zuvor alles clean machen). Das Projekt benennen z. B. Serienbrief und speichern. Nach dem Speichern die Funktion onOpen per Pfeiltaste ausführen.

Google möchte dann noch eine Verifizierung (unsicher verifizieren).

 

Für alle Nicht-Coder unter euch ist dieser Abschnitt, bis auf den eigentlichen Script, weniger interessant. (Siehe auch GitLab Project)

BATCH_SIZE=50;

/*
Beim start werden neue Menüpunkte zu Sheets hinzugefügt
*/
function onOpen() {
  renderMenu();
}

/*
Aktualisiert das Menü
*/
function renderMenu(){
  var ui = SpreadsheetApp.getUi();
  var progress=PropertiesService.getDocumentProperties().getProperty('progress');
  if(progress>0){
     ui.createMenu('Serienbrief')
     .addItem('Neu starten', 'startOver')
     .addItem('Bei Eintrag '+parseInt(progress)+' fortsetzen', 'createDocuments')
     .addToUi();
  }else{
    ui.createMenu('Serienbrief')
    .addItem('Erstellen', 'startOver')
    .addToUi();
  }
}

/*
Startet einen neuen Vorgang
*/
function startOver(){
  PropertiesService.getDocumentProperties().setProperty('progress', 0);
  createDocuments();
}

/*
Konvertiere die aktuelle Datarange in ein Object-Array und nutze die erste Zeile als Object-Properties
*/
function spreadSheetToObjects(){
  //Alle gefüllten Spalten auslesen
  var range=SpreadsheetApp.getActiveSheet().getDataRange().getDisplayValues();
  //Array für alle Einträge
  var data=[];
  for(var row=1; row<range.length; row++){ //Durchläuft alle Zeilen bis auf die Überschriften (daher row=1)
    var dataObject={};
    for (var col=0; col<range[0].length; col++){ //Durchläuft alle Spalten
      var key=range[0][col]; //Übernimmt die Überschrift als Object-Property
      var value=range[row][col]; //Schreibt den Wert der aktuellen Zeile ins Objekt
      dataObject[key]=value; // Fügt den Eintrag zum Array hinzu
    } 
    data.push(dataObject); //Fügt das Datenobjekt in die Liste hinzu
  }
  return data; //Gibt das Object-Array zurück
}

/*
Zielordner für die erstellten Dokumente.
Wird als "gen" auf gleicher Ebene wie das Spreadsheet angelegt
*/
function getTargetFolder(){
  var folderName="gen"; //Name des Zielordners
  //Aktuellen Ordner ermitteln:
  var folder=DriveApp.getFileById(SpreadsheetApp.getActiveSpreadsheet().getId()).getParents().next();
  //Gibt es einen "gen" Ordner?
  var genFolders=folder.getFoldersByName(folderName);
  if(genFolders.hasNext()){
    return genFolders.next();    
  }else{
    //"gen" Ordner erstellen
    return folder.createFolder(folderName);
  }
}

function createDocument(dataObject, targetFolder){
  //Template-File laden
  var templateFile=DriveApp.getFileById(dataObject.template);
  //Dann eine Kopie im "gen" Ordner erstellen
  var document=templateFile.makeCopy(targetFolder);
  
  //Schauen, ob es sich um eine Präsentation handelt:
  if(document.getMimeType()=="application/vnd.google-apps.presentation"){
    //Dann das Dokument aus der Präsentation erstellen
    var slideApp=SlidesApp.openById(document.getId());
    //Alle Slides durchlaufen
    slideApp.getSlides().forEach(slide=>{
      //Alle Platzhalter ersetzen                       
      for(var key in dataObject){
        slide.replaceAllText('{{'+key+'}}',dataObject[key]);
      }                      
    });
    slideApp.saveAndClose();
  }else if(document.getMimeType()=="application/vnd.google-apps.document"){
    //Dokument ist ein Textdokument -> Offnen und Platzhalter tauschen
    var docApp=DocumentApp.openById(document.getId());
    for(var key in dataObject){
      docApp.getBody().replaceText('{{'+key+'}}', dataObject[key]);
    }     
    docApp.saveAndClose();
  }else{
    //unknown type - Hier abbrechen
    return;
  }
  //Create pdf export
  var pdfFile=getTargetFolder().createFile(
    DriveApp.getFileById(document.getId()).getAs(MimeType.PDF),
  ).setName(dataObject.filename);
  //Originaldokument löschen
  document.setTrashed(true);
}

/*
Hauptfunktion, die alle Daten aus dem Spreadsheet ausliest und die Dokumente erzeugt
*/
function createDocuments(){
  //Die Dokumente in Objekte umwandeln
  var data=spreadSheetToObjects();
  var targetFolder=getTargetFolder();
  var start=parseInt(PropertiesService.getDocumentProperties().getProperty('progress'));
  //Durchlaufe alle Objekte und lege die Dokumente an
  for(var i=start; i<data.length && i< (start+BATCH_SIZE); i++){
    createDocument(data[i], targetFolder);    
  }
  if(i<data.length){
    SpreadsheetApp.getUi().alert("Es sind zu viele Einträge für einen Durchlauf. Nutzen Sie die Funktion \"Fortsetzen\" im Menü um die nächsten Dokumente zu erstellen");
    PropertiesService.getDocumentProperties().setProperty('progress', i);
    renderMenu();
  }
}