feat: Implement article list, detail, and referential endpoints, and document the monthly article analysis endpoint.
This commit is contained in:
1 parent
2101606bb4
commit
c98f6834a8
2 files changed
+168
No files matched your search
@@ -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
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user