From 6e5bc3bc0778a8ae60400550343bf43b16280177 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 8 Apr 2026 15:24:51 +0200 Subject: [PATCH] feat: ajout endpoint /api/commandes-auto avec franco fournisseur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- postgres/init.sql | 14 ++++ src/app.js | 6 +- src/routes/commandes-auto.js | 123 +++++++++++++++++++++++++++++++++++ sync/tables/appro.js | 35 ++++++++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/routes/commandes-auto.js diff --git a/postgres/init.sql b/postgres/init.sql index 1e11d86..b750a1e 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -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) -- ============================================================ diff --git a/src/app.js b/src/app.js index 4ec1250..cd6978c 100644 --- a/src/app.js +++ b/src/app.js @@ -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'); }); diff --git a/src/routes/commandes-auto.js b/src/routes/commandes-auto.js new file mode 100644 index 0000000..4a27443 --- /dev/null +++ b/src/routes/commandes-auto.js @@ -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; diff --git a/sync/tables/appro.js b/sync/tables/appro.js index d86f854..e800eef 100644 --- a/sync/tables/appro.js +++ b/sync/tables/appro.js @@ -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(`