feat: implement stock management API endpoints and update the API documentation.

This commit is contained in:
Michael committed 2026-03-18 12:03:18 +01:00
1 parent 6dbfa2b32d
commit 2101606bb4
3 files changed
+1144 -3

No files matched your search

+100 -3
View File
@@ -2,14 +2,31 @@ const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
// GET /api/mouvements/articles?dateDebut=&dateFin=&site=&codefou=&page=&limit=
// GET /api/mouvements/types - Liste des types de mouvements distincts dans la base
router.get('/types', async (req, res) => {
try {
const pool = getPool();
const result = await pool.query(`
SELECT GenreMvt, LibMvt, COUNT(*) AS nb_occurrences
FROM MvtArt
GROUP BY GenreMvt, LibMvt
ORDER BY GenreMvt, nb_occurrences DESC
`);
res.json({ types: result.rows });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/mouvements/articles?dateDebut=&dateFin=&site=&codefou=&genremvt=&page=&limit=
router.get('/articles', async (req, res) => {
try {
const pool = getPool();
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', codefou = '', page = 1, limit = 100 } = req.query;
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', codefou = '', genremvt = '', page = 1, limit = 100 } = req.query;
const pageNum = Math.max(1, parseInt(page) || 1);
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
const offsetNum = (pageNum - 1) * limitNum;
const genreFilter = genremvt !== '' ? parseInt(genremvt) : null;
const result = await pool.query(`
SELECT
@@ -23,9 +40,10 @@ router.get('/articles', async (req, res) => {
WHERE m.DatMvt BETWEEN $1 AND $2
AND m.Site LIKE $3
AND m.CODEFOU LIKE $4
AND ($5::int IS NULL OR m.GenreMvt = $5)
ORDER BY m.DatMvt DESC
LIMIT ${limitNum} OFFSET ${offsetNum}
`, [dateDebut, dateFin, `%${site}%`, `%${codefou}%`]);
`, [dateDebut, dateFin, `%${site}%`, `%${codefou}%`, genreFilter]);
res.json({ page: pageNum, limit: limitNum, mouvements: result.rows });
} catch (err) {
@@ -33,6 +51,85 @@ router.get('/articles', async (req, res) => {
}
});
// GET /api/mouvements/entrees?dateDebut=&dateFin=&site=&artNoId=&page=&limit=
// Entrées en stock uniquement (GenreMvt = 1) avec date de création article
router.get('/entrees', async (req, res) => {
try {
const pool = getPool();
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', artNoId = '', page = 1, limit = 100 } = req.query;
const pageNum = Math.max(1, parseInt(page) || 1);
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
const offsetNum = (pageNum - 1) * limitNum;
const result = await pool.query(`
SELECT
m.DatMvt AS date_entree,
m.Site,
a.NO_ID AS artnoid,
a.CODEIN,
a.LIBELLE1,
a.SUIVIDATECREATION AS date_creation_article,
m.LibMvt AS libelle_mouvement,
m.QteMvt AS qte_entree,
m.Prmp AS prmp,
m.QteStock AS stock_apres_entree,
m.CODEFOU AS codefou
FROM MvtArt m
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
WHERE m.GenreMvt = 1
AND m.DatMvt BETWEEN $1 AND $2
AND m.Site LIKE $3
AND ($4 = '' OR m.ArtNoId::text = $4)
ORDER BY m.DatMvt DESC
LIMIT ${limitNum} OFFSET ${offsetNum}
`, [dateDebut, dateFin, `%${site}%`, artNoId]);
res.json({ page: pageNum, limit: limitNum, entrees: result.rows });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/mouvements/regularisations?dateDebut=&dateFin=&site=&artNoId=&page=&limit=
// Mouvements hors ventes (GenreMvt=3) et hors entrées standard (GenreMvt=1)
// = inventaires, corrections, démarques, transferts, etc.
router.get('/regularisations', async (req, res) => {
try {
const pool = getPool();
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', artNoId = '', page = 1, limit = 100 } = req.query;
const pageNum = Math.max(1, parseInt(page) || 1);
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
const offsetNum = (pageNum - 1) * limitNum;
const result = await pool.query(`
SELECT
m.DatMvt AS date_mouvement,
m.Site,
a.NO_ID AS artnoid,
a.CODEIN,
a.LIBELLE1,
m.GenreMvt,
m.LibMvt AS libelle_mouvement,
m.QteMvt AS qte,
m.ValMvt AS valeur,
m.QteStock AS stock_apres,
m.Prmp AS prmp
FROM MvtArt m
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
WHERE m.GenreMvt NOT IN (1, 3)
AND m.DatMvt BETWEEN $1 AND $2
AND m.Site LIKE $3
AND ($4 = '' OR m.ArtNoId::text = $4)
ORDER BY m.DatMvt DESC
LIMIT ${limitNum} OFFSET ${offsetNum}
`, [dateDebut, dateFin, `%${site}%`, artNoId]);
res.json({ page: pageNum, limit: limitNum, regularisations: result.rows });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/mouvements/reglements?dateDebut=&dateFin=&page=&limit=
router.get('/reglements', async (req, res) => {
try {
+125
View File
@@ -83,6 +83,131 @@ router.get('/site/:site', async (req, res) => {
}
});
// GET /api/stock/article/:id/historique?date=&site= - Stock d'un article à une date donnée
// Utilise le champ QteStock de MvtArt (stock enregistré au moment de chaque mouvement)
router.get('/article/:id/historique', async (req, res) => {
try {
const pool = getPool();
const { date = new Date().toISOString().slice(0, 10), site = '' } = req.query;
const result = await pool.query(`
SELECT DISTINCT ON (Site)
Site,
QteStock AS qte,
Prmp,
DatMvt AS date_dernier_mouvement,
LibMvt AS libelle_dernier_mouvement
FROM MvtArt
WHERE ArtNoId = $1
AND DatMvt <= $2
AND Site LIKE $3
ORDER BY Site, DatMvt DESC
`, [req.params.id, date + ' 23:59:59', `%${site}%`]);
res.json({
artNoId: req.params.id,
date,
stock: result.rows,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/stock/article/:id/periode?dateDebut=&dateFin=&site= - Stock + ventes sur une période
router.get('/article/:id/periode', async (req, res) => {
try {
const pool = getPool();
const {
dateDebut = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().slice(0, 10),
dateFin = new Date().toISOString().slice(0, 10),
site = '',
} = req.query;
// Stock au début de la période (dernier mouvement avant dateDebut)
const stockDebut = await pool.query(`
SELECT DISTINCT ON (Site)
Site, QteStock AS qte, Prmp, DatMvt AS date_mouvement
FROM MvtArt
WHERE ArtNoId = $1
AND DatMvt < $2
AND Site LIKE $3
ORDER BY Site, DatMvt DESC
`, [req.params.id, dateDebut, `%${site}%`]);
// Stock à la fin de la période (dernier mouvement <= dateFin)
const stockFin = await pool.query(`
SELECT DISTINCT ON (Site)
Site, QteStock AS qte, Prmp, DatMvt AS date_mouvement
FROM MvtArt
WHERE ArtNoId = $1
AND DatMvt <= $2
AND Site LIKE $3
ORDER BY Site, DatMvt DESC
`, [req.params.id, dateFin + ' 23:59:59', `%${site}%`]);
// Ventes sur la période (GenreMvt = 3)
const ventes = await pool.query(`
SELECT
Site,
COUNT(*) AS nb_passages,
SUM(QteMvt) AS qte_vendue,
SUM(MntMvtHt) AS ca_ht,
SUM(MntMvtTTC) AS ca_ttc,
SUM(MargeMvt) AS marge,
CASE WHEN SUM(MntMvtTTC) > 0
THEN ROUND(SUM(MargeMvt) / SUM(MntMvtTTC) * 100, 2)
ELSE 0 END AS taux_marge,
MIN(DatMvt) AS premiere_vente,
MAX(DatMvt) AS derniere_vente
FROM MvtArt
WHERE ArtNoId = $1
AND GenreMvt = 3
AND DatMvt BETWEEN $2 AND $3
AND Site LIKE $4
GROUP BY Site
`, [req.params.id, dateDebut, dateFin + ' 23:59:59', `%${site}%`]);
// Réceptions sur la période (GenreMvt = 1)
const receptions = await pool.query(`
SELECT
Site,
COUNT(*) AS nb_receptions,
SUM(QteMvt) AS qte_recue
FROM MvtArt
WHERE ArtNoId = $1
AND GenreMvt = 1
AND DatMvt BETWEEN $2 AND $3
AND Site LIKE $4
GROUP BY Site
`, [req.params.id, dateDebut, dateFin + ' 23:59:59', `%${site}%`]);
// Indexer par site pour faciliter l'assemblage
const bysite = (rows) => Object.fromEntries(rows.map(r => [r.site, r]));
const sd = bysite(stockDebut.rows);
const sf = bysite(stockFin.rows);
const sv = bysite(ventes.rows);
const sr = bysite(receptions.rows);
const sites = [...new Set([
...Object.keys(sd), ...Object.keys(sf),
...Object.keys(sv), ...Object.keys(sr),
])].sort();
const data = sites.map(s => ({
site: s,
stock_debut: sd[s] ? { qte: sd[s].qte, prmp: sd[s].prmp, au: sd[s].date_mouvement } : null,
stock_fin: sf[s] ? { qte: sf[s].qte, prmp: sf[s].prmp, au: sf[s].date_mouvement } : null,
ventes: sv[s] ?? null,
receptions: sr[s] ?? null,
}));
res.json({ artNoId: req.params.id, dateDebut, dateFin, data });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/stock/valorisation - Valorisation du stock par site
router.get('/valorisation', async (req, res) => {
try {