📍 Article principal : Google Workspace pour PME : architecture et gouvernance
Ce tutoriel construit une automatisation Apps Script de bout en bout : depuis une ligne de Google Sheets, génération d’un PDF à partir d’un modèle Docs, envoi par mail au destinataire, mise à jour du statut.
Objectif
Automatiser un flux Sheets → Docs → PDF → Gmail dans Apps Script avec runtime V8, déclencheurs installables et menu personnalisé Sheets. Code testé sur runtime V8 (mai 2026), API SpreadsheetApp, DocumentApp, DriveApp, GmailApp.
Quotas Apps Script (référence officielle)
Source : Quotas for Google Services (Apps Script) — dernière mise à jour 20 avril 2026.
- Durée d’exécution : 6 minutes par appel (Consumer comme Workspace).
- Triggers : 20 par utilisateur et par script.
- URL Fetch : 20 000 / jour Consumer, 100 000 / jour Workspace.
- MailApp recipients : 100 / jour Consumer, 1 500 / jour Workspace.
- Réinitialisation 24 heures après la première requête.
Runtime
Rhino déprécié depuis le 20 février 2025, exécution arrêtée le 31 janvier 2026 (source officielle Google for Developers). Tous les projets tournent sur le runtime V8. Vérification : Paramètres du projet → Activer le runtime Chrome V8.
Prérequis
- Compte Google Workspace Business Standard ou supérieur (quotas plus larges).
- Connaissances JavaScript de base (boucle, condition, fonction).
- Un Google Sheet et un Google Docs (modèle), tous deux dans un drive partagé.
- Un dossier Drive (id récupéré depuis l’URL) pour archiver les PDF.
- Niveau : intermédiaire.
- Temps : 90 minutes.
Étape 1 — Schéma fixe de la feuille
Créer un Sheet Devis 2026 avec en ligne 1 les en-têtes :
A: Date | B: N° devis | C: Client | D: Email client | E: Description | F: Montant HT | G: Statut | H: Lien PDF
Le script lit par index de colonne. Toute insertion de colonne au milieu casse les références — figer le schéma dès le départ.
Étape 2 — Projet Apps Script lié
Depuis le Sheet : Extensions → Apps Script. Renommer le projet Automation Devis. Cocher Paramètres du projet → Afficher le fichier de manifeste appsscript.json. Vérifier Activer le runtime Chrome V8.
Étape 3 — Modèle Docs
Créer un Google Docs Modele Devis avec balises de fusion :
DEVIS N° {{NUMERO}}
Date : {{DATE}}
Client : {{CLIENT}}
Description :
{{DESCRIPTION}}
Montant HT : {{MONTANT}} EUR
TVA 20 % : {{TVA}} EUR
Montant TTC : {{TTC}} EUR
Récupérer l’ID du Docs depuis l’URL (segment entre /d/ et /edit).
Étape 4 — Code de génération PDF
Dans Code.gs, code de la fonction qui copie le modèle, remplace les balises, exporte en PDF :
const TEMPLATE_DOC_ID = '1abcDEF...'; // Id du Docs modèle
const ARCHIVE_FOLDER_ID = '1xyzGHI...'; // Id du dossier Drive cible
function genererPdfDevis(d) {
const modele = DriveApp.getFileById(TEMPLATE_DOC_ID);
const copie = modele.makeCopy(`Devis_${d.numero}_${d.client}`);
const doc = DocumentApp.openById(copie.getId());
const corps = doc.getBody();
const tva = d.montant * 0.20;
const ttc = d.montant * 1.20;
corps.replaceText('{{NUMERO}}', d.numero);
corps.replaceText('{{DATE}}', d.date);
corps.replaceText('{{CLIENT}}', d.client);
corps.replaceText('{{DESCRIPTION}}', d.description);
corps.replaceText('{{MONTANT}}', d.montant.toFixed(2));
corps.replaceText('{{TVA}}', tva.toFixed(2));
corps.replaceText('{{TTC}}', ttc.toFixed(2));
doc.saveAndClose();
const pdfBlob = copie.getAs('application/pdf');
const dossier = DriveApp.getFolderById(ARCHIVE_FOLDER_ID);
const pdfFile = dossier.createFile(pdfBlob).setName(`Devis_${d.numero}.pdf`);
copie.setTrashed(true); // Met le Docs intermédiaire à la corbeille, conserve le PDF
return pdfFile;
}
Note : setTrashed(true) envoie en corbeille le Docs intermédiaire (réversible 30 jours). Le fichier PDF reste dans le dossier d’archive.
Étape 5 — Test isolé
function testGenerationPdf() {
const donnees = {
numero: 'DEV-TEST-001',
date: '04/05/2026',
client: 'Acme SARL',
description: 'Audit de sécurité Workspace — 2 jours',
montant: 1500
};
const pdf = genererPdfDevis(donnees);
Logger.log('PDF généré : ' + pdf.getUrl());
}
Sauvegarder, sélectionner testGenerationPdf dans le sélecteur de fonction, cliquer Exécuter. Au premier appel, autoriser les scopes demandés (Drive, Docs). Sortie attendue dans le panneau d’exécution : ligne PDF généré : https://drive.google.com/file/d/…. Ouvrir l’URL pour vérifier le PDF rempli.
Si une balise n’est pas remplacée : vérifier que le Docs contient bien {{NUMERO}} sans espaces ({ {NUMERO} } ou {{ NUMERO }} ne matchent pas).
Étape 6 — Envoi par mail
function envoyerDevisParMail(d, pdfFile) {
const objet = `Votre devis ${d.numero}`;
const corps =
`Bonjour ${d.client},
Veuillez trouver ci-joint le devis ${d.numero} d'un montant de ${d.montant.toFixed(2)} EUR HT.
Cordialement.`;
GmailApp.sendEmail(d.email, objet, corps, {
attachments: [pdfFile.getBlob()],
name: 'Service Commercial'
});
}
Quota MailApp Workspace : 1 500 destinataires / 24 h. Pour des volumes supérieurs, basculer sur Gmail API (quota par utilisateur plus généreux, à voir dans la documentation officielle).
Étape 7 — Menu personnalisé sur le Sheet
Le déclencheur simple onOpen installe automatiquement un menu à l’ouverture du Sheet :
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Devis')
.addItem('Envoyer le devis sélectionné', 'envoyerDevisLigneActive')
.addToUi();
}
function envoyerDevisLigneActive() {
const ui = SpreadsheetApp.getUi();
const feuille = SpreadsheetApp.getActiveSheet();
const ligne = feuille.getActiveRange().getRow();
if (ligne === 1) {
ui.alert('Sélectionnez une ligne de devis (pas l’en-tête).');
return;
}
const v = feuille.getRange(ligne, 1, 1, 8).getValues()[0];
const donnees = {
date: Utilities.formatDate(v[0], Session.getScriptTimeZone(), 'dd/MM/yyyy'),
numero: v[1],
client: v[2],
email: v[3],
description: v[4],
montant: parseFloat(v[5])
};
const pdf = genererPdfDevis(donnees);
envoyerDevisParMail(donnees, pdf);
feuille.getRange(ligne, 7).setValue('Envoyé');
feuille.getRange(ligne, 8).setValue(pdf.getUrl());
ui.alert(`Devis ${donnees.numero} envoyé à ${donnees.email}`);
}
Sauvegarder, fermer/rouvrir le Sheet : le menu Devis apparaît à droite de Aide.
Étape 8 — Manifeste OAuth explicite
Dans appsscript.json :
{
"timeZone": "Africa/Dakar",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly",
"https://www.googleapis.com/auth/documents",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/script.send_mail",
"https://www.googleapis.com/auth/script.container.ui"
]
}
Adapter timeZone. La déclaration explicite des scopes rend l’autorisation prévisible et auditable lors d’un partage du script à d’autres utilisateurs.
Étape 9 — Trigger temporel pour relances
Pour balayer le Sheet une fois par jour et relancer les devis envoyés depuis plus de 7 jours, ajouter une fonction et la planifier via Déclencheurs dans l’éditeur (icône horloge) :
function rappelDevisNonRepondus() {
const feuille = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
const donnees = feuille.getDataRange().getValues();
const seuil = new Date();
seuil.setDate(seuil.getDate() - 7);
for (let i = 1; i < donnees.length; i++) {
const dateDevis = donnees[i][0];
const statut = donnees[i][6];
const email = donnees[i][3];
if (statut === 'Envoyé' && dateDevis instanceof Date && dateDevis < seuil && email) {
GmailApp.sendEmail(email, `Rappel devis ${donnees[i][1]}`,
`Bonjour, nous revenons vers vous concernant le devis ${donnees[i][1]} envoyé le ${Utilities.formatDate(dateDevis, Session.getScriptTimeZone(), 'dd/MM/yyyy')}.`);
feuille.getRange(i + 1, 7).setValue('Relancé');
}
}
}
Dans Déclencheurs → Ajouter un déclencheur : fonction rappelDevisNonRepondus, source d’événement Déclenchement temporel, type Quotidien, plage horaire de votre choix.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Exécution timeout 6 min | Trop de lignes en un appel | Découper en lots avec PropertiesService et trigger récurrent |
| Authorization required | Premier appel d’un nouveau scope | Exécuter la fonction manuellement pour autoriser |
| Balises non remplacées | Espaces parasites dans le Docs ({ {NUMERO} }) |
Vérifier la casse exacte sans espaces |
| Mails en spam | SPF/DKIM non configurés | Configurer côté tenant (cf. tutoriel de setup, étape 8) |
| Quota MailApp dépassé | Plus de 1 500 destinataires/24 h | Workspace : attendre 24 h ou basculer sur Gmail API |
| Runtime Rhino actif | Projet ancien | Bascule V8 dans Paramètres du projet |
Tutoriels associés
- Construire une application AppSheet sans code — quand le besoin dépasse le scripting et nécessite une interface utilisateur.
- Mettre en place les Drive partagés Google Workspace — destination correcte pour le dossier d’archive.
Références officielles
- 🔝 Retour au guide de référence
- Quotas Apps Script
- Runtime V8 Apps Script
- DocumentApp — référence API
- GmailApp — référence API
- DriveApp — référence API
- Déclencheurs installables
- Manifeste appsscript.json
FAQ
Coût d’Apps Script ?
Inclus dans tous les plans Workspace, gratuit pour les comptes Consumer, sans coût additionnel. Les seules limites sont les quotas publiés.
Exécution sans utilisateur connecté ?
Possible via les déclencheurs temporels, qui exécutent en arrière-plan au nom du créateur du trigger.
Dépassement des 6 minutes ?
Pattern documenté : sauvegarde de la position courante via PropertiesService, exit volontaire avant 5 min 30, trigger qui reprend à la position sauvegardée.
Test sans envoi de vrais mails ?
Remplacer GmailApp.sendEmail par GmailApp.createDraft qui crée un brouillon dans la boîte sans l’envoyer.
Versionnement du code ?
L’éditeur intègre un historique des versions. Pour Git, utiliser clasp, l’outil officiel CLI Apps Script qui synchronise un projet avec un dépôt local.
Apps Script peut-il appeler une API externe ?
Oui via UrlFetchApp.fetch(), soumis au quota URL Fetch (100 000 / jour Workspace).