diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 7c5be26..42875f7 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -152,7 +152,76 @@ Retourne toutes les informations consolidées de l'article : --- +### Analyse mensuelle d'un article (ventes + stock mois par mois) + +``` +GET /api/articles/:id/mensuel?dateDebut=&dateFin=&site= +``` + +Retourne en une seule requête, mois par mois et par site : +- **Ventes** du mois (quantité, CA, marge) +- **Stock fin de mois** (dernier QteStock enregistré dans le mois) +- **Réceptions** du mois + +| Paramètre | Type | Description | Défaut | +|-------------|--------|----------------------------|------------| +| `dateDebut` | date | Date début `YYYY-MM-DD` | 2024-01-01 | +| `dateFin` | date | Date fin `YYYY-MM-DD` | 2099-12-31 | +| `site` | string | Filtrer par site | tous | + +**Réponse** : + +```json +{ + "artNoId": "12345", + "dateDebut": "2025-01-01", + "dateFin": "2025-12-31", + "data": [ + { + "mois": "2025-12", + "site": "001", + "stock_fin_mois": 28, + "prmp_fin_mois": 10.20, + "ventes": { + "nb_passages": 18, + "qte_vendue": 17, + "ca_ht": 274.00, + "ca_ttc": 315.10, + "marge": 72.00, + "taux_marge": 22.86 + }, + "receptions": { + "nb_receptions": 1, + "qte_recue": 24 + } + }, + { + "mois": "2025-11", + "site": "001", + "stock_fin_mois": 45, + "prmp_fin_mois": 10.20, + "ventes": { + "nb_passages": 12, + "qte_vendue": 10, + "ca_ht": 161.00, + "ca_ttc": 185.15, + "marge": 42.00, + "taux_marge": 22.69 + }, + "receptions": null + } + ] +} +``` + +> `stock_fin_mois` = QteStock du **dernier mouvement du mois** (vente, réception ou autre). +> Si un mois n'a aucun mouvement il n'apparaît pas dans la réponse. +> `ventes` ou `receptions` sont `null` si aucun mouvement de ce type dans le mois. + +--- + ### Mouvements d'un article + ``` GET /api/articles/:id/mouvements ``` diff --git a/src/routes/articles.js b/src/routes/articles.js index 8c3ec58..a738131 100644 --- a/src/routes/articles.js +++ b/src/routes/articles.js @@ -267,4 +267,103 @@ router.get('/:id/mouvements', async (req, res) => { } }); +// GET /api/articles/:id/mensuel?dateDebut=&dateFin=&site= +// Ventes + stock fin de mois, mois par mois, par site +router.get('/:id/mensuel', async (req, res) => { + try { + const pool = getPool(); + const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '' } = req.query; + + // Ventes par mois et par site + const ventes = await pool.query(` + SELECT + TO_CHAR(DatMvt, 'YYYY-MM') AS mois, + 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 + FROM MvtArt + WHERE ArtNoId = $1 + AND GenreMvt = 3 + AND DatMvt BETWEEN $2 AND $3 + AND Site LIKE $4 + GROUP BY TO_CHAR(DatMvt, 'YYYY-MM'), Site + ORDER BY mois DESC, Site + `, [req.params.id, dateDebut, dateFin, `%${site}%`]); + + // Réceptions par mois et par site + const receptions = await pool.query(` + SELECT + TO_CHAR(DatMvt, 'YYYY-MM') AS mois, + 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 TO_CHAR(DatMvt, 'YYYY-MM'), Site + ORDER BY mois DESC, Site + `, [req.params.id, dateDebut, dateFin, `%${site}%`]); + + // Stock fin de mois : dernier QteStock de chaque mois par site + const stocks = await pool.query(` + SELECT DISTINCT ON (TO_CHAR(DatMvt, 'YYYY-MM'), Site) + TO_CHAR(DatMvt, 'YYYY-MM') AS mois, + Site, + QteStock AS stock_fin_mois, + Prmp AS prmp_fin_mois + FROM MvtArt + WHERE ArtNoId = $1 + AND DatMvt BETWEEN $2 AND $3 + AND Site LIKE $4 + ORDER BY TO_CHAR(DatMvt, 'YYYY-MM'), Site, DatMvt DESC + `, [req.params.id, dateDebut, dateFin, `%${site}%`]); + + // Assembler par mois+site + const key = (mois, site) => `${mois}|${site}`; + const byKey = (rows) => Object.fromEntries(rows.map(r => [key(r.mois, r.site), r])); + + const sv = byKey(ventes.rows); + const sr = byKey(receptions.rows); + const ss = byKey(stocks.rows); + + const keys = [...new Set([ + ...Object.keys(sv), ...Object.keys(sr), ...Object.keys(ss), + ])].sort().reverse(); + + const data = keys.map(k => { + const [mois, site] = k.split('|'); + return { + mois, + site, + stock_fin_mois: ss[k]?.stock_fin_mois ?? null, + prmp_fin_mois: ss[k]?.prmp_fin_mois ?? null, + ventes: sv[k] ? { + nb_passages: sv[k].nb_passages, + qte_vendue: sv[k].qte_vendue, + ca_ht: sv[k].ca_ht, + ca_ttc: sv[k].ca_ttc, + marge: sv[k].marge, + taux_marge: sv[k].taux_marge, + } : null, + receptions: sr[k] ? { + nb_receptions: sr[k].nb_receptions, + qte_recue: sr[k].qte_recue, + } : null, + }; + }); + + res.json({ artNoId: req.params.id, dateDebut, dateFin, data }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + module.exports = router;