8 Commits

Author SHA1 Message Date
Lazanimady Andrianirindrainy
a707d2785f feat: ajout de la saisie manuel pour purchase.routes 2026-06-24 14:28:25 +02:00
Lazanimady Andrianirindrainy
dbdebae45e update: purchase.routes.ts pour fonction avec pdf-parse.ts
on peut maintenant envoyer des fichier pdf sur un form et il renvoie un json avec les informations du pdf. dans /new-purchase
2026-06-23 23:46:52 +02:00
Lazanimady Andrianirindrainy
93a1aab86a refactor: Merge remote-tracking branch 'origin/features/pdf-parse' into core/routes 2026-06-23 22:52:55 +02:00
Lazanimady Andrianirindrainy
de3bcbaea2 corriger bug fonction pour prendre un fichier à partir d'un form fonctionel #1 2026-06-19 12:43:54 +02:00
Lazanimady Andrianirindrainy
b1ee077754 Merge branch 'features/pdf-parse' into core/routes tester les routes 2026-06-19 09:41:03 +02:00
Lazanimady Andrianirindrainy
d82ae9f313 1ère tentative purchase.route #1 2026-06-19 09:38:45 +02:00
Lazanimady Andrianirindrainy
27de44d895 modification liste_routes.txt #1 2026-06-19 09:37:37 +02:00
Lazanimady Andrianirindrainy
3f42d80768 1ère définition des routes dans un fichiers .txt #1 2026-06-18 17:17:22 +02:00
10 changed files with 280 additions and 4218 deletions

View File

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

4152
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,22 +11,21 @@
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "jest",
"test": "echo \"Error: no test specified\" && exit 1",
"server": "nodemon ./src/server.ts"
},
"dependencies": {
"dotenv": "^17.4.2",
"express": "^5.2.1",
"formidable": "^3.5.4",
"pdf-parse-fork": "^1.2.0",
"pdfjs-dist": "^3.11.174"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/formidable": "^3.5.1",
"@types/node": "^25.9.3",
"jest": "^30.4.2",
"nodemon": "^3.1.14",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
}

View File

@@ -2,142 +2,131 @@ 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;
async function pdfParse(travel: string) {
interface DetailProduit {
quantité: number;
prixHT: number;
TVA: string;
}
interface res {
brand: string;
purchase_date: string;
marque: string;
date_achats: string;
products: {
[product_name: string]: product_details;
[nomProduit: string]: DetailProduit;
};
}
const product_list: { [key: string]: product_details } = {};
let supplier = '';
const productsList: { [key: string]: DetailProduit } = {};
let fournisseur = '';
let date = '';
const pdf_path = travel;
const cheminPDF = travel;
try {
// Lecture du fichier binaire du PDF et conversion pour PDF.js
const data_buffer = new Uint8Array(fs.readFileSync(pdf_path));
const dataBuffer = new Uint8Array(fs.readFileSync(cheminPDF));
// Chargement du document par PDF.js
const loading_task = pdfjsLib.getDocument({ data: data_buffer });
const pdf = await loading_task.promise;
const loadingTask = pdfjsLib.getDocument({ data: dataBuffer });
const pdf = await loadingTask.promise;
let text = '';
let texte = '';
// Boucle pour extraire le text de chaque page du PDF
// Boucle pour extraire le texte 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();
const textContent = 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';
// On récupère chaque fragment de texte 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');
texte += textePage + '\n';
}
const regex_auchan = /auchan/i.test(text);
const regexAuchan = /auchan/i.test(texte);
// on regarde si c'est auchan ou metro
if (regex_auchan) {
supplier = "Auchan";
if (regexAuchan) {
fournisseur = "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]
const regexDate = /(\d{2})\/(\d{2})\/(\d{4})/;
const correspondance = texte.match(regexDate);
if (correspondance) {
date = correspondance[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);
// Découpage en lignes pour l'analyse
const lignes = texte.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++) {
for (let i = 0; i < lignes.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];
if (/^\d{13}$/.test(lignes[i])) {
const nomProduit = lignes[i + 1];
let idxQuantite = i + 3;
let idxPrixHT = i + 4;
let idxTva = i + 5;
// 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
};
product_list[product_name] = {
quantity: parseInt(qte, 10),
price_ht: parseFloat(price.replace(',', '.')),
tva: tva.replace(',', '.').trim() + "%"
};
}
}
} else {
supplier = "METRO";
fournisseur = "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];
const regexDate = /(\d{2})-(\d{2})-(\d{4})/;
const correspondance = texte.match(regexDate);
if (correspondance) {
date = correspondance[0]
} 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
const result: res = {
brand: supplier,
purchase_date: date,
products: product_list
const resultat: res = {
marque: fournisseur,
date_achats: date,
products: productsList
};
console.log(result);
return result;
return resultat;
} catch (e) {
console.error("Erreur :", e);
}
}
export default pdf_parse;
export default pdfParse;

View File

@@ -0,0 +1,24 @@
/ : 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
post /create-account : ajouter les informations de compte dans la DB
/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
post /new-purchase/file : extraire les données d'une facture et la mettre dans la DB
post /new-purchase/new : mettre les produits saisie manullemment dans la DB
/product-report : page de déclaration de casse/perte
/product-report/new : page pour déclarer la casse/perte d'un produit
post /product-report/new : modifier les informations du stock dans la DB
/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
/webhooks/sumup : Lorsque un client à payé modifie les informations du stock dans la DB

View File

@@ -0,0 +1,107 @@
import {Router, Request as ExpressRequest} from "express";
import formidable from "formidable";
import type { Files, File } from "formidable";
import pdf_parse from "../modules/pdf-parse";
// ----------------------------------------
// Router config
// ----------------------------------------
const router = Router();
// ----------------------------------------
// Routes
// ----------------------------------------
router.get("/", (req,res) => {
let html: string = `
<form action="/new-purchase/file" method="POST" enctype="multipart/form-data">
<input type="file" name="image" multiple>
<button type="submit">Envoyer</button>
</form>
<form action="/new-purchase/new" method="POST">
<label for="nom_produit">Nom du produit:</label>
<input type="text" name="nom_produit" >
</br>
<label for="quantite">Quantité:</label>
<input type="text" name="quantite" >
</br>
<label for="prixHT">Prix HT:</label>
<input type="text" name="prixHT" >
</br>
<label for="tva">TVA:</label>
<input type="text" name="tva" >
</br>
<button type="submit">Envoyer</button>
</form>
`;
res.send(html);
});
// gère la réception d'un ou de plusieurs fichiers PDF
router.post("/file", async (req,res) => {
try {
const files = await file_form_handler(req);
const file_paths: string[] = [];
for (const file of Object.values(files) as any[]) {
if (Array.isArray(file)) {
for (const f of file) {
file_paths.push(f.filepath);
}
} else {
file_paths.push(file.filepath);
}
}
const results_of_parsing = [];
for (const path of file_paths) {
results_of_parsing.push(await pdf_parse(path));
};
res.send(results_of_parsing);
} catch (error) {
console.error(error);
res.status(500).json({
error: error instanceof Error ? error.message : "Unknown error"
});
}
});
// gère la récéption de produit saisie manuellement
router.post("/new", async (req, res) => {
const nom_produit : string = req.body.nom_produit ;
const quantite : number = Number(req.body.quantite);
const prixHT : number = Number(req.body.prixHT);
const tva : string = req.body.tva;
res.send({
[nom_produit]: {
quantité: quantite,
prixHT: prixHT,
TVA: tva
}
});
});
// fonction pour gérer la récéption des fichier par un form
function file_form_handler(req: ExpressRequest): Promise<Files> {
return new Promise<Files>((resolve, reject) => {
const form = formidable({ multiples: true});
form.parse(req, (error: Error | null, fields: any, files: Files) => {
if (error){
reject(error);
return;
}
resolve({ ...fields, ...files})
})
})
}
// ----------------------------------------
// Export router
// ----------------------------------------
export default router;

View File

@@ -15,4 +15,4 @@ router.get("/", (req, res) => {
// ----------------------------------------
// Export router
// ----------------------------------------
module.exports = router;
export default router;

View File

@@ -15,7 +15,11 @@ app.use(express.urlencoded( {extended: false }));
// ----------------------------------------
// Routes import
// ----------------------------------------
app.use("/status", require("./routes/status.routes"));
import statusRoutes from "./routes/status.routes";
import purchaseRoutes from "./routes/purchase.routes";
app.use("/status", statusRoutes);
app.use("/new-purchase", purchaseRoutes);
// ----------------------------------------
// Starting Server

View File

@@ -1,25 +0,0 @@
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,8 +7,7 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"types": ["jest", "node"]
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]