/************************************************************ * CONFIGURATION GENERALE ************************************************************/ const MANAGER_TOKEN = "COLLEZ_VOTRE_CLE_API_ICI"; const MANAGER_URL = "https://zeintektest6.cpt.manager.cloud/api2"; const FEUILLE_JOURNAL = "Journal"; const COLONNE_STATUT = 10; const COMPTES_BANQUE_CAISSE = ["521000", "571000"]; /************************************************************ * MENU GOOGLE SHEETS ************************************************************/ function onOpen() { SpreadsheetApp.getUi() .createMenu("Manager.io") .addItem("Tester la connexion", "testerConnexionManager") .addItem("Charger le plan comptable (test)", "testerPlanComptable") .addSeparator() .addItem("Envoyer les écritures", "envoyerJournalManager") .addToUi(); } /************************************************************ * FONCTION GENERIQUE D'APPEL API ************************************************************/ function appelerManager(methode, endpoint, payload) { const options = { method: methode, headers: { "X-API-KEY": MANAGER_TOKEN, "Accept": "application/json" }, muteHttpExceptions: true }; if (payload !== undefined && payload !== null) { options.contentType = "application/json"; options.payload = JSON.stringify(payload); } const url = MANAGER_URL + endpoint; const response = UrlFetchApp.fetch(url, options); const code = response.getResponseCode(); const contenu = response.getContentText(); Logger.log("URL : " + url); Logger.log("Méthode : " + methode); Logger.log("Code HTTP : " + code); Logger.log("Réponse : " + contenu); if (code < 200 || code >= 300) { throw new Error( "Erreur HTTP " + code + " sur " + endpoint + ". Réponse Manager.io : " + contenu ); } if (!contenu) return {}; try { return JSON.parse(contenu); } catch (e) { throw new Error("Réponse JSON invalide de Manager.io : " + contenu); } } /************************************************************ * TEST DE CONNEXION ************************************************************/ function testerConnexionManager() { const ui = SpreadsheetApp.getUi(); try { const json = appelerManager("get", "/journal-entries"); const nomEntreprise = json.business && json.business.name ? json.business.name : "Connexion API confirmée"; const nombreEcritures = json.totalRecords !== undefined ? json.totalRecords : "Non communiqué"; ui.alert( "Connexion réussie\n\nEntreprise : " + nomEntreprise + "\nNombre d'écritures : " + nombreEcritures ); } catch (err) { ui.alert("Échec de la connexion\n\n" + err.message); } } /************************************************************ * CHARGEMENT DU PLAN COMPTABLE ************************************************************/ function chargerPlanComptable() { const json = appelerManager("get", "/chart-of-accounts"); if (!json.chartOfAccounts) { throw new Error( "Le plan comptable est absent de la réponse de l'API." ); } const comptes = {}; json.chartOfAccounts.forEach(function(compte) { if ( compte.code !== null && compte.code !== undefined && compte.code !== "" && compte.key ) { comptes[String(compte.code).trim()] = compte.key; } }); return comptes; } /************************************************************ * TEST DU PLAN COMPTABLE ************************************************************/ function testerPlanComptable() { const ui = SpreadsheetApp.getUi(); try { const comptes = chargerPlanComptable(); Logger.log(JSON.stringify(comptes, null, 2)); ui.alert( "Plan comptable chargé avec succès.\n\nNombre de comptes : " + Object.keys(comptes).length ); } catch (err) { ui.alert("Erreur lors du chargement du plan comptable\n\n" + err.message); } } /************************************************************ * CHARGEMENT DES BANQUES ET CAISSES ************************************************************/ function chargerBanquesEtCaisses() { const json = appelerManager("get", "/bank-and-cash-accounts"); if (!json.bankAndCashAccounts) { throw new Error( "La liste des banques et caisses est absente de la réponse API." ); } const banques = {}; json.bankAndCashAccounts.forEach(function(compte) { if (compte.name && compte.key) { banques[String(compte.name).trim()] = compte.key; } }); return banques; } /************************************************************ * VALIDATION ET FORMATAGE DE LA DATE ************************************************************/ function convertirDate(dateCellule) { let date; if (dateCellule instanceof Date && !isNaN(dateCellule.getTime())) { date = dateCellule; } else if (dateCellule !== "" && dateCellule !== null) { date = new Date(dateCellule); } else { throw new Error("La date de l'écriture est vide."); } if (isNaN(date.getTime())) { throw new Error("La date de l'écriture est invalide."); } return Utilities.formatDate( date, Session.getScriptTimeZone(), "yyyy-MM-dd" ); } /************************************************************ * ENVOI DES ECRITURES COMPTABLES * * Colonnes attendues dans la feuille Journal : * A : Date * B : Référence * C : Libellé * D : Compte débit * E : Banque/caisse débit (si applicable) * F : Montant débit * G : Compte crédit * H : Banque/caisse crédit (si applicable) * I : Montant crédit * J : Statut ************************************************************/ function envoyerJournalManager() { const ui = SpreadsheetApp.getUi(); const verrou = LockService.getScriptLock(); if (!verrou.tryLock(5000)) { ui.alert( "Un autre transfert est déjà en cours. Réessayez dans quelques instants." ); return; } try { const classeur = SpreadsheetApp.getActiveSpreadsheet(); const sheet = classeur.getSheetByName(FEUILLE_JOURNAL); if (!sheet) { ui.alert("La feuille '" + FEUILLE_JOURNAL + "' est introuvable."); return; } if ( !MANAGER_TOKEN || MANAGER_TOKEN === "COLLEZ_VOTRE_CLE_API_ICI" ) { ui.alert("Veuillez renseigner votre clé API Manager.io dans MANAGER_TOKEN."); return; } const derniereLigne = sheet.getLastRow(); if (derniereLigne < 2) { ui.alert("Aucune écriture à envoyer."); return; } // Charger les références avant de commencer le transfert. const comptes = chargerPlanComptable(); const banques = chargerBanquesEtCaisses(); const data = sheet .getRange(2, 1, derniereLigne - 1, COLONNE_STATUT) .getValues(); let nbEnvoyes = 0; let nbErreurs = 0; let nbIgnores = 0; for (let i = 0; i < data.length; i++) { const ligne = data[i]; const numeroLigne = i + 2; const statut = String(ligne[COLONNE_STATUT - 1] || "") .trim() .toUpperCase(); if (statut === "ENVOYE" || statut === "ENVOYÉ") { nbIgnores++; continue; } const ligneVide = ligne.slice(0, 9).every(function(valeur) { return valeur === "" || valeur === null; }); if (ligneVide) { nbIgnores++; continue; } try { const date = convertirDate(ligne[0]); const reference = String(ligne[1] || "").trim(); const narration = String(ligne[2] || "").trim(); const compteDebitCode = String(ligne[3] || "").trim(); const auxDebit = String(ligne[4] || "").trim(); const compteCreditCode = String(ligne[6] || "").trim(); const auxCredit = String(ligne[7] || "").trim(); if (!compteDebitCode || !compteCreditCode) { throw new Error("Le compte de débit ou de crédit est vide."); } if ( ligne[5] === "" || ligne[5] === null || ligne[8] === "" || ligne[8] === null ) { throw new Error("Le montant débit ou crédit est vide."); } const debit = Number(ligne[5]); const credit = Number(ligne[8]); if ( !Number.isFinite(debit) || !Number.isFinite(credit) || debit <= 0 || credit <= 0 ) { throw new Error( "Les montants débit et crédit doivent être numériques et supérieurs à zéro." ); } if (Math.abs(debit - credit) > 0.000001) { throw new Error( "Écriture non équilibrée : débit = " + debit + ", crédit = " + credit ); } const compteDebitKey = comptes[compteDebitCode]; const compteCreditKey = comptes[compteCreditCode]; if (!compteDebitKey) { throw new Error( "Compte débit introuvable dans Manager.io : " + compteDebitCode ); } if (!compteCreditKey) { throw new Error( "Compte crédit introuvable dans Manager.io : " + compteCreditCode ); } const ligneDebit = { Account: compteDebitKey, LineDescription: narration, Debit: debit }; const ligneCredit = { Account: compteCreditKey, LineDescription: narration, Credit: credit }; if (COMPTES_BANQUE_CAISSE.indexOf(compteDebitCode) !== -1) { if (!auxDebit || !banques[auxDebit]) { throw new Error( "Banque/caisse débit introuvable : " + auxDebit ); } ligneDebit.BankOrCashAccount = banques[auxDebit]; } if (COMPTES_BANQUE_CAISSE.indexOf(compteCreditCode) !== -1) { if (!auxCredit || !banques[auxCredit]) { throw new Error( "Banque/caisse crédit introuvable : " + auxCredit ); } ligneCredit.BankOrCashAccount = banques[auxCredit]; } const payload = { Date: date, Reference: reference, Narration: narration, Lines: [ligneDebit, ligneCredit] }; Logger.log("Envoi de la ligne Google Sheets " + numeroLigne); Logger.log("Payload : " + JSON.stringify(payload)); /* * Création directe de l'écriture. * Si l'API de votre instance exige un autre format de payload, * le message complet de l'API apparaîtra dans le statut. */ appelerManager("post", "/journal-entry-form", payload); sheet.getRange(numeroLigne, COLONNE_STATUT).setValue("ENVOYE"); nbEnvoyes++; Utilities.sleep(300); } catch (err) { const message = String(err.message || err); sheet .getRange(numeroLigne, COLONNE_STATUT) .setValue("ERREUR : " + message.substring(0, 450)); Logger.log("Erreur ligne " + numeroLigne + " : " + message); nbErreurs++; } } ui.alert( "Import terminé\n\n" + "Écritures envoyées : " + nbEnvoyes + "\nErreurs : " + nbErreurs + "\nLignes ignorées : " + nbIgnores + "\n\nConsultez la colonne J et le journal d'exécution pour les détails." ); } catch (err) { ui.alert("Le transfert n'a pas pu être lancé.\n\n" + err.message); Logger.log(err.stack || err.message); } finally { verrou.releaseLock(); } }