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_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
@@ -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');
|
||||
});
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
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(`
|
||||
|
||||
Reference in new issue
Block a user