Compare commits
7 Commits
3f42d80768
...
features/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8092ca05e6 | ||
|
|
170cb74650 | ||
|
|
bdff1f9028 | ||
|
|
e597b4d161 | ||
|
|
ef2d1ca3b2 | ||
|
|
00949c60c5 | ||
|
|
2ca6dc31a9 |
11
jest.config.js
Normal file
11
jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { createDefaultPreset } = require("ts-jest");
|
||||||
|
|
||||||
|
const tsJestTransformCfg = createDefaultPreset().transform;
|
||||||
|
|
||||||
|
/** @type {import("jest").Config} **/
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
...tsJestTransformCfg,
|
||||||
|
},
|
||||||
|
};
|
||||||
4762
package-lock.json
generated
4762
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,17 +11,22 @@
|
|||||||
"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": {
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1"
|
"express": "^5.2.1",
|
||||||
|
"pdf-parse-fork": "^1.2.0",
|
||||||
|
"pdfjs-dist": "^3.11.174"
|
||||||
},
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/modules/pdf-parse.ts
Normal file
143
src/modules/pdf-parse.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
const pdfjsLib = require('pdfjs-dist');
|
||||||
|
|
||||||
|
|
||||||
|
async function pdf_parse(travel: string) {
|
||||||
|
interface product_details {
|
||||||
|
quantity: number;
|
||||||
|
price_ht: number;
|
||||||
|
tva: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface res {
|
||||||
|
brand: string;
|
||||||
|
purchase_date: string;
|
||||||
|
products: {
|
||||||
|
[product_name: string]: product_details;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const product_list: { [key: string]: product_details } = {};
|
||||||
|
let supplier = '';
|
||||||
|
let date = '';
|
||||||
|
|
||||||
|
const pdf_path = travel;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Lecture du fichier binaire du PDF et conversion pour PDF.js
|
||||||
|
const data_buffer = new Uint8Array(fs.readFileSync(pdf_path));
|
||||||
|
|
||||||
|
// Chargement du document par PDF.js
|
||||||
|
const loading_task = pdfjsLib.getDocument({ data: data_buffer });
|
||||||
|
const pdf = await loading_task.promise;
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
// Boucle pour extraire le text de chaque page du PDF
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const text_content = await page.getTextContent();
|
||||||
|
|
||||||
|
// On récupère chaque fragment de text trouvé graphiquement sur la page et on les sépare par un saut de ligne
|
||||||
|
const text_page = text_content.items.map((item: any) => item.str).join('\n');
|
||||||
|
text += text_page + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex_auchan = /auchan/i.test(text);
|
||||||
|
|
||||||
|
// on regarde si c'est auchan ou metro
|
||||||
|
|
||||||
|
if (regex_auchan) {
|
||||||
|
supplier = "Auchan";
|
||||||
|
|
||||||
|
// on regarde la date d'achat avec une expression reguliere pour auchan
|
||||||
|
const regex_date = /(\d{2})\/(\d{2})\/(\d{4})/;
|
||||||
|
const corres = text.match(regex_date);
|
||||||
|
if (corres) {
|
||||||
|
date = corres[0]
|
||||||
|
} else {
|
||||||
|
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 Auchan
|
||||||
|
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 + 3];
|
||||||
|
const price = lines[i + 4];
|
||||||
|
const tva = lines[i + 5];
|
||||||
|
|
||||||
|
product_list[product_name] = {
|
||||||
|
quantity: parseInt(qte, 10),
|
||||||
|
price_ht: parseFloat(price.replace(',', '.')),
|
||||||
|
tva: tva.replace(',', '.').trim() + "%"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
supplier = "METRO";
|
||||||
|
|
||||||
|
// on regarde la date d'achat avec une expression reguliere pour metro
|
||||||
|
const regex_date = /(\d{2})-(\d{2})-(\d{4})/;
|
||||||
|
const corres = text.match(regex_date);
|
||||||
|
if (corres) {
|
||||||
|
date = corres[0];
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
const result: res = {
|
||||||
|
brand: supplier,
|
||||||
|
purchase_date: date,
|
||||||
|
products: product_list
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(result);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Erreur :", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default pdf_parse;
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/ : accueil pour le public avec la visualisation des produits dans le stock et la date prévisionelle des prochaines courses
|
|
||||||
|
|
||||||
/login : page de connexion
|
|
||||||
|
|
||||||
/create-account : page pour que l'admin crée des comptes renseignant son niveau d'accès
|
|
||||||
|
|
||||||
/dashboard : page recensant les indicateur clé des ventes pour les vendeur / bureaux
|
|
||||||
|
|
||||||
/new-purchase : page d'import de facture ou pour la sasie manulle des achats
|
|
||||||
|
|
||||||
/product-report : page de déclaration de casse/perte
|
|
||||||
/product-report/new : page pour déclarer la casse/perte d'un produit
|
|
||||||
|
|
||||||
/stock : page de visualisation du stock virtuel
|
|
||||||
|
|
||||||
/shopping-list : gestion des listes de course (création, dupplication, suppression)
|
|
||||||
/shopping-list/${list_id} : page de gestion d'une liste de course
|
|
||||||
|
|
||||||
|
|
||||||
post /new-purchase : extraire les données d'une facture et la mettre dans la DB / mettre les produits saisie manullemment dans la DB
|
|
||||||
post /create-account : ajouter les informations de compte dans la DB
|
|
||||||
post /product-report/new : modifier les informations du stock dans la DB
|
|
||||||
|
|
||||||
/webhooks/sumup : Lorsque un client à payé modifie les informations du stock dans la DB
|
|
||||||
25
tests/test_parse.test.ts
Normal file
25
tests/test_parse.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user