Retour au blog

AutoProposal V2 : Automatisation PDF avec Python

12 Septembre 2024 12 min de lecture Python

Comment j'ai développé une solution complète d'extraction de données PDF pour automatiser la saisie des propales dans un CRM. Retour d'expérience technique avec code source et techniques d'OCR.

Le contexte du projet

Client : Entreprise de services (Marseille)

Problème : Les commerciaux passaient 2h par jour à saisir manuellement les données des propales PDF dans le CRM

Objectif : Automatiser l'extraction et permettre la validation avant intégration

💡 Challenge technique : Les PDFs provenaient de différents fournisseurs avec des formats variés. Il fallait gérer la reconnaissance de texte (OCR) et l'extraction de données structurées.

Architecture de la solution

La solution AutoProposal V2 se compose de 3 modules principaux :

  1. Module d'upload : Interface web pour déposer les PDFs
  2. Module d'extraction : OCR + parsing des données
  3. Module d'intégration : Validation + import dans le CRM

Article Premium

Cet article contient du code source complet, des techniques avancées d'OCR et des exemples d'intégration CRM.

Ce que vous découvrirez :

  • Code Python complet pour l'extraction PDF
  • Techniques d'OCR avec Tesseract
  • Interface web Flask responsive
  • Intégration CRM avec API REST
  • Gestion d'erreurs et validation
  • Architecture modulaire et évolutive

1. Installation des dépendances

# requirements.txt
Flask==2.3.3
PyPDF2==3.0.1
pytesseract==0.3.10
Pillow==10.0.0
pandas==2.0.3
openpyxl==3.1.2
python-dotenv==1.0.0

2. Module d'extraction PDF

Voici le cœur de la solution : l'extraction intelligente des données PDF.

import PyPDF2
import pytesseract
from PIL import Image
import re
import json

class PDFExtractor:
    def __init__(self):
        self.patterns = {
            'montant': r'(\d{1,3}(?:\s?\d{3})*(?:[.,]\d{2})?)\s*€',
            'date': r'(\d{1,2}[./-]\d{1,2}[./-]\d{2,4})',
            'client': r'Client[:\s]+([^\n\r]+)',
            'reference': r'Réf[:\s]+([A-Z0-9-]+)'
        }
    
    def extract_text_from_pdf(self, pdf_path):
        """Extraction du texte depuis un PDF"""
        text = ""
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                for page in pdf_reader.pages:
                    text += page.extract_text() + "\n"
        except Exception as e:
            print(f"Erreur extraction PDF: {e}")
            return None
        return text
    
    def extract_with_ocr(self, pdf_path):
        """Extraction via OCR si le PDF est scanné"""
        try:
            # Conversion PDF vers images
            images = convert_from_path(pdf_path)
            text = ""
            for image in images:
                text += pytesseract.image_to_string(image, lang='fra') + "\n"
            return text
        except Exception as e:
            print(f"Erreur OCR: {e}")
            return None
    
    def parse_data(self, text):
        """Parsing des données extraites"""
        data = {}
        
        for key, pattern in self.patterns.items():
            matches = re.findall(pattern, text, re.IGNORECASE)
            if matches:
                data[key] = matches[0] if len(matches) == 1 else matches
        
        # Nettoyage des données
        if 'montant' in data:
            data['montant'] = self.clean_amount(data['montant'])
        
        return data
    
    def clean_amount(self, amount_str):
        """Nettoyage du montant"""
        # Supprimer les espaces et normaliser les séparateurs
        amount_str = amount_str.replace(' ', '').replace(',', '.')
        try:
            return float(amount_str)
        except ValueError:
            return None

3. Interface web Flask

Interface simple et intuitive pour les commerciaux :

from flask import Flask, render_template, request, jsonify
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads/'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max

@app.route('/')
def index():
    return render_template('upload.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'Aucun fichier sélectionné'})
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'Aucun fichier sélectionné'})
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
        # Extraction des données
        extractor = PDFExtractor()
        text = extractor.extract_text_from_pdf(filepath)
        
        if not text or len(text.strip()) < 50:
            # Essayer avec OCR
            text = extractor.extract_with_ocr(filepath)
        
        if text:
            data = extractor.parse_data(text)
            return jsonify({
                'success': True,
                'data': data,
                'raw_text': text[:500] + '...' if len(text) > 500 else text
            })
        else:
            return jsonify({'error': 'Impossible d\'extraire le texte du PDF'})
    
    return jsonify({'error': 'Format de fichier non supporté'})

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() == 'pdf'

if __name__ == '__main__':
    app.run(debug=True)

4. Interface utilisateur (HTML/JavaScript)

<!-- upload.html -->
<!DOCTYPE html>
<html>
<head>
    <title>AutoProposal V2</title>
    <style>
        .upload-area {
            border: 2px dashed #ccc;
            border-radius: 10px;
            padding: 40px;
            text-align: center;
            margin: 20px 0;
        }
        .upload-area.dragover {
            border-color: #007bff;
            background-color: #f8f9fa;
        }
        .data-preview {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
            margin: 20px 0;
        }
        .form-group {
            margin: 10px 0;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .form-group input {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <h1>AutoProposal V2</h1>
    
    <div class="upload-area" id="uploadArea">
        <p>Glissez-déposez votre PDF ici ou cliquez pour sélectionner</p>
        <input type="file" id="fileInput" accept=".pdf" style="display: none;">
    </div>
    
    <div id="dataPreview" style="display: none;">
        <h3>Données extraites :</h3>
        <div class="data-preview">
            <div class="form-group">
                <label>Client :</label>
                <input type="text" id="client">
            </div>
            <div class="form-group">
                <label>Montant :</label>
                <input type="number" id="montant" step="0.01">
            </div>
            <div class="form-group">
                <label>Date :</label>
                <input type="date" id="date">
            </div>
            <div class="form-group">
                <label>Référence :</label>
                <input type="text" id="reference">
            </div>
            <button onclick="saveToCRM()">Sauvegarder dans le CRM</button>
        </div>
    </div>

    <script>
        const uploadArea = document.getElementById('uploadArea');
        const fileInput = document.getElementById('fileInput');
        const dataPreview = document.getElementById('dataPreview');

        uploadArea.addEventListener('click', () => fileInput.click());
        uploadArea.addEventListener('dragover', handleDragOver);
        uploadArea.addEventListener('drop', handleDrop);

        function handleDragOver(e) {
            e.preventDefault();
            uploadArea.classList.add('dragover');
        }

        function handleDrop(e) {
            e.preventDefault();
            uploadArea.classList.remove('dragover');
            const files = e.dataTransfer.files;
            if (files.length > 0) {
                handleFile(files[0]);
            }
        }

        fileInput.addEventListener('change', (e) => {
            if (e.target.files.length > 0) {
                handleFile(e.target.files[0]);
            }
        });

        function handleFile(file) {
            const formData = new FormData();
            formData.append('file', file);

            fetch('/upload', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    displayData(data.data);
                } else {
                    alert('Erreur: ' + data.error);
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('Erreur lors du traitement du fichier');
            });
        }

        function displayData(data) {
            document.getElementById('client').value = data.client || '';
            document.getElementById('montant').value = data.montant || '';
            document.getElementById('date').value = data.date || '';
            document.getElementById('reference').value = data.reference || '';
            dataPreview.style.display = 'block';
        }

        function saveToCRM() {
            // Logique de sauvegarde dans le CRM
            alert('Données sauvegardées dans le CRM !');
        }
    </script>
</body>
</html>

5. Intégration CRM

Pour l'intégration avec le CRM, j'ai créé un module d'export :

import pandas as pd
import requests
import json

class CRMExporter:
    def __init__(self, crm_api_url, api_key):
        self.crm_api_url = crm_api_url
        self.api_key = api_key
        self.headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        }
    
    def export_to_crm(self, data):
        """Export des données vers le CRM"""
        crm_data = {
            'client_name': data.get('client', ''),
            'amount': data.get('montant', 0),
            'date': data.get('date', ''),
            'reference': data.get('reference', ''),
            'status': 'pending_validation'
        }
        
        try:
            response = requests.post(
                f"{self.crm_api_url}/proposals",
                headers=self.headers,
                json=crm_data
            )
            
            if response.status_code == 201:
                return {
                    'success': True,
                    'crm_id': response.json().get('id'),
                    'message': 'Propal ajoutée au CRM avec succès'
                }
            else:
                return {
                    'success': False,
                    'error': f'Erreur CRM: {response.status_code}'
                }
        except Exception as e:
            return {
                'success': False,
                'error': f'Erreur de connexion: {str(e)}'
            }
    
    def export_to_excel(self, data_list, filename):
        """Export vers Excel pour validation manuelle"""
        df = pd.DataFrame(data_list)
        df.to_excel(filename, index=False)
        return f'Données exportées vers {filename}'

Résultats obtenus

La solution AutoProposal V2 a transformé le processus de saisie :

Évolutions V2

La version 2 apporte des améliorations significatives :

💡 Prochaine étape : Intégration d'IA pour améliorer la reconnaissance des formats non standard et la validation automatique des données.

Points techniques importants

  1. Gestion des erreurs : Fallback OCR si extraction PDF échoue
  2. Performance : Traitement asynchrone pour les gros volumes
  3. Sécurité : Validation des fichiers uploadés
  4. Scalabilité : Architecture modulaire pour faciliter les évolutions

🚀 Besoin d'automatiser vos processus PDF ?

Chaque entreprise a ses spécificités. Contactez-moi pour une solution d'extraction PDF sur-mesure adaptée à vos besoins.

Discuter de votre projet