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

+14
View File
@@ -447,6 +447,20 @@ CREATE TABLE IF NOT EXISTS commande_auto_qtepropo (
CREATE INDEX IF NOT EXISTS idx_caqp_art_no_id ON commande_auto_qtepropo (art_no_id);
CREATE INDEX IF NOT EXISTS idx_caqp_codesite ON commande_auto_qtepropo (codesite);
CREATE TABLE IF NOT EXISTS foucad (
foucode TEXT PRIMARY KEY,
franco NUMERIC(12,2),
actif BOOLEAN,
duree INTEGER,
unite INTEGER,
dateref TIMESTAMP,
datecom TIMESTAMP,
montantcdeauto NUMERIC(12,2),
datecdeauto TIMESTAMP,
suividatecreation TIMESTAMP,
suividatemodif TIMESTAMP
);
-- ============================================================
-- Publicités (FBENTPUB + ECOULEMENT + ECOULEMENT_DETAIL SQL Server)
-- ============================================================
+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;
+35
View File
@@ -17,6 +17,11 @@ const COMMANDE_AUTO_COLS = [
'fou_code','art_no_id','codesite','qtepropo','parametre_numero','suividatecreation','suividatemodif',
];
const FOUCAD_COLS = [
'foucode','franco','actif','duree','unite','dateref','datecom',
'montantcdeauto','datecdeauto','suividatecreation','suividatemodif',
];
const PLAN_REAPPRO_COLS = [
'id','magasin','codein','bu_no_id','stockmag','encours_palette_mag',
'attente_prepa_mag','colis_rea','colis_ajout_web','stock_dispo',
@@ -133,6 +138,36 @@ async function syncAppro(force) {
console.error(`[commande_auto_qtepropo] ERREUR: ${err.message}`);
}
// === FOUCAD (full refresh — config commande auto par fournisseur) ===
try {
const res = await ms.request().query(`
SELECT FOUCODE, FRANCO, ACTIF, DUREE, UNITE, DATEREF, DATECOM,
MONTANTCDEAUTO, DATECDEAUTO, SUIVIDATECREATION, SUIVIDATEMODIF
FROM FOUCAD
`);
const rows = res.recordset.map(r => ({
foucode: r.FOUCODE,
franco: r.FRANCO ?? null,
actif: r.ACTIF ?? null,
duree: r.DUREE ?? null,
unite: r.UNITE ?? null,
dateref: r.DATEREF ?? null,
datecom: r.DATECOM ?? null,
montantcdeauto: r.MONTANTCDEAUTO ?? null,
datecdeauto: r.DATECDEAUTO ?? null,
suividatecreation: r.SUIVIDATECREATION,
suividatemodif: r.SUIVIDATEMODIF,
}));
const count = await fullRefresh(pg, 'foucad', rows, FOUCAD_COLS);
await logSync(pg, 'foucad', count, 'ok');
console.log(`[foucad] ${count} lignes`);
} catch (err) {
await logSync(pg, 'foucad', 0, 'error', err.message);
console.error(`[foucad] ERREUR: ${err.message}`);
}
// === PLAN_REAPPRO (full refresh — état courant) ===
try {
const res = await ms.request().query(`