using Services.ReActAgent; using System.Text; using System.Text.RegularExpressions; using ToolsServices; namespace Services { public static class FactureService { #region Variables private static readonly string NomFichierParametres = FichiersInternesService.ParamsFactures; #endregion #region Méthodes publiques public static async Task<(bool, string)> Traitement(string inputPath, string outputPath, bool isApiExterne) { StringBuilder sb = new(); LoggerService.LogInfo($"FactureService.Traitement"); if (!Directory.Exists(inputPath)) { LoggerService.LogWarning($"Le dossier {inputPath} n'existe pas."); return (false, $"Le dossier {inputPath} n'existe pas."); } if (!Directory.Exists(outputPath)) { LoggerService.LogWarning($"Le dossier {outputPath} n'existe pas."); return (false, $"Le dossier {outputPath} n'existe pas."); } var fichiers = Directory .EnumerateFiles(inputPath, "*.*", SearchOption.AllDirectories) .Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) .ToArray(); if (fichiers.Length == 0) { LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {inputPath}"); return (false, $"Aucun fichier trouvé dans le dossier spécifié : {inputPath}"); } var reActRagAgent = new ReActAgent.ReActAgent(); var isOk = true; foreach (var fichier in fichiers) { try { #region Analyse de la facture var facturePDF = FilesService.ExtractText(fichier); //facturePDF = CleanPdfText(facturePDF); //facturePDF = SegmentInvoice(facturePDF); var prompt = PromptService.GetPrompt(PromptService.ePrompt.FactureService_AnalyserFacture_Extract, facturePDF); LoggerService.LogInfo($"Traitement de facture PDF to XML : {fichier}"); var (reponse,m) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.Facture_To_Xml, true, prompt, "Traitement de facture PDF to XML", "", isApiExterne); if (!XmlService.IsXML(reponse)) { sb.AppendLine($"Le résultat n'est pas au format XML pour le fichier : {fichier}"); LoggerService.LogWarning($"Le résultat n'est pas au format XML pour le fichier : {fichier}"); string fileNameXML = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + ".xml"); TxtService.CreateTextFile(fileNameXML, reponse); } else { #region Sauvegarde du résultat LoggerService.LogDebug($"Le résultat est au format XML pour le fichier : {fichier}"); string fileNameXML = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + ".xml"); TxtService.CreateTextFile(fileNameXML, reponse); #endregion } #endregion } catch (Exception ex) { isOk = false; LoggerService.LogError($"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}"); } } LoggerService.LogInfo($"Traitement des factures PDF to XML terminée"); return (isOk, sb.ToString()); } public static (string, string) LoadParametres() { LoggerService.LogInfo("FactureService.LoadParametres"); //ParametresOllamaService SelectedItem = new(); try { string FicheMission = ""; string PathCV = ""; if (File.Exists(NomFichierParametres)) { string[] lignes = File.ReadAllLines(NomFichierParametres); if (lignes.Length > 0) FicheMission = lignes[0]; if (lignes.Length > 1) PathCV = lignes[1]; } return (FicheMission, PathCV); } catch { LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}"); return ("", ""); } } public static bool SaveParametres(string inputPath, string outputPath) { LoggerService.LogInfo("FactureService.SaveParametres"); try { StringBuilder sb = new(); sb.AppendLine(inputPath); sb.AppendLine(outputPath); File.WriteAllText(NomFichierParametres, sb.ToString()); return true; } catch { LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}"); return false; } } #endregion #region Méthodes privées private static string CleanPdfText(string rawText) { var text = rawText; // Normaliser les fins de ligne text = text.Replace("\r", "\n"); // Supprimer les caractères de contrôle (sauf \n) text = new string(text.Where(c => !char.IsControl(c) || c == '\n').ToArray()); // Supprimer les en-têtes et pieds de page typiques (ex: "Page 1/3", "Facture ACME", etc.) text = Regex.Replace(text, @"Page\s+\d+(/\d+)?", "", RegexOptions.IgnoreCase); text = Regex.Replace(text, @"Facture\s+n[°º]\s*\d+", "", RegexOptions.IgnoreCase); // Uniformiser les espaces : tabulations → espace, et espaces multiples → 1 seul text = Regex.Replace(text, @"[ \t]+", " "); // Supprimer les lignes vides excessives text = Regex.Replace(text, @"\n{2,}", "\n"); // Supprimer les traits de séparation (souvent des "-----" ou "=====") text = Regex.Replace(text, @"[-=]{3,}", ""); // Corriger les montants : ex "1 234,56 €" → "1234.56 EUR" text = Regex.Replace(text, @"(\d{1,3}(?:[ \u00A0]\d{3})*,\d{2}) ?€", m => m.Groups[1].Value.Replace(" ", "").Replace("\u00A0", "").Replace(",", ".") + " EUR"); // Corriger les dates : ex "01-02-2025" → "2025-02-01" text = Regex.Replace(text, @"\b(\d{2})[-/](\d{2})[-/](\d{4})\b", "$3-$2-$1"); // Fusionner les lignes coupées artificiellement (ex: "TOTAL\n123,45" → "TOTAL 123,45") text = Regex.Replace(text, @"([A-Za-z])\n(\d)", "$1 $2"); // Supprimer les espaces en trop au début et fin text = text.Trim(); return text; } private static string SegmentInvoice(string text) { var sb = new StringBuilder(); // Bloc fournisseur var fournisseurMatch = Regex.Match(text, @"(?i)(?:fournisseur|vendor|seller).{0,50}\n(.+?)(\n|$)"); if (fournisseurMatch.Success) { sb.AppendLine("=== FOURNISSEUR ==="); sb.AppendLine(fournisseurMatch.Groups[1].Value.Trim()); } // Bloc client var clientMatch = Regex.Match(text, @"(?i)(?:client|acheteur|buyer|bill to|destinataire).{0,50}\n(.+?)(\n|$)"); if (clientMatch.Success) { sb.AppendLine("\n=== CLIENT ==="); sb.AppendLine(clientMatch.Groups[1].Value.Trim()); } // Numéro de facture var numeroMatch = Regex.Match(text, @"(?i)facture[^\d]*(\d+)"); if (numeroMatch.Success) { sb.AppendLine("\n=== NUMERO FACTURE ==="); sb.AppendLine(numeroMatch.Groups[1].Value.Trim()); } // Date de facture var dateMatch = Regex.Match(text, @"(?i)(?:date|issued|emission)[^\d]*(\d{4}-\d{2}-\d{2})"); if (dateMatch.Success) { sb.AppendLine("\n=== DATE FACTURE ==="); sb.AppendLine(dateMatch.Groups[1].Value.Trim()); } // Bloc lignes de facture (simplifié : cherche un tableau prix/quantité) var lignesMatch = Regex.Match(text, @"(?is)(description.*?total)", RegexOptions.IgnoreCase); if (lignesMatch.Success) { sb.AppendLine("\n=== LIGNES FACTURE ==="); sb.AppendLine(lignesMatch.Groups[0].Value.Trim()); } // Total général var totalMatch = Regex.Match(text, @"(?i)(total (?:ttc|general|amount)).{0,10}([\d.,]+ ?eur)"); if (totalMatch.Success) { sb.AppendLine("\n=== TOTAL GENERAL ==="); sb.AppendLine(totalMatch.Groups[2].Value.Trim()); } // Retour du texte segmenté return sb.Length > 0 ? sb.ToString() : text; } #endregion } }