Files
demat-facturation/server/freeproService.ts

177 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<Record<string, unknown>>(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<string, number>();
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 || "");
// Les lignes sans structure sont affectées à 3069UDA par convention
const structureRaw = row.structure ? String(row.structure).trim() : "";
const structure = structureRaw !== "" ? structureRaw : "3069UDA";
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 [structureVal, type] = key.split("|||");
lines.push({
structure: structureVal || null,
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,
};
}