Le problème qui a tout déclenché
Je dois suivre la comptabilité de deux associations, l’une que je préside et l’autre dont je suis trésorier. C’est une tâche qui nécessite plus de rigueur que de compétences comptables, d’autant que je tiens à ce que tout soit accessible par les membres des bureaux associatifs dans un souci de transparence. J’ai donc opté pour une solution sur Google drive. Chaque exercice comptable possède son propre dossier dans lequel un tableur Google Sheets tient à jour les entrées/sorties. et où les justificatifs sont scannés et enregistrés.
Cela signifie que chaque opération, et chaque reçu que j’accumule implique qu’une deuxième tâche m’attendra plus tard : basculer Chrome sur le compte Google de l’association concernée, naviguer jusqu’au Google Sheets, saisir la date, le bénéficiaire ou la justification du versement, le mode de paiement, et enfin le montant dans la colonne débit ou crédit en fonction de la nature de l’opération.
Mon tableau de suivi est simple et efficace avec six colonnes : date, type, libellé, débit, crédit, solde (calculé pour chaque opération à l’aide d’une formule) mais la ressaisie manuelle après coup était une pure friction.
Ensuite vient la numérisation du justificatif, son renommage et son téléchargement dans le dossier correspondant du Google Drive. Rien de bien compliqué, j’en suis bien conscient, mais ce sont des opérations multiples qu’il faut prendre le temps de faire. J’ai donc décidé de construire un seul raccourci iPhone qui gère tout le cycle : photo du reçu, analyse, choix du paiement, écriture dans le Sheet, et archivage de l’image renommée dans Drive.
L’analyse du reçu elle-même (la partie OCR et IA qui reconnaît le montant, le bénéficiaire, le type de dépense) était un point que j’avais déjà exploré. Mais alors que je comptais pour cette première version sur un LLM embarqué (Google Gemma), je m’appuie désormais sur les modèles Apple Foundation Private Cloud Compute, notamment pour sa meilleure interprétation des mentions manuscrites sur les chèques. L’IA d’Apple a suffisamment progressé pour lui permettre de lire et extraire les champs pertinents. Mon vrai défi se trouvait en aval : faire entrer ces données dans Sheets et gérer la photo. C’est là que le vrai travail d’ingénierie a eu lieu.
Pourquoi j’ai écarté l’API officielle
Pour la partie écriture dans Sheets et archivage de la photo, j’ai posé le problème à Claude en partant de ce que j’avais déjà : l’extraction du montant, du bénéficiaire et du type de dépense fonctionnait dans un autre raccourci, donc pas besoin de revenir dessus. Ce qui me manquait, c’était la mécanique pour faire atterrir ces données à la fin d’un Google Sheets plutôt que dans un tableau Numbers local (pour lequel il existe une action standard), et la possibilité de conserver la photo du ticket quelque part. J’ai simplement demandé s’il existait une API gratuite pour ça.
La réponse a écarté l’API officielle Google Sheets v4 d’emblée elle exige une authentification OAuth 2.0 : un flux d’autorisation, une gestion de tokens, des tokens de rafraîchissement. Raccourcis n’a aucun mécanisme natif pour gérer proprement un cycle de token OAuth, donc cette voie est vite devenue hostile. Direction écartée immédiatement plutôt que de se battre contre la plateforme.
À la place, j’ai été orienté vers Apps Script déployé en tant qu’application web. Cette approche expose un script via une URL HTTP publique, sans authentification requise côté appelant. La logique est simple : Raccourcis envoie une requête POST contenant du JSON, Apps Script la reçoit, écrit dans le Sheet, dépose la photo dans Drive si une photo est fournie, et renvoie une confirmation JSON. Un seul fichier raccourci, un seul point d’accès, aucun jonglage de token.
Le code fourni n’a pas vocation à être lu ligne par ligne. Il se colle directement dans l’éditeur Apps Script de Google, à l’intérieur d’un nouveau projet créé depuis script.google.com. Une fois collé, il suffit de le déployer en application web pour obtenir une URL, et c’est cette URL que le raccourci appelle à chaque envoi de données.
const SHEET_NAME = "NOM DE LA FEUILLE DANS LE DOCUMENT SHEETS";
const DRIVE_FOLDER_ID = "IDENTIFIANT DU DOSSIER RECEVANT L'IMAGE";
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const ss = SpreadsheetApp.openById("IDENTIFIANT DU DOCUMENT GOOGLE SHEETS");
const sheet = ss.getSheetByName(SHEET_NAME);
// Cherche la première ligne vide dans la plage A2:A38
const range = sheet.getRange("A2:A38");
const values = range.getValues();
let firstEmptyRow = -1;
for (let i = 0; i < values.length; i++) {
if (values[i][0] === "") {
firstEmptyRow = i + 2;
break;
}
}
if (firstEmptyRow === -1) {
return ContentService
.createTextOutput(JSON.stringify({ success: false, error: "Plage pleine" }))
.setMimeType(ContentService.MimeType.JSON);
}
// Split de la chaîne sur le point-virgule
const cols = data.chaine.split(";");
// Upload de la photo si présente
let photoUrl = "";
if (data.photo_base64) {
photoUrl = savePhotoToDrive(data.photo_base64, data.filename);
}
// Écrit les colonnes dans l'ordre
sheet.getRange(firstEmptyRow, 1).setValue(cols[0] || ""); // date
sheet.getRange(firstEmptyRow, 2).setValue(cols[1] || ""); // type
sheet.getRange(firstEmptyRow, 3).setValue(cols[2] || ""); // libellé
sheet.getRange(firstEmptyRow, 4).setValue(cols[3] || ""); // débit ou vide
sheet.getRange(firstEmptyRow, 5).setValue(cols[4] || ""); // crédit ou absent
return ContentService
.createTextOutput(JSON.stringify({ success: true, photo_url: photoUrl, ligne: firstEmptyRow }))
.setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ success: false, error: err.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function savePhotoToDrive(base64Data, filename) {
const folder = DriveApp.getFolderById(DRIVE_FOLDER_ID);
const blob = Utilities.newBlob(
Utilities.base64Decode(base64Data),
"image/jpeg",
filename || "ticket_" + new Date().getTime() + ".jpg"
);
const file = folder.createFile(blob);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return file.getUrl();
} Construire le raccourci et déboguer le script
Côté Raccourcis, un menu propose trois options : prendre une photo, en choisir une dans la galerie, ou passer cette étape pour une saisie uniquement manuelle. Quand une photo est choisie, le raccourci la redimensionne à 800px, la convertit en JPEG, et l’encode en Base64. Apparaît ensuite un menu de mode de paiement, avec un rappel de ce qui a été détecté par Apple Foundation : je confirme ou je corrige, ce qui couvre les cas particuliers comme les dons où aucun document n’existe.
Un détail mérite d’être su avant d’utiliser ce script tel quel : la ligne qui enregistre la photo dans Drive la rend accessible à quiconque possède le lien, sans authentification ANYONE_WITH_LINK. C’est nécessaire parce que l’URL de la photo doit pouvoir circuler librement dans le JSON de retour sans que Raccourcis ait besoin de gérer une couche d’authentification supplémentaire. Dans les faits, personne ne possède ce lien à part moi : il n’est jamais partagé, jamais affiché publiquement, il existe uniquement dans la réponse JSON que mon raccourci reçoit et n’exploite pas davantage. Le risque réel est donc faible, mais il existe, et je préfère le dire plutôt que de le passer sous silence.
Les données voyagent sous la forme d’une seule chaîne séparée par des points-virgules plutôt qu’en champs JSON distincts en utilisant l’action « obtenir le contenu de l’URL », ce qui simplifie la construction du texte dans Raccourcis. Fait notable, deux points-virgules consécutifs produisent une cellule vide, ce qui correspond parfaitement à la logique comptable : une écriture est soit un débit, soit un crédit, jamais les deux. Côté script, je recherche la première ligne vide dans une plage fixe qui correspond à l’emplacement où j’indique les opérations dans la feuille de calcul plutôt que d’utiliser appendRow(). Cela répond à mon besoin de préserver les zones pour des formules en dehors de cette plage de mon tableau comptable.
Trois bugs m’ont ralenti. D’abord, getActiveSpreadsheet() renvoyait null dans le contexte d’une application web, donc je suis passé à openById(). Ensuite, un décalage entre ma constante SHEET_NAME et le nom réel de l’onglet a tout cassé jusqu’à ce que je le fasse correspondre caractère pour caractère. Enfin, une URL de déploiement tronquée produisait une erreur « le fichier n’existe pas » résolue en copiant strictement depuis Gérer les déploiements.
Pour les mises à jour futures, j’utilise toujours Nouvelle version, jamais Nouveau déploiement. La distinction se trouve dans l’éditeur Apps Script, sous le bouton bleu Déployer en haut à droite. Quand on clique dessus puis sur Gérer les déploiements, on trouve une icône en forme de crayon qui permet de modifier le déploiement actif, c’est là qu’apparaît le choix entre les deux options.
« Nouveau déploiement » crée une deuxième instance du script avec sa propre URL, ce qui casserait immédiatement le raccourci, puisque celui-ci pointe vers l’URL d’origine, copiée une seule fois lors de la configuration initiale. « Nouvelle version » en revanche met à jour le code derrière l’URL existante, sans rien changer côté Raccourcis. Je modifie le script dans l’éditeur, je clique sur Déployer, je choisis Gérer les déploiements, je sélectionne Nouvelle version, et l’automatisation continue de fonctionner sans la moindre intervention sur l’appareil.
Au moment de déployer le script en application web, Google demande de choisir qui peut y accéder, et l’option à sélectionner est « Tout le monde » — un choix qui peut surprendre la première fois qu’on le voit. Ce n’est pas un relâchement de sécurité sur le Sheet ou le dossier Drive : c’est uniquement ce qui permet à Raccourcis d’appeler l’URL du script sans passer par un login Google. « Tout le monde » signifie que l’URL peut recevoir une requête POST sans authentification — pas que n’importe qui peut consulter mon tableau de dépenses. Le script s’exécute toujours avec mes propres autorisations, sur mes propres fichiers ; seul l’appel initial ne demande pas de session ouverte.
Notes éditoriales
- Script testé avec un Google Sheets personnel, structure à six colonnes (date, type, libellé, débit, crédit, solde).
- L’analyse OCR et IA des reçus s’appuie sur les modèles Apple Foundation locaux et en ligne, disponibles selon les appareils et les régions.
- La plage de recherche de ligne vide est fixée à A2:A38 dans mon tableau, à adapter à la taille du vôtre avant utilisation.
- Les photos archivées dans Drive sont accessibles via lien, sans authentification. Le lien lui-même n’est jamais partagé ni rendu public.
- Le raccourci est disponible en téléchargement ici : https://www.icloud.com/shortcuts/7612c08b14d64ed09598f4bfa51e1d67.
