Intégration API TTN / El Fatoora avec votre ERP : Guide complet Odoo, SAP, WooCommerce et systèmes personnalisés

Noqta Team
Par Noqta Team ·

Chargement du lecteur de synthèse vocale...

La facturation électronique en Tunisie n'est plus optionnelle. Avec l'extension aux prestataires de services (LdF 2026) et les sanctions pouvant atteindre 50 000 DT par exercice, l'intégration de votre système de gestion avec l'API TTN / El Fatoora est devenue une priorité opérationnelle.

Ce guide couvre les différents scénarios d'intégration — que vous utilisiez Odoo, SAP, un ERP personnalisé ou une plateforme e-commerce — avec des architectures détaillées, des exemples de code et les pièges à éviter.

Vue d'ensemble de l'API TTN

Architecture générale

L'intégration avec TTN suit un flux en 5 étapes :

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Votre ERP   │────▶│  Middleware   │────▶│  Signature   │
│  (facturation)│     │  (validation  │     │  TUNTRUST    │
│              │     │   + TEIF)     │     │  (ANCE)      │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                  │
                     ┌──────────────┐     ┌───────▼───────┐
                     │  Réponse     │◀────│  API TTN      │
                     │  (statut,    │     │  El Fatoora   │
                     │   PDF, QR)   │     │               │
                     └──────────────┘     └───────────────┘

Endpoints principaux

EndpointMéthodeDescription
/api/v1/invoice/submitPOSTSoumettre une facture TEIF signée
/api/v1/invoice/{id}/statusGETVérifier le statut d'une facture
/api/v1/invoice/{id}/pdfGETRécupérer le PDF officiel (avec QR)
/api/v1/company/registerPOSTInscrire une entreprise
/api/v1/invoice/bulkPOSTSoumission en lot

Authentification

L'API TTN utilise une authentification par certificat client (mutual TLS) :

// Exemple d'authentification avec certificat TUNTRUST
import https from 'https';
import fs from 'fs';
 
const agent = new https.Agent({
  cert: fs.readFileSync('/path/to/tuntrust-cert.pem'),
  key: fs.readFileSync('/path/to/private-key.pem'),
  ca: fs.readFileSync('/path/to/ance-ca.pem'),
  rejectUnauthorized: true
});
 
const response = await fetch('https://api.elfatoora.digital/api/v1/invoice/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/xml' },
  body: teifXml,
  agent
});

Format TEIF (rappel)

Chaque facture doit être au format TEIF — un XML structuré avec des champs obligatoires :

<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
  <InvoiceHeader>
    <InvoiceNumber>FAC-2026-001234</InvoiceNumber>
    <IssueDate>2026-03-31</IssueDate>
    <InvoiceTypeCode>380</InvoiceTypeCode>
    <CurrencyCode>TND</CurrencyCode>
  </InvoiceHeader>
  <SellerParty>
    <TaxID>1234567A/M/000</TaxID>
    <Name>Votre Entreprise SARL</Name>
    <!-- ... -->
  </SellerParty>
  <BuyerParty>
    <TaxID>7654321B/M/000</TaxID>
    <Name>Client SA</Name>
  </BuyerParty>
  <InvoiceLines>
    <Line>
      <Description>Service de développement</Description>
      <Quantity>1</Quantity>
      <UnitPrice>5000.000</UnitPrice>
      <TaxRate>19</TaxRate>
      <LineTotal>5950.000</LineTotal>
    </Line>
  </InvoiceLines>
  <TaxTotal>950.000</TaxTotal>
  <InvoiceTotal>5950.000</InvoiceTotal>
  <DigitalSignature><!-- Signature TUNTRUST --></DigitalSignature>
  <QRCode><!-- Données QR code --></QRCode>
</Invoice>

Scénario 1 : Intégration Odoo

Odoo est l'ERP le plus déployé en Tunisie pour les PME. Voici l'architecture recommandée :

Architecture

┌─────────────────────────────────────────────────────────┐
│  Odoo (15/16/17)                                        │
│  ┌────────────┐  ┌────────────┐  ┌──────────────────┐  │
│  │ Module      │  │ Facturation │  │ Module El Fatoora│  │
│  │ Comptabilité│─▶│ PDF standard│─▶│ (personnalisé)   │  │
│  └────────────┘  └────────────┘  └────────┬─────────┘  │
│                                            │            │
└────────────────────────────────────────────┼────────────┘
                                             │
                                   ┌─────────▼──────────┐
                                   │  Middleware Noqta   │
                                   │  - Validation TEIF  │
                                   │  - Signature        │
                                   │  - Transmission TTN │
                                   └─────────┬──────────┘
                                             │
                                   ┌─────────▼──────────┐
                                   │  API TTN El Fatoora │
                                   └────────────────────┘

Module Odoo personnalisé

# odoo_elfatoora/models/account_move.py
from odoo import models, fields, api
import requests
import xmltodict
 
class AccountMove(models.Model):
    _inherit = 'account.move'
 
    elfatoora_status = fields.Selection([
        ('draft', 'Brouillon'),
        ('submitted', 'Soumise'),
        ('accepted', 'Acceptée'),
        ('rejected', 'Rejetée'),
    ], string='Statut El Fatoora', default='draft')
    elfatoora_id = fields.Char('ID El Fatoora')
    elfatoora_qr = fields.Binary('QR Code')
 
    def action_submit_elfatoora(self):
        """Soumettre la facture à El Fatoora via le middleware"""
        self.ensure_one()
 
        # Générer le XML TEIF
        teif_data = self._generate_teif_xml()
 
        # Envoyer au middleware
        response = requests.post(
            'https://middleware.noqta.tn/api/v1/invoice/submit',
            json={
                'invoice_data': teif_data,
                'company_tax_id': self.company_id.vat,
                'odoo_invoice_id': self.id,
            },
            headers={'Authorization': f'Bearer {self.env["ir.config_parameter"].get_param("elfatoora.api_key")}'}
        )
 
        if response.status_code == 200:
            result = response.json()
            self.write({
                'elfatoora_status': 'submitted',
                'elfatoora_id': result['invoice_id'],
            })
        else:
            raise UserError(f"Erreur El Fatoora: {response.text}")
 
    def _generate_teif_xml(self):
        """Convertir la facture Odoo en format TEIF"""
        lines = []
        for line in self.invoice_line_ids:
            lines.append({
                'Description': line.name,
                'Quantity': str(line.quantity),
                'UnitPrice': f'{line.price_unit:.3f}',
                'TaxRate': str(int(line.tax_ids[0].amount)) if line.tax_ids else '0',
                'LineTotal': f'{line.price_total:.3f}',
            })
 
        return {
            'InvoiceNumber': self.name,
            'IssueDate': str(self.invoice_date),
            'SellerTaxID': self.company_id.vat,
            'BuyerTaxID': self.partner_id.vat or '',
            'Lines': lines,
            'TaxTotal': f'{self.amount_tax:.3f}',
            'InvoiceTotal': f'{self.amount_total:.3f}',
        }

Pièges courants avec Odoo

  1. Format des montants : TTN exige 3 décimales pour les dinars (5000.000, pas 5000.00)
  2. Identifiant fiscal : Doit être au format exact XXXXXXXXA/M/NNN
  3. Séquences : Les numéros de facture Odoo doivent être continus (pas de trous)
  4. Notes de crédit : Doivent référencer la facture originale via elfatoora_id
  5. Multi-devises : TEIF n'accepte que TND. Les factures en EUR/USD doivent être converties

Scénario 2 : SAP, Sage et autres ERP

Architecture middleware

Pour les ERP propriétaires (SAP, Sage, Microsoft Dynamics), un middleware dédié est recommandé :

┌───────────┐   ┌───────────┐   ┌───────────┐
│  SAP B1   │   │  Sage X3  │   │  Dynamics │
│           │   │           │   │  365      │
└─────┬─────┘   └─────┬─────┘   └─────┬─────┘
      │               │               │
      └───────────────┼───────────────┘
                      │
            ┌─────────▼──────────┐
            │  API Middleware     │
            │  Noqta El Fatoora  │
            │  ─────────────────│
            │  • Mapping ERP→TEIF│
            │  • Validation      │
            │  • Signature       │
            │  • File d'attente  │
            │  • Retry & logs    │
            └─────────┬──────────┘
                      │
            ┌─────────▼──────────┐
            │  API TTN El Fatoora│
            └────────────────────┘

SAP : Integration via RFC/BAPI

// Middleware: SAP → TEIF mapping
function mapSAPInvoiceToTEIF(sapInvoice) {
  return {
    InvoiceHeader: {
      InvoiceNumber: sapInvoice.BELNR,
      IssueDate: formatSAPDate(sapInvoice.BLDAT),
      CurrencyCode: 'TND',
    },
    SellerParty: {
      TaxID: sapInvoice.STCD1, // Matricule fiscal
      Name: sapInvoice.BUTXT,
    },
    BuyerParty: {
      TaxID: sapInvoice.KUNNR_STCD1,
      Name: sapInvoice.NAME1,
    },
    Lines: sapInvoice.ITEMS.map(item => ({
      Description: item.ARKTX,
      Quantity: item.MENGE,
      UnitPrice: item.NETPR,
      TaxRate: getTVARate(item.MWSKZ),
      LineTotal: item.NETWR,
    })),
    TaxTotal: sapInvoice.MWSBK,
    InvoiceTotal: sapInvoice.WRBTR,
  };
}

Sage : Export CSV → Middleware

# Middleware Python pour Sage
import csv
from datetime import datetime
 
def process_sage_export(csv_path: str) -> list[dict]:
    """Convertir l'export CSV Sage en données TEIF"""
    invoices = {}
 
    with open(csv_path) as f:
        reader = csv.DictReader(f, delimiter=';')
        for row in reader:
            inv_num = row['NumFacture']
            if inv_num not in invoices:
                invoices[inv_num] = {
                    'InvoiceNumber': inv_num,
                    'IssueDate': datetime.strptime(row['Date'], '%d/%m/%Y').isoformat()[:10],
                    'SellerTaxID': row['MatriculeFiscal'],
                    'BuyerTaxID': row['MatriculeFiscalClient'],
                    'Lines': [],
                    'TaxTotal': 0,
                    'InvoiceTotal': 0,
                }
            invoices[inv_num]['Lines'].append({
                'Description': row['Designation'],
                'Quantity': row['Quantite'],
                'UnitPrice': f"{float(row['PrixUnitaire']):.3f}",
                'TaxRate': row['TauxTVA'],
                'LineTotal': f"{float(row['MontantTTC']):.3f}",
            })
 
    return list(invoices.values())

Scénario 3 : Système personnalisé (API directe)

Pour les systèmes développés sur mesure, voici un client API complet en TypeScript :

// ttn-client.ts — Client API TTN El Fatoora
import { Agent } from 'https';
import { readFileSync } from 'fs';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
 
interface TTNConfig {
  baseUrl: string;     // https://api.elfatoora.digital
  certPath: string;    // Certificat TUNTRUST
  keyPath: string;     // Clé privée
  caPath: string;      // CA ANCE
  companyTaxId: string;
}
 
interface InvoiceLine {
  description: string;
  quantity: number;
  unitPrice: number;
  taxRate: number;
}
 
interface InvoiceData {
  number: string;
  date: string;
  buyerTaxId: string;
  buyerName: string;
  lines: InvoiceLine[];
}
 
class TTNClient {
  private agent: Agent;
  private config: TTNConfig;
  private xmlBuilder: XMLBuilder;
  private xmlParser: XMLParser;
 
  constructor(config: TTNConfig) {
    this.config = config;
    this.agent = new Agent({
      cert: readFileSync(config.certPath),
      key: readFileSync(config.keyPath),
      ca: readFileSync(config.caPath),
    });
    this.xmlBuilder = new XMLBuilder({ ignoreAttributes: false });
    this.xmlParser = new XMLParser({ ignoreAttributes: false });
  }
 
  async submitInvoice(invoice: InvoiceData): Promise<{
    id: string;
    status: string;
    qrCode: string;
  }> {
    const teifXml = this.buildTEIF(invoice);
 
    const response = await fetch(`${this.config.baseUrl}/api/v1/invoice/submit`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/xml' },
      body: teifXml,
      // @ts-ignore
      agent: this.agent,
    });
 
    if (!response.ok) {
      const error = await response.text();
      throw new Error(`TTN API error (${response.status}): ${error}`);
    }
 
    return response.json();
  }
 
  async getStatus(invoiceId: string): Promise<string> {
    const response = await fetch(
      `${this.config.baseUrl}/api/v1/invoice/${invoiceId}/status`,
      { agent: this.agent as any }
    );
    const data = await response.json();
    return data.status;
  }
 
  async getPDF(invoiceId: string): Promise<Buffer> {
    const response = await fetch(
      `${this.config.baseUrl}/api/v1/invoice/${invoiceId}/pdf`,
      { agent: this.agent as any }
    );
    return Buffer.from(await response.arrayBuffer());
  }
 
  private buildTEIF(invoice: InvoiceData): string {
    const taxTotal = invoice.lines.reduce(
      (sum, l) => sum + l.quantity * l.unitPrice * l.taxRate / 100, 0
    );
    const invoiceTotal = invoice.lines.reduce(
      (sum, l) => sum + l.quantity * l.unitPrice * (1 + l.taxRate / 100), 0
    );
 
    return this.xmlBuilder.build({
      Invoice: {
        InvoiceHeader: {
          InvoiceNumber: invoice.number,
          IssueDate: invoice.date,
          InvoiceTypeCode: '380',
          CurrencyCode: 'TND',
        },
        SellerParty: {
          TaxID: this.config.companyTaxId,
        },
        BuyerParty: {
          TaxID: invoice.buyerTaxId,
          Name: invoice.buyerName,
        },
        InvoiceLines: {
          Line: invoice.lines.map(l => ({
            Description: l.description,
            Quantity: l.quantity.toString(),
            UnitPrice: l.unitPrice.toFixed(3),
            TaxRate: l.taxRate.toString(),
            LineTotal: (l.quantity * l.unitPrice * (1 + l.taxRate / 100)).toFixed(3),
          })),
        },
        TaxTotal: taxTotal.toFixed(3),
        InvoiceTotal: invoiceTotal.toFixed(3),
      },
    });
  }
}
 
// Utilisation
const ttn = new TTNClient({
  baseUrl: 'https://api.elfatoora.digital',
  certPath: './certs/tuntrust.pem',
  keyPath: './certs/private.pem',
  caPath: './certs/ance-ca.pem',
  companyTaxId: '1234567A/M/000',
});
 
const result = await ttn.submitInvoice({
  number: 'FAC-2026-001234',
  date: '2026-03-31',
  buyerTaxId: '7654321B/M/000',
  buyerName: 'Client SA',
  lines: [
    { description: 'Développement web', quantity: 1, unitPrice: 5000, taxRate: 19 },
    { description: 'Hébergement annuel', quantity: 1, unitPrice: 1200, taxRate: 19 },
  ],
});
 
console.log(`Facture soumise: ${result.id}, QR: ${result.qrCode}`);

Scénario 4 : E-commerce (WooCommerce, PrestaShop)

Architecture e-commerce

┌───────────────────────────────────────────┐
│  Boutique en ligne                        │
│  ┌─────────────┐   ┌──────────────────┐  │
│  │ WooCommerce  │   │ PrestaShop /     │  │
│  │ / Custom     │   │ Magento          │  │
│  └──────┬──────┘   └──────┬───────────┘  │
│         │                  │              │
│  ┌──────▼──────────────────▼──────────┐  │
│  │  Plugin/Module El Fatoora          │  │
│  │  - Webhook sur commande validée    │  │
│  │  - Extraction données facture      │  │
│  │  - Envoi au middleware             │  │
│  └────────────────┬───────────────────┘  │
└───────────────────┼──────────────────────┘
                    │
          ┌─────────▼──────────┐
          │  Middleware Noqta   │
          │  El Fatoora         │
          └─────────┬──────────┘
                    │
          ┌─────────▼──────────┐
          │  API TTN            │
          └────────────────────┘

Plugin WooCommerce

<?php
/**
 * Plugin Name: Noqta El Fatoora pour WooCommerce
 * Description: Intégration facturation électronique TTN
 */
 
add_action('woocommerce_order_status_completed', 'noqta_submit_elfatoora');
 
function noqta_submit_elfatoora($order_id) {
    $order = wc_get_order($order_id);
 
    // Vérifier que c'est un client tunisien (B2B)
    $tax_id = get_post_meta($order_id, '_billing_tax_id', true);
    if (empty($tax_id)) return;
 
    $lines = [];
    foreach ($order->get_items() as $item) {
        $lines[] = [
            'description' => $item->get_name(),
            'quantity'    => $item->get_quantity(),
            'unit_price'  => $item->get_total() / $item->get_quantity(),
            'tax_rate'    => 19, // TVA tunisienne standard
        ];
    }
 
    $payload = [
        'invoice_number' => 'WC-' . $order->get_order_number(),
        'date'           => $order->get_date_completed()->format('Y-m-d'),
        'buyer_tax_id'   => $tax_id,
        'buyer_name'     => $order->get_billing_company() ?: $order->get_billing_first_name(),
        'lines'          => $lines,
    ];
 
    $response = wp_remote_post('https://middleware.noqta.tn/api/v1/invoice/submit', [
        'body'    => json_encode($payload),
        'headers' => [
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . get_option('noqta_elfatoora_api_key'),
        ],
    ]);
 
    if (!is_wp_error($response)) {
        $body = json_decode(wp_remote_retrieve_body($response), true);
        update_post_meta($order_id, '_elfatoora_id', $body['invoice_id']);
        update_post_meta($order_id, '_elfatoora_status', 'submitted');
    }
}

Pièges courants (tous scénarios)

1. Format des montants

TTN exige exactement 3 décimales pour tous les montants en TND. 5000.00 sera rejeté, 5000.000 sera accepté.

2. Matricule fiscal

Le format exact est XXXXXXXXA/M/NNN. Un espace ou un tiret en trop causera un rejet.

3. Certificat TUNTRUST

  • Le certificat expire après 2 ans — planifiez le renouvellement
  • En environnement de test (sandbox), utilisez le certificat de test fourni par TTN
  • Ne stockez jamais la clé privée dans le code source

4. Gestion des erreurs

L'API TTN peut retourner des erreurs temporaires. Implémentez un système de retry avec backoff exponentiel :

async function submitWithRetry(invoice: InvoiceData, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await ttn.submitInvoice(invoice);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = Math.pow(2, attempt) * 1000;
      console.log(`Tentative ${attempt} échouée, retry dans ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

5. Continuité des numéros

TTN vérifie la séquentialité des numéros de facture. Un trou dans la séquence déclenche une alerte.

6. Notes de crédit

Les avoirs doivent référencer la facture originale par son ID El Fatoora (pas votre numéro interne).

Noqta : Votre partenaire intégration TTN

Nous avons intégré l'API TTN dans plus de 20 systèmes différents. Notre offre :

Middleware El Fatoora as-a-Service

  • API REST unifiée pour tous vos ERP
  • Validation TEIF automatique
  • Signature électronique intégrée
  • File d'attente et retry automatique
  • Dashboard de suivi en temps réel
  • Support multi-entreprises

Modules ERP clé en main

  • Module Odoo El Fatoora (Community & Enterprise)
  • Connecteur SAP Business One
  • Plugin WooCommerce / PrestaShop
  • API pour systèmes personnalisés

Accompagnement

  • Audit de votre système actuel
  • Conception de l'architecture d'intégration
  • Développement et déploiement
  • Formation des équipes
  • Support post-déploiement

📩 Contactez-nous pour un devis gratuit d'intégration TTN.

📞 Appelez-nous : +216 XX XXX XXX — Consultation gratuite de 30 minutes.


FAQ

Combien de temps prend l'intégration TTN ?

Pour un ERP standard (Odoo, SAP) avec notre middleware : 2 à 4 semaines. Pour un système personnalisé : 4 à 8 semaines selon la complexité.

Quel est le coût du certificat TUNTRUST ?

Le certificat de signature électronique coûte entre 200 et 500 DT selon le prestataire accrédité (TUNTRUST, ANCE). Validité 2 ans.

Puis-je tester avant de passer en production ?

Oui. TTN fournit un environnement sandbox. Nous configurons votre intégration en sandbox d'abord, puis basculons en production après validation.

El Fatoora accepte-t-elle les factures en devises étrangères ?

Le format TEIF est en TND. Les factures en EUR/USD doivent inclure le montant converti en TND au taux du jour.

Quelle est la différence entre le mode Web et le mode EDI ?

  • Mode Web : Saisie manuelle sur elfatoora.digital — adapté aux faibles volumes
  • Mode EDI (API) : Intégration automatisée — indispensable dès 50+ factures/mois

Articles connexes :


Vous voulez lire plus d'articles de blog? Découvrez notre dernier article sur Claude Code vs Cursor vs Windsurf : Quel outil de codage IA vaut votre argent en 2026 ?.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.