Initial commit - Facturation SANTINOVA
This commit is contained in:
30
backend/src/services/auditService.ts
Normal file
30
backend/src/services/auditService.ts
Normal 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;
|
||||
}
|
||||
224
backend/src/services/ocrService.ts
Normal file
224
backend/src/services/ocrService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user