feat: ajout endpoint /api/commandes-auto avec franco fournisseur

- Nouvelle route GET /api/commandes-auto : propositions par site et fournisseur
  avec comparaison au franco (franco_atteint, ecart_franco)
- Nouvelle route GET /api/commandes-auto/:codefou : détail articles d'un fournisseur
- Sync FOUCAD (config commande auto + franco) vers PostgreSQL
- Ajout table foucad dans init.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-04-08 15:24:51 +02:00
1 parent 42f7c9bac6
commit 6e5bc3bc07
4 files changed
+177 -1

No files matched your search

+5 -1
View File
@@ -12,6 +12,7 @@ const performanceRouter = require('./routes/performance');
const syncRouter = require('./routes/sync');
const rankingRouter = require('./routes/ranking');
const publicitesRouter = require('./routes/publicites');
const commandesAutoRouter = require('./routes/commandes-auto');
const app = express();
app.use(express.json());
@@ -37,7 +38,8 @@ app.use('/api/mouvements', mouvementsRouter);
app.use('/api/performance', performanceRouter);
app.use('/api/sync', syncRouter);
app.use('/api/ranking', rankingRouter);
app.use('/api/publicites', publicitesRouter);
app.use('/api/publicites', publicitesRouter);
app.use('/api/commandes-auto', commandesAutoRouter);
// 404
app.use((req, res) => {
@@ -76,5 +78,7 @@ app.listen(PORT, () => {
console.log(' GET /api/performance/ca/gamme[?dateDebut=&dateFin=&site=]');
console.log(' GET /api/publicites[?search=&site=&dateDebut=&dateFin=&page=&limit=]');
console.log(' GET /api/publicites/:code[?site=]');
console.log(' GET /api/commandes-auto[?site=&codefou=]');
console.log(' GET /api/commandes-auto/:codefou[?site=]');
console.log(' GET /api/sync/status');
});
+123
View File
@@ -0,0 +1,123 @@
const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
// GET /api/commandes-auto?site=&codefou=
// Propositions de commandes automatiques par site et fournisseur, avec comparaison au franco
router.get('/', async (req, res) => {
try {
const pool = getPool();
const { site = '', codefou = '' } = req.query;
const result = await pool.query(`
SELECT
q.codesite AS site,
q.fou_code AS codefou,
fi.nom AS nom_fou,
fc.franco AS franco_ht,
COUNT(q.art_no_id) AS nb_articles,
SUM(q.qtepropo) AS qte_totale,
ROUND(SUM(q.qtepropo * cp.pa), 2) AS montant_propo_ht,
CASE
WHEN fc.franco IS NULL OR fc.franco = 0 THEN 'N/A'
WHEN SUM(q.qtepropo * cp.pa) >= fc.franco THEN 'OUI'
ELSE 'NON'
END AS franco_atteint,
CASE
WHEN fc.franco IS NULL OR fc.franco = 0 THEN NULL
ELSE ROUND(fc.franco - SUM(q.qtepropo * cp.pa), 2)
END AS ecart_franco
FROM commande_auto_qtepropo q
LEFT JOIN fouident fi ON fi.code = q.fou_code
LEFT JOIN foucad fc ON fc.foucode = q.fou_code
LEFT JOIN cube_pa cp ON cp.artnoid = q.art_no_id
WHERE q.qtepropo > 0
AND ($1 = '' OR q.codesite LIKE $1)
AND ($2 = '' OR q.fou_code ILIKE $2)
GROUP BY q.codesite, q.fou_code, fi.nom, fc.franco
ORDER BY q.codesite, montant_propo_ht DESC
`, [`%${site}%`, `%${codefou}%`]);
res.json({ count: result.rows.length, propositions: result.rows });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/commandes-auto/:codefou?site=
// Détail des articles proposés pour un fournisseur donné
router.get('/:codefou', async (req, res) => {
try {
const pool = getPool();
const { site = '' } = req.query;
const { codefou } = req.params;
// Résumé par site
const resume = await pool.query(`
SELECT
q.codesite AS site,
q.fou_code AS codefou,
fi.nom AS nom_fou,
fc.franco AS franco_ht,
COUNT(q.art_no_id) AS nb_articles,
SUM(q.qtepropo) AS qte_totale,
ROUND(SUM(q.qtepropo * cp.pa), 2) AS montant_propo_ht,
CASE
WHEN fc.franco IS NULL OR fc.franco = 0 THEN 'N/A'
WHEN SUM(q.qtepropo * cp.pa) >= fc.franco THEN 'OUI'
ELSE 'NON'
END AS franco_atteint,
CASE
WHEN fc.franco IS NULL OR fc.franco = 0 THEN NULL
ELSE ROUND(fc.franco - SUM(q.qtepropo * cp.pa), 2)
END AS ecart_franco
FROM commande_auto_qtepropo q
LEFT JOIN fouident fi ON fi.code = q.fou_code
LEFT JOIN foucad fc ON fc.foucode = q.fou_code
LEFT JOIN cube_pa cp ON cp.artnoid = q.art_no_id
WHERE q.qtepropo > 0
AND q.fou_code ILIKE $1
AND ($2 = '' OR q.codesite LIKE $2)
GROUP BY q.codesite, q.fou_code, fi.nom, fc.franco
ORDER BY q.codesite
`, [codefou, `%${site}%`]);
if (resume.rows.length === 0) {
return res.status(404).json({ error: 'Aucune proposition pour ce fournisseur' });
}
// Détail des articles
const lignes = await pool.query(`
SELECT
q.codesite AS site,
q.art_no_id AS no_id,
a.codein,
a.libelle1,
af.reference AS ref_fou,
af.pcb,
q.qtepropo,
cp.pa,
ROUND(q.qtepropo * cp.pa, 2) AS montant_ht,
cs.qte AS stock_actuel,
cs.dernierevente
FROM commande_auto_qtepropo q
LEFT JOIN articles a ON a.no_id = q.art_no_id
LEFT JOIN artfou1 af ON af.fou_code = q.fou_code AND af.art_no_id = q.art_no_id AND af.preference = 1
LEFT JOIN cube_pa cp ON cp.artnoid = q.art_no_id
LEFT JOIN cube_stock cs ON cs.artnoid = q.art_no_id AND cs.site = q.codesite
WHERE q.qtepropo > 0
AND q.fou_code ILIKE $1
AND ($2 = '' OR q.codesite LIKE $2)
ORDER BY q.codesite, montant_ht DESC
`, [codefou, `%${site}%`]);
res.json({
resume: resume.rows,
lignes: lignes.rows,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;