4 Commits

Author SHA1 Message Date
Aurelien
8092ca05e6 chore : ajout de test pur le parser de pdf #5 2026-06-28 17:54:45 +02:00
Aurelien
170cb74650 chore : modification de la fonction pour pouvoir parse les facture metro #5 2026-06-26 10:24:24 +02:00
Aurelien
bdff1f9028 chore : correction de la fonction #5 2026-06-24 00:09:16 +02:00
Aurelien
e597b4d161 chore : correction de la fonction #5 2026-06-24 00:04:08 +02:00
6 changed files with 4232 additions and 92 deletions

11
jest.config.js Normal file
View File

@@ -0,0 +1,11 @@
const { createDefaultPreset } = require("ts-jest");
const tsJestTransformCfg = createDefaultPreset().transform;
/** @type {import("jest").Config} **/
module.exports = {
testEnvironment: "node",
transform: {
...tsJestTransformCfg,
},
};

4120
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"type": "commonjs", "type": "commonjs",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "jest",
"server": "nodemon ./src/server.ts" "server": "nodemon ./src/server.ts"
}, },
"dependencies": { "dependencies": {
@@ -22,8 +22,11 @@
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^25.9.3", "@types/node": "^25.9.3",
"jest": "^30.4.2",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }

View File

@@ -2,127 +2,137 @@ import * as fs from 'fs';
const pdfjsLib = require('pdfjs-dist'); const pdfjsLib = require('pdfjs-dist');
async function pdfParse(travel: string) { async function pdf_parse(travel: string) {
interface DetailProduit { interface product_details {
quantité: number; quantity: number;
prixHT: number; price_ht: number;
TVA: string; tva: string;
} }
interface res { interface res {
marque: string; brand: string;
date_achats: string; purchase_date: string;
products: { products: {
[nomProduit: string]: DetailProduit; [product_name: string]: product_details;
}; };
} }
const productsList: { [key: string]: DetailProduit } = {}; const product_list: { [key: string]: product_details } = {};
let fournisseur = ''; let supplier = '';
let date = ''; let date = '';
const cheminPDF = travel; const pdf_path = travel;
try { try {
// Lecture du fichier binaire du PDF et conversion pour PDF.js // Lecture du fichier binaire du PDF et conversion pour PDF.js
const dataBuffer = new Uint8Array(fs.readFileSync(cheminPDF)); const data_buffer = new Uint8Array(fs.readFileSync(pdf_path));
// Chargement du document par PDF.js // Chargement du document par PDF.js
const loadingTask = pdfjsLib.getDocument({ data: dataBuffer }); const loading_task = pdfjsLib.getDocument({ data: data_buffer });
const pdf = await loadingTask.promise; const pdf = await loading_task.promise;
let texte = ''; let text = '';
// Boucle pour extraire le texte de chaque page du PDF // Boucle pour extraire le text de chaque page du PDF
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i); const page = await pdf.getPage(i);
const textContent = await page.getTextContent(); const text_content = await page.getTextContent();
// On récupère chaque fragment de texte trouvé graphiquement sur la page et on les sépare par un saut de ligne // On récupère chaque fragment de text trouvé graphiquement sur la page et on les sépare par un saut de ligne
const textePage = textContent.items.map((item: any) => item.str).join('\n'); const text_page = text_content.items.map((item: any) => item.str).join('\n');
texte += textePage + '\n'; text += text_page + '\n';
} }
const regexAuchan = /auchan/i.test(texte); const regex_auchan = /auchan/i.test(text);
// on regarde si c'est auchan ou metro // on regarde si c'est auchan ou metro
if (regexAuchan) { if (regex_auchan) {
fournisseur = "Auchan"; supplier = "Auchan";
// on regarde la date d'achat avec une expression reguliere pour auchan // on regarde la date d'achat avec une expression reguliere pour auchan
const regexDate = /(\d{2})\/(\d{2})\/(\d{4})/; const regex_date = /(\d{2})\/(\d{2})\/(\d{4})/;
const correspondance = texte.match(regexDate); const corres = text.match(regex_date);
if (correspondance) { if (corres) {
date = correspondance[0] date = corres[0]
} else { } else {
date = "non trouvé" date = "non trouvé"
} }
// Découpage en lignes pour l'analyse // Découpage en lignes pour l'analyse et cration d'un tableau de toutes les lignes
const lignes = texte.split('\n').map(l => l.trim()).filter(l => l.length > 0); const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
// Algorithme de recherche verticale pour Auchan // Algorithme de recherche verticale pour Auchan
for (let i = 0; i < lignes.length; i++) { for (let i = 0; i < lines.length; i++) {
// Si la ligne correspond exactement à un code-barres à 13 chiffres // Si la ligne correspond exactement à un code-barres à 13 chiffres
if (/^\d{13}$/.test(lignes[i])) { if (/^\d{13}$/.test(lines[i])) {
const nomProduit = lignes[i + 1]; const product_name = lines[i + 1];
const qte = lines[i + 3];
const price = lines[i + 4];
const tva = lines[i + 5];
let idxQuantite = i + 3; product_list[product_name] = {
let idxPrixHT = i + 4; quantity: parseInt(qte, 10),
let idxTva = i + 5; price_ht: parseFloat(price.replace(',', '.')),
tva: tva.replace(',', '.').trim() + "%"
// Gestion de la ligne parasite Eco-participation qui décale le tableau
if (lignes[i + 2] && lignes[i + 2].includes("Eco-participation")) {
idxQuantite += 2;
idxPrixHT += 2;
idxTva += 2;
}
const qte = lignes[idxQuantite];
const prix = lignes[idxPrixHT];
const tva = lignes[idxTva];
let tvaFinal: string;
if (tva) {
tvaFinal = tva.replace(',', '.').trim() + "%";
} else {
tvaFinal = "non trouvé";
}
productsList[nomProduit] = {
quantité: parseInt(qte, 10),
prixHT: parseFloat(prix.replace(',', '.')),
TVA: tvaFinal
}; };
} }
} }
} else { } else {
fournisseur = "Metro"; supplier = "METRO";
// on regarde la date d'achat avec une expression reguliere pour metro // on regarde la date d'achat avec une expression reguliere pour metro
const regexDate = /(\d{2})-(\d{2})-(\d{4})/; const regex_date = /(\d{2})-(\d{2})-(\d{4})/;
const correspondance = texte.match(regexDate); const corres = text.match(regex_date);
if (correspondance) { if (corres) {
date = correspondance[0] date = corres[0];
} else { } else {
date = "non trouvé" date = "non trouvé";
}
// Découpage en lignes pour l'analyse et cration d'un tableau de toutes les lignes
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
// Algorithme de recherche verticale pour metro
for (let i = 0; i < lines.length; i++) {
// Si la ligne correspond exactement à un code-barres à 13 chiffres
if (/^\d{13}$/.test(lines[i])) {
const product_name = lines[i + 1];
const qte = lines[i + 4];
const price = lines[i + 5];
const tva = lines[i + 6];
let tva_final = "";
if (tva == "B"){
tva_final = "5.5%";
}
else {
tva_final = "20%";
} }
product_list[product_name] = {
quantity: parseInt(qte, 10),
price_ht: parseFloat(price.replace(',', '.')),
tva: tva_final
};
}
}
} }
// Construction de l'objet final // Construction de l'objet final
const resultat: res = { const result: res = {
marque: fournisseur, brand: supplier,
date_achats: date, purchase_date: date,
products: productsList products: product_list
}; };
console.log(resultat); console.log(result);
return result;
} catch (e) { } catch (e) {
console.error("Erreur :", e); console.error("Erreur :", e);
@@ -130,4 +140,4 @@ async function pdfParse(travel: string) {
} }
export default pdfParse; export default pdf_parse;

25
tests/test_parse.test.ts Normal file
View File

@@ -0,0 +1,25 @@
import pdf_parse from "../src/modules/pdf-parse";
// utilisation de jest pour faire les tests
describe('Test du parser de factures (pdf_parse)', () => {
it('doit correctement parser la facture Metro et extraire les produits', async () => {
// test pour les fichier metro
const cheminPDF = "00(014)0057010856.pdf";
const resultat = await pdf_parse(cheminPDF);
expect(resultat!.brand).toBe("METRO");
expect(resultat!.purchase_date).toBe("22-04-2026");
expect(resultat!.products).toHaveProperty("2015675 SCHWEPPES POMME SLIM 33CL");
});
it('doit correctement parser une facture Auchan', async () => {
// test pour les fichier auchan
const cheminPDF = "2026-02-25 _ Facture AUCHAN _ Soupe.pdf";
const resultat = await pdf_parse(cheminPDF);
expect(resultat!.brand).toBe("Auchan");
expect(resultat!.purchase_date).toBe("25/02/2026");
expect(resultat!.products).toHaveProperty("PURSOUP VELOUTE LEGUMES SOLEIL");
});
});

View File

@@ -7,7 +7,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true,
"types": ["jest", "node"]
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]