/** * FreePro Ventilation Service * Transforms a FreePro Excel invoice file into a ventilation summary (structure × type → TTC) * * Transformation rules (from Process Scripting facture freepro.txt): * 1. Colonne "script": =SI(OU(F2="Offre Freebox Pro";F2="Support Premium";ESTNUM(TROUVE("Remise première année";F2)));"Lien fibre";"Tél. mobile") * 2. Colonne "ttc": =ARRONDI(total_ht * 1.2, 2) * 3. Colonne "type": =SI(ESTNUM(TROUVE("Routeur 5G"; label_client)); "Lien 5G"; script) * 4. TCD: agrégation par (structure, type) → somme des TTC */ import * as xlsx from "xlsx"; export interface FreeproSourceRow { ref_piece: string; date: Date | null; reference_service: string; label_client: string; structure: string; description: string; debut_factu: Date | null; fin_factu: Date | null; quantite: number; prix: number; total: number; } export interface FreeproVentilationResult { refPiece: string; moisLabel: string; annee: number; mois: number; nbLignes: number; totalTtc: number; lines: Array<{ structure: string | null; type: string; montant: number; // TTC en euros }>; } /** * Détermine la colonne "script" selon la description */ function getScript(description: string): string { if (!description) return "Tél. mobile"; if ( description === "Offre Freebox Pro" || description === "Support Premium" || description.includes("Remise première année") ) { return "Lien fibre"; } return "Tél. mobile"; } /** * Calcule le TTC : arrondi(total_ht * 1.2, 2) */ function getTtc(totalHt: number): number { return Math.round(totalHt * 1.2 * 100) / 100; } /** * Détermine le "type" final selon label_client et script */ function getType(labelClient: string, script: string): string { if (labelClient && labelClient.includes("Routeur 5G")) { return "Lien 5G"; } return script; } /** * Parse et transforme un fichier Excel FreePro (Buffer) * Retourne les données de ventilation agrégées */ export function processFreeproExcel( buffer: Buffer, moisLabel: string, fileName?: string ): FreeproVentilationResult { // Détecter si c'est un CSV (par extension ou par détection du contenu) const isCsv = fileName ? /\.csv$/i.test(fileName) : !buffer[0] || (buffer[0] !== 0x50 && buffer[0] !== 0xD0); // PK = xlsx, D0CF = xls const wb = isCsv ? xlsx.read(buffer.toString("utf8"), { type: "string", cellDates: true }) : xlsx.read(buffer, { type: "buffer", cellDates: true }); const ws = wb.Sheets[wb.SheetNames[0]]; const rawRows = xlsx.utils.sheet_to_json>(ws, { header: [ "ref_piece", "date", "reference_service", "label_client", "structure", "description", "debut_factu", "fin_factu", "quantite", "prix", "total", ], range: 1, // skip header row defval: null, }); if (rawRows.length === 0) { throw new Error("Le fichier Excel est vide ou ne contient pas de données."); } // Extraire ref_piece et date du mois depuis la première ligne const firstRow = rawRows[0]; const refPiece = String(firstRow.ref_piece || ""); // Parser moisLabel "MM/YYYY" const [moisStr, anneeStr] = moisLabel.split("/"); const mois = parseInt(moisStr, 10); const annee = parseInt(anneeStr, 10); // Agrégation par (structure, type) → somme TTC en centimes (pour éviter les flottants) const aggregation = new Map(); let nbLignes = 0; for (const row of rawRows) { if (!row.ref_piece) continue; nbLignes++; const description = String(row.description || ""); const labelClient = String(row.label_client || ""); const structure = row.structure ? String(row.structure) : null; const totalHt = parseFloat(String(row.total || "0").replace(",", ".")) || 0; const script = getScript(description); const ttc = getTtc(totalHt); const type = getType(labelClient, script); const key = `${structure ?? ""}|||${type}`; aggregation.set(key, (aggregation.get(key) || 0) + Math.round(ttc * 100)); } // Construire les lignes de résultat const lines: FreeproVentilationResult["lines"] = []; for (const [key, centimes] of Array.from(aggregation.entries())) { const [structureRaw, type] = key.split("|||"); lines.push({ structure: structureRaw === "" ? null : structureRaw, type, montant: centimes / 100, }); } // Trier par structure (null en dernier) puis type lines.sort((a, b) => { const sa = a.structure ?? "\uFFFF"; // null → fin de liste const sb = b.structure ?? "\uFFFF"; if (sa !== sb) return sa.localeCompare(sb); return a.type.localeCompare(b.type); }); const totalTtc = Math.round(lines.reduce((sum, l) => sum + l.montant, 0) * 100) / 100; return { refPiece, moisLabel, annee, mois, nbLignes, totalTtc, lines, }; }