Initial commit - Facturation SANTINOVA

This commit is contained in:
manus-admin
2026-04-23 04:49:21 -04:00
commit 6ab833945c
55 changed files with 12642 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
import mysql from 'mysql2/promise';
import { getPool } from '../config/database';
export async function logAction(
entityType: string,
entityId: number,
action: string,
userId?: number,
userName?: string,
details?: any
) {
try {
const pool = getPool();
await pool.execute(
`INSERT INTO audit_log (entity_type, entity_id, action, user_id, user_name, details) VALUES (?, ?, ?, ?, ?, ?)`,
[entityType, entityId, action, userId || null, userName || null, details ? JSON.stringify(details) : null]
);
} catch (error) {
console.error('Erreur lors de l\'enregistrement de l\'audit:', error);
}
}
export async function getAuditLog(entityType: string, entityId: number) {
const pool = getPool();
const [rows] = await pool.execute(
`SELECT * FROM audit_log WHERE entity_type = ? AND entity_id = ? ORDER BY created_at DESC`,
[entityType, entityId]
);
return rows;
}

View File

@@ -0,0 +1,224 @@
import OpenAI from 'openai';
import fs from 'fs';
import path from 'path';
import { getPool } from '../config/database';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
interface ExtractedInvoiceData {
invoiceNumber?: string;
supplierName?: string;
supplierSiret?: string;
supplierAddress?: string;
invoiceDate?: string;
dueDate?: string;
amountHT?: number;
amountTVA?: number;
amountTTC?: number;
tvaRate?: number;
currency?: string;
lines?: Array<{
description?: string;
quantity?: number;
unitPrice?: number;
amountHT?: number;
tvaRate?: number;
amountTVA?: number;
amountTTC?: number;
}>;
fullText?: string;
confidence?: number;
}
export async function extractInvoiceData(filePath: string, mimeType: string, invoiceId: number): Promise<ExtractedInvoiceData> {
try {
console.log(`🔍 OCR en cours pour la facture #${invoiceId}...`);
// Read file and convert to base64
const fileBuffer = fs.readFileSync(filePath);
const base64 = fileBuffer.toString('base64');
// Determine media type
let mediaType = 'image/jpeg';
if (mimeType === 'application/pdf') {
mediaType = 'application/pdf';
} else if (mimeType) {
mediaType = mimeType;
}
const dataUrl = `data:${mediaType};base64,${base64}`;
const prompt = `Tu es un expert en extraction de données de factures. Analyse cette facture et extrais les informations suivantes au format JSON strict.
IMPORTANT: Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant ou après.
{
"invoiceNumber": "numéro de facture",
"supplierName": "nom du fournisseur",
"supplierSiret": "numéro SIRET du fournisseur",
"supplierAddress": "adresse complète du fournisseur",
"invoiceDate": "date de facture au format YYYY-MM-DD",
"dueDate": "date d'échéance au format YYYY-MM-DD",
"amountHT": montant_HT_nombre,
"amountTVA": montant_TVA_nombre,
"amountTTC": montant_TTC_nombre,
"tvaRate": taux_TVA_nombre,
"currency": "EUR",
"lines": [
{
"description": "description de la ligne",
"quantity": quantité_nombre,
"unitPrice": prix_unitaire_nombre,
"amountHT": montant_HT_ligne_nombre,
"tvaRate": taux_TVA_nombre,
"amountTVA": montant_TVA_ligne_nombre,
"amountTTC": montant_TTC_ligne_nombre
}
],
"fullText": "texte complet extrait de la facture",
"confidence": score_de_confiance_0_à_100
}
Si une information n'est pas trouvée, utilise null. Les montants doivent être des nombres (pas de chaînes). Les dates doivent être au format YYYY-MM-DD.`;
const response = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
{
type: 'image_url',
image_url: {
url: dataUrl,
detail: 'high',
},
},
],
},
],
max_tokens: 4096,
temperature: 0.1,
});
const content = response.choices[0]?.message?.content || '{}';
// Parse JSON response
let extracted: ExtractedInvoiceData;
try {
// Try to extract JSON from the response
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
extracted = JSON.parse(jsonMatch[0]);
} else {
extracted = JSON.parse(content);
}
} catch (parseError) {
console.error('Erreur parsing OCR JSON:', parseError);
extracted = { fullText: content, confidence: 0 };
}
// Update the invoice in the database
const pool = getPool();
// Try to find or create supplier
let supplierId: number | null = null;
if (extracted.supplierName) {
const [existingSuppliers]: any = await pool.execute(
'SELECT id FROM suppliers WHERE name LIKE ? OR siret = ? LIMIT 1',
[`%${extracted.supplierName}%`, extracted.supplierSiret || '']
);
if (existingSuppliers.length > 0) {
supplierId = existingSuppliers[0].id;
} else {
// Create new supplier
const [result]: any = await pool.execute(
'INSERT INTO suppliers (name, siret, address) VALUES (?, ?, ?)',
[extracted.supplierName, extracted.supplierSiret || null, extracted.supplierAddress || null]
);
supplierId = result.insertId;
}
}
await pool.execute(
`UPDATE invoices SET
invoice_number = COALESCE(?, invoice_number),
supplier_id = COALESCE(?, supplier_id),
supplier_name = COALESCE(?, supplier_name),
supplier_siret = COALESCE(?, supplier_siret),
supplier_address = COALESCE(?, supplier_address),
invoice_date = COALESCE(?, invoice_date),
due_date = COALESCE(?, due_date),
amount_ht = COALESCE(?, amount_ht),
amount_tva = COALESCE(?, amount_tva),
amount_ttc = COALESCE(?, amount_ttc),
tva_rate = COALESCE(?, tva_rate),
currency = COALESCE(?, currency),
ocr_raw_data = ?,
ocr_confidence = ?,
full_text = ?,
status = 'en_verification'
WHERE id = ?`,
[
extracted.invoiceNumber || null,
supplierId,
extracted.supplierName || null,
extracted.supplierSiret || null,
extracted.supplierAddress || null,
extracted.invoiceDate || null,
extracted.dueDate || null,
extracted.amountHT || null,
extracted.amountTVA || null,
extracted.amountTTC || null,
extracted.tvaRate || null,
extracted.currency || null,
JSON.stringify(extracted),
extracted.confidence || null,
extracted.fullText || null,
invoiceId,
]
);
// Insert invoice lines
if (extracted.lines && extracted.lines.length > 0) {
for (let idx = 0; idx < extracted.lines.length; idx++) {
const line = extracted.lines[idx];
await pool.execute(
`INSERT INTO invoice_lines (invoice_id, description, quantity, unit_price, amount_ht, tva_rate, amount_tva, amount_ttc, line_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[invoiceId, line.description || null, line.quantity || null, line.unitPrice || null,
line.amountHT || null, line.tvaRate || null, line.amountTVA || null, line.amountTTC || null, idx]
);
}
}
// Log the OCR action
await pool.execute(
`INSERT INTO audit_log (entity_type, entity_id, action, details) VALUES (?, ?, ?, ?)`,
['invoice', invoiceId, 'ocr_extraction', JSON.stringify({ confidence: extracted.confidence, linesCount: extracted.lines?.length || 0 })]
);
console.log(`✅ OCR terminé pour la facture #${invoiceId} (confiance: ${extracted.confidence}%)`);
return extracted;
} catch (error: any) {
console.error(`❌ Erreur OCR pour la facture #${invoiceId}:`, error);
// Update invoice with error status
try {
const pool = getPool();
await pool.execute(
`INSERT INTO audit_log (entity_type, entity_id, action, details) VALUES (?, ?, ?, ?)`,
['invoice', invoiceId, 'ocr_erreur', JSON.stringify({ error: error.message })]
);
} catch (logError) {
console.error('Erreur log OCR:', logError);
}
throw error;
}
}