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:
1 parent
42f7c9bac6
commit
6e5bc3bc07
4 files changed
+177
-1
No files matched your search
@@ -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_art_no_id ON commande_auto_qtepropo (art_no_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_caqp_codesite ON commande_auto_qtepropo (codesite);
|
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)
|
-- Publicités (FBENTPUB + ECOULEMENT + ECOULEMENT_DETAIL SQL Server)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|||||||
+5
-1
@@ -12,6 +12,7 @@ const performanceRouter = require('./routes/performance');
|
|||||||
const syncRouter = require('./routes/sync');
|
const syncRouter = require('./routes/sync');
|
||||||
const rankingRouter = require('./routes/ranking');
|
const rankingRouter = require('./routes/ranking');
|
||||||
const publicitesRouter = require('./routes/publicites');
|
const publicitesRouter = require('./routes/publicites');
|
||||||
|
const commandesAutoRouter = require('./routes/commandes-auto');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -37,7 +38,8 @@ app.use('/api/mouvements', mouvementsRouter);
|
|||||||
app.use('/api/performance', performanceRouter);
|
app.use('/api/performance', performanceRouter);
|
||||||
app.use('/api/sync', syncRouter);
|
app.use('/api/sync', syncRouter);
|
||||||
app.use('/api/ranking', rankingRouter);
|
app.use('/api/ranking', rankingRouter);
|
||||||
app.use('/api/publicites', publicitesRouter);
|
app.use('/api/publicites', publicitesRouter);
|
||||||
|
app.use('/api/commandes-auto', commandesAutoRouter);
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
app.use((req, res) => {
|
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/performance/ca/gamme[?dateDebut=&dateFin=&site=]');
|
||||||
console.log(' GET /api/publicites[?search=&site=&dateDebut=&dateFin=&page=&limit=]');
|
console.log(' GET /api/publicites[?search=&site=&dateDebut=&dateFin=&page=&limit=]');
|
||||||
console.log(' GET /api/publicites/:code[?site=]');
|
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');
|
console.log(' GET /api/sync/status');
|
||||||
});
|
});
|
||||||
@@ -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;
|
||||||
@@ -17,6 +17,11 @@ const COMMANDE_AUTO_COLS = [
|
|||||||
'fou_code','art_no_id','codesite','qtepropo','parametre_numero','suividatecreation','suividatemodif',
|
'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 = [
|
const PLAN_REAPPRO_COLS = [
|
||||||
'id','magasin','codein','bu_no_id','stockmag','encours_palette_mag',
|
'id','magasin','codein','bu_no_id','stockmag','encours_palette_mag',
|
||||||
'attente_prepa_mag','colis_rea','colis_ajout_web','stock_dispo',
|
'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}`);
|
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) ===
|
// === PLAN_REAPPRO (full refresh — état courant) ===
|
||||||
try {
|
try {
|
||||||
const res = await ms.request().query(`
|
const res = await ms.request().query(`
|
||||||
|
|||||||
Reference in new issue
Block a user