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

+919
View File
@@ -0,0 +1,919 @@
# Documentation API Foirfouille
**Base URL** : `https://api.ffnancy.fr`
**Format** : JSON
**Méthode** : GET uniquement (API lecture seule)
**Données** : Synchronisées depuis SQL Server chaque nuit à 3h00
---
## Santé
```
GET /api/health
```
```json
{ "status": "ok", "database": "foirfouille", "server": "postgres" }
```
---
## Articles
### Liste avec filtres
```
GET /api/articles
```
| Paramètre | Type | Description | Défaut |
|-----------|--------|--------------------------------------|--------|
| `search` | string | Recherche dans libellé | — |
| `codein` | string | Code interne (partiel) | — |
| `ean` | string | EAN / GTIN | — |
| `codefou` | string | Code fournisseur (partiel) | — |
| `actif` | `1`/`0`| `1` = actif, `0` = suspendu | — |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Lignes par page (max 500) | 50 |
**Réponse** :
```json
{
"page": 1,
"limit": 50,
"articles": [
{
"no_id": 12345,
"codein": "ABC123",
"libelle1": "MON ARTICLE",
"libelle2": "SOUS-LIBELLE",
"lib_ticket": "MON ART",
"tax_code": "A",
"ach_code": "X",
"utilisable": "O",
"actif": "O",
"suspendu": null,
"suividatecreation": "2020-01-15T00:00:00.000Z",
"suividatemodif": "2024-06-01T00:00:00.000Z",
"prix_vente_mini": 15.00,
"prix_vente_maxi": 25.00,
"eco_ttc": 0.10,
"on_web": "O",
"interdit_remise": null,
"nomphoto": "ABC123.jpg",
"datedebvente": null,
"datefinvente": null,
"pa": 10.50,
"gtin": "3760000000001",
"codefou_principal": "FOU01",
"nom_fou_principal": "MON FOURNISSEUR",
"ref_fou_principale": "REF-FOU-001",
"pcb_principal": 6
}
]
}
```
---
### Détail d'un article
```
GET /api/articles/:id
```
**Paramètre** : `id` = `no_id` de l'article
**Réponse** :
```json
{
"article": { "no_id": 12345, "codein": "...", "libelle1": "...", "pa": 10.50, "..." : "..." },
"gtins": [
{ "gtin": "3760000000001", "preferentiel": 1 }
],
"stock": [
{ "site": "001", "qte": 12, "prmp": 10.20, "valstock": 122.40, "pv": 18.50, "stockdispo": 10 }
],
"prix": [
{ "site": "001", "pv": 18.50 }
],
"fournisseurs": [
{
"codefou": "FOU01", "nom_fou": "MON FOURNISSEUR",
"reference": "REF-001", "ean13": "3760000000001",
"pcb": 6, "delai": 7, "preference": 1,
"prixachat": 10.50, "remise_promotion": 5.00
}
]
}
```
---
### Fiche complète (référentiel)
```
GET /api/articles/:id/referentiel
```
Retourne toutes les informations consolidées de l'article :
```json
{
"article": {
"no_id": 12345, "codein": "ABC123", "libelle1": "MON ARTICLE",
"prix_vente_mini": 15.00, "prix_vente_maxi": 25.00,
"eco_ht": 0.08, "eco_ttc": 0.10,
"on_web": "O", "pv_conseille": 19.90,
"nom_code": "RAY01", "nom_libelle": "RAYON MAISON", "nom_niveau": 1
},
"gtins": [ { "gtin": "3760000000001", "preferentiel": 1 } ],
"gammes": [
{ "gamme_code": "ETE2024", "gamme_libelle": "ÉTÉ 2024", "saison_code": "S24", "saison_libelle": "SAISON 2024" }
],
"stock": [
{ "site": "001", "qte": 12, "prmp": 10.20, "valstock": 122.40, "pv": 18.50, "stockdispo": 10 }
],
"stock_remballe": [],
"prix": {
"achat": 10.50,
"vente_par_site": [ { "site": "001", "pv": 18.50 } ]
},
"fournisseurs": [
{ "codefou": "FOU01", "nom_fou": "MON FOURNISSEUR", "ref_fou": "REF-001", "pcb": 6, "prixachat": 10.50 }
],
"performance": {
"derniere_entree": "2024-05-10T00:00:00.000Z",
"derniere_vente": "2024-06-15T00:00:00.000Z",
"qte_totale_vendue": 248,
"ca_ttc_total": 4588.00,
"marge_totale": 1020.50
},
"notes": []
}
```
---
### Mouvements d'un article
```
GET /api/articles/:id/mouvements
```
| Paramètre | Type | Description | Défaut |
|-------------|--------|--------------------------------|------------|
| `dateDebut` | date | Date début `YYYY-MM-DD` | 2000-01-01 |
| `dateFin` | date | Date fin `YYYY-MM-DD` | 2099-12-31 |
| `site` | string | Filtrer par site (partiel) | — |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Lignes par page (max 1000) | 100 |
**Réponse** :
```json
{
"page": 1, "limit": 100,
"mouvements": [
{
"datmvt": "2024-06-15T00:00:00.000Z",
"site": "001",
"libmvt": "VENTE",
"genremvt": 3,
"qtemvt": 2,
"mntmvtttc": 37.00,
"margemvt": 8.20,
"qtestock": 10
}
]
}
```
**Valeurs `GenreMvt`** : `1` = entrée stock, `3` = vente
---
## Fournisseurs
### Liste des fournisseurs
```
GET /api/fournisseurs?search=
```
| Paramètre | Type | Description |
|-----------|--------|---------------------------|
| `search` | string | Recherche sur code/nom |
**Réponse** :
```json
{
"count": 142,
"fournisseurs": [
{
"codefou": "FOU01",
"nom": "MON FOURNISSEUR SARL",
"adresse": "1 RUE DE LA PAIX",
"telephone": "0383000000",
"email": "contact@fournisseur.fr",
"nb_articles": 320,
"nb_actifs": 285,
"nb_suspendus": 35
}
]
}
```
---
### Articles d'un fournisseur
```
GET /api/fournisseurs/:code/articles?page=&limit=
```
**Réponse** :
```json
{
"code": "FOU01",
"page": 1, "limit": 50,
"articles": [
{
"no_id": 12345, "codein": "ABC123", "libelle1": "MON ARTICLE",
"ref_fou": "REF-FOU-001", "ean13": "3760000000001",
"pcb": 6, "delai": 7, "suspendu": null,
"prixachat": 10.50, "remise_promotion": 5.00,
"pa": 10.50, "pv_central": 18.50
}
]
}
```
---
### Commandes en cours d'un fournisseur
```
GET /api/fournisseurs/:code/commandes
```
| Paramètre | Type | Description | Défaut |
|-------------|------|-------------------------|------------|
| `dateDebut` | date | Date début `YYYY-MM-DD` | 2020-01-01 |
| `dateFin` | date | Date fin `YYYY-MM-DD` | 2099-12-31 |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
---
## Stock
### Stock global
```
GET /api/stock?site=&page=&limit=
```
| Paramètre | Type | Description | Défaut |
|-----------|--------|--------------------------|--------|
| `site` | string | Filtrer par site | tous |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
---
### Stock d'un article (tous sites)
```
GET /api/stock/article/:id
```
**Réponse** :
```json
{
"artnoid": 12345,
"stock": [
{
"site": "001",
"qte": 12,
"prmp": 10.20,
"valstock": 122.40,
"pv": 18.50,
"stockdispo": 10,
"stockmort": 0,
"stockcolis": 2,
"dernierevente": "2024-06-15T00:00:00.000Z",
"dernierereception": "2024-05-10T00:00:00.000Z",
"nbjoursdernierevente": 5,
"interditachat": null,
"codefou": "FOU01"
}
]
}
```
---
### Stock d'un article à une date précise
```
GET /api/stock/article/:id/historique?date=&site=
```
Retourne le stock tel qu'il était à une date donnée, en utilisant le champ `QteStock`
enregistré dans chaque mouvement (valeur du stock au moment exact du mouvement).
| Paramètre | Type | Description | Défaut |
|-----------|--------|------------------------------------|-------------|
| `date` | date | Date cible `YYYY-MM-DD` | aujourd'hui |
| `site` | string | Filtrer par site (partiel) | tous |
**Réponse** :
```json
{
"artNoId": "12345",
"date": "2025-12-31",
"stock": [
{
"site": "001",
"qte": 28,
"prmp": 10.20,
"date_dernier_mouvement": "2025-12-29T00:00:00.000Z",
"libelle_dernier_mouvement": "VENTE"
}
]
}
```
> **Principe** : on prend le dernier mouvement enregistré avant ou à la date demandée.
> Si aucun mouvement n'existe avant cette date, le site ne figure pas dans la réponse.
---
### Stock + ventes d'un article sur une période
```
GET /api/stock/article/:id/periode?dateDebut=&dateFin=&site=
```
Endpoint principal pour analyser un article sur une période passée.
Retourne en une seule requête : stock de début, stock de fin, ventes et réceptions.
| Paramètre | Type | Description | Défaut |
|-------------|--------|------------------------------------|------------------|
| `dateDebut` | date | Date début `YYYY-MM-DD` | 1er du mois |
| `dateFin` | date | Date fin `YYYY-MM-DD` | aujourd'hui |
| `site` | string | Filtrer par site (partiel) | tous |
**Réponse** :
```json
{
"artNoId": "12345",
"dateDebut": "2025-12-01",
"dateFin": "2025-12-31",
"data": [
{
"site": "001",
"stock_debut": {
"qte": 45,
"prmp": 10.20,
"au": "2025-11-30T00:00:00.000Z"
},
"stock_fin": {
"qte": 28,
"prmp": 10.20,
"au": "2025-12-29T00:00:00.000Z"
},
"ventes": {
"site": "001",
"nb_passages": 18,
"qte_vendue": 17,
"ca_ht": 274.00,
"ca_ttc": 315.10,
"marge": 72.00,
"taux_marge": 22.86,
"premiere_vente": "2025-12-02T00:00:00.000Z",
"derniere_vente": "2025-12-29T00:00:00.000Z"
},
"receptions": {
"site": "001",
"nb_receptions": 1,
"qte_recue": 0
}
}
]
}
```
> **Lecture** : stock_debut = dernier mouvement **avant** dateDebut / stock_fin = dernier mouvement **à ou avant** dateFin.
> Les champs `ventes` et `receptions` sont `null` si aucun mouvement sur la période.
---
### Stock d'un site (tous articles)
```
GET /api/stock/site/:site?page=&limit=
```
---
### Valorisation du stock par site
```
GET /api/stock/valorisation
```
**Réponse** :
```json
{
"valorisation": [
{
"site": "001",
"nb_articles": 4200,
"qte_totale": 85000,
"val_stock": 420000.00,
"val_pv_theorique": 890000.00,
"prmp_moyen": 12.50
}
]
}
```
---
## Commandes
### Commandes en cours
```
GET /api/commandes
```
| Paramètre | Type | Description | Défaut |
|-------------|--------|--------------------------|------------|
| `dateDebut` | date | Date début `YYYY-MM-DD` | 2020-01-01 |
| `dateFin` | date | Date fin `YYYY-MM-DD` | 2099-12-31 |
| `codefou` | string | Filtrer par fournisseur | — |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
**Réponse** :
```json
{
"page": 1, "limit": 100,
"commandes": [
{
"no_commande": 98765,
"codefou": "FOU01",
"codein": "ABC123",
"libelle": "MON ARTICLE",
"ref_fou": "REF-001",
"qte_cde": 24,
"prix_brut": 12.00,
"remise": 5.00,
"prix_net": 11.40,
"montant": 273.60,
"qte_acceptee": 24,
"qte_annulee": 0,
"qte_attente": 0,
"qte_reliquat": 0,
"date_livraison_cible": "2024-07-01T00:00:00.000Z",
"date_commande": "2024-06-01T00:00:00.000Z"
}
]
}
```
---
### Détail d'une commande
```
GET /api/commandes/:noCommande
```
**Réponse** :
```json
{
"noCommande": "98765",
"nbLignes": 15,
"montantTotal": 4250.80,
"lignes": [ { "..." : "..." } ]
}
```
---
### Liste des réceptions
```
GET /api/commandes/receptions/liste
```
| Paramètre | Type | Description | Défaut |
|-------------|------|--------------------------|------------|
| `dateDebut` | date | Date début `YYYY-MM-DD` | 2020-01-01 |
| `dateFin` | date | Date fin `YYYY-MM-DD` | 2099-12-31 |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
---
## Mouvements
### Types de mouvements disponibles
```
GET /api/mouvements/types
```
Retourne tous les codes `GenreMvt` et libellés présents dans la base, avec leur nombre d'occurrences.
**À appeler en premier** pour connaître les codes de régularisation propres à votre magasin.
```json
{
"types": [
{ "genremvt": 1, "libmvt": "RECEPTION", "nb_occurrences": "142580" },
{ "genremvt": 3, "libmvt": "VENTE", "nb_occurrences": "2130137" },
{ "genremvt": 2, "libmvt": "RETOUR CLIENT", "nb_occurrences": "8420" },
{ "genremvt": 5, "libmvt": "REGULARISATION", "nb_occurrences": "3210" },
{ "genremvt": 6, "libmvt": "INVENTAIRE", "nb_occurrences": "1850" }
]
}
```
> Les codes exacts dépendent de votre paramétrage Mercalys. Consultez cet endpoint pour connaître la liste réelle.
---
### Mouvements articles (tous types)
```
GET /api/mouvements/articles
```
| 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 | — |
| `codefou` | string | Filtrer par fournisseur | — |
| `genremvt` | int | Filtrer par type (ex: `1`, `3`, `5`...) | tous |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
**Types principaux `GenreMvt`** :
| Code | Type |
|------|-------------------|
| `1` | Entrée stock |
| `3` | Vente |
| autres | Régularisations (voir `/api/mouvements/types`) |
---
### Entrées en stock
```
GET /api/mouvements/entrees
```
Toutes les réceptions / entrées en stock (`GenreMvt = 1`).
Inclut la **date de création de l'article** pour retrouver la première entrée.
| 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 | — |
| `artNoId` | int | Filtrer sur un article précis | — |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
**Réponse** :
```json
{
"page": 1, "limit": 100,
"entrees": [
{
"date_entree": "2025-12-10T00:00:00.000Z",
"site": "001",
"artnoid": 12345,
"codein": "ABC123",
"libelle1": "MON ARTICLE",
"date_creation_article": "2020-03-15T00:00:00.000Z",
"libelle_mouvement": "RECEPTION",
"qte_entree": 24,
"prmp": 10.20,
"stock_apres_entree": 36,
"codefou": "FOU01"
}
]
}
```
> Pour trouver la **première entrée d'un article** : filtrez par `artNoId` avec `dateDebut=2000-01-01` et triez par date croissante (la première ligne = première réception).
---
### Régularisations de stock
```
GET /api/mouvements/regularisations
```
Tous les mouvements **hors ventes et hors réceptions** : inventaires, corrections, démarques, transferts, retours, etc.
| 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 | — |
| `artNoId` | int | Filtrer sur un article précis | — |
| `page` | int | Numéro de page | 1 |
| `limit` | int | Max 1000 | 100 |
**Réponse** :
```json
{
"page": 1, "limit": 100,
"regularisations": [
{
"date_mouvement": "2025-12-05T00:00:00.000Z",
"site": "001",
"artnoid": 12345,
"codein": "ABC123",
"libelle1": "MON ARTICLE",
"genremvt": 6,
"libelle_mouvement": "INVENTAIRE",
"qte": -3,
"valeur": -30.60,
"stock_apres": 42,
"prmp": 10.20
}
]
}
```
> Une `qte` négative = sortie de stock (correction à la baisse, démarque…).
> Une `qte` positive = entrée hors réception (correction à la hausse, retour…).
---
### Synthèse par jour et site
```
GET /api/mouvements/synthese?dateDebut=&dateFin=&site=
```
**Réponse** :
```json
{
"synthese": [
{
"jour": "2024-06-15",
"site": "001",
"genremvt": 3,
"nb_mvt": 152,
"qte_totale": 312,
"mnt_ht": 4200.00,
"mnt_ttc": 4830.00,
"marge": 1050.00
}
]
}
```
---
## Performance
### Chiffre d'affaires
```
GET /api/performance/ca
```
| 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 |
| `groupBy` | `jour` / `mois` | Granularité | `jour` |
**Réponse** :
```json
{
"groupBy": "mois",
"dateDebut": "2024-01-01",
"dateFin": "2024-12-31",
"site": "tous",
"ca": [
{
"periode": "2024-06",
"site": "001",
"nb_ventes": 4820,
"qte_vendue": 9640,
"ca_ht": 85200.00,
"ca_ttc": 97980.00,
"marge": 21300.00,
"taux_marge": 21.74
}
]
}
```
---
### Hit-parade articles
```
GET /api/performance/hitparade
```
| 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 |
| `limit` | int | Nombre d'articles (max 500) | 50 |
| `groupBy` | `ca` / `qte` / `marge`| Critère de classement | `ca` |
**Réponse** :
```json
{
"classementPar": "ca",
"hitparade": [
{
"codein": "ABC123",
"libelle1": "MON ARTICLE",
"site": "001",
"nb_passages": 320,
"qte_vendue": 640,
"ca_ht": 5760.00,
"ca_ttc": 6624.00,
"marge": 1440.00,
"taux_marge": 21.74,
"derniere_vente": "2024-06-15T00:00:00.000Z"
}
]
}
```
---
### CA par nomenclature (rayon)
```
GET /api/performance/ca/nomenclature?dateDebut=&dateFin=&site=&niveau=1
```
| Paramètre | Type | Description |
|-----------|------|--------------------------------------|
| `niveau` | int | Niveau de nomenclature (1 = racine) |
**Réponse** :
```json
{
"ca_par_nomenclature": [
{
"code_nomen": "RAY01",
"libelle": "RAYON MAISON",
"niveau": 1,
"nb_articles": 420,
"qte_vendue": 8400,
"ca_ht": 95000.00,
"ca_ttc": 109250.00,
"marge": 23750.00
}
]
}
```
---
### CA par gamme
```
GET /api/performance/ca/gamme?dateDebut=&dateFin=&site=
```
**Réponse** :
```json
{
"ca_par_gamme": [
{
"gamme": "ETE2024",
"libelle_gamme": "ÉTÉ 2024",
"nb_articles": 85,
"qte_vendue": 1700,
"ca_ttc": 28900.00,
"marge": 6450.00
}
]
}
```
---
## Synchronisation
### État de la sync
```
GET /api/sync/status
```
**Réponse** :
```json
{
"sync": [
{
"table_name": "articles",
"last_sync": "2026-03-18T03:00:00.000Z",
"rows_synced": 42062,
"status": "ok",
"error_msg": null
}
]
}
```
**Tables synchronisées** :
| Table | Mode | Fréquence |
|-------|------|-----------|
| `articles` | Delta (SUIVIDATEMODIF) | Nuit |
| `article_infosup` | Delta | Nuit |
| `art_gtin` | Upsert complet | Nuit |
| `artfou1` | Delta | Nuit |
| `artfou2` | Upsert complet | Nuit |
| `fouadr1` | Refresh complet | Nuit |
| `cube_stock` | Refresh complet | Nuit |
| `cube_pa` | Refresh complet | Nuit |
| `cube_pv` | Refresh complet | Nuit |
| `mvtart` | Append (DatMvt >= hier) | Nuit |
| `nomenclature` | Refresh complet | Nuit |
| `gammes` | Refresh complet | Nuit |
| `art_gamme_saison` | Refresh complet | Nuit |
| `saisons` | Refresh complet | Nuit |
| `cdefou_vivant` | Refresh complet | Nuit |
| `cdefou_reception` | Delta | Nuit |
| `cdefou_receplig` | Delta | Nuit |
---
## Exemples d'intégration JavaScript
```javascript
const API_BASE = 'https://api.ffnancy.fr';
// Rechercher des articles
async function rechercherArticles(search, page = 1) {
const params = new URLSearchParams({ search, page, limit: 50 });
const res = await fetch(`${API_BASE}/api/articles?${params}`);
return res.json();
}
// Fiche complète d'un article
async function getFicheArticle(id) {
const res = await fetch(`${API_BASE}/api/articles/${id}/referentiel`);
return res.json();
}
// CA du mois en cours par jour
async function getCaMoisEnCours() {
const now = new Date();
const debut = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
const fin = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
const params = new URLSearchParams({ dateDebut: debut, dateFin: fin, groupBy: 'jour' });
const res = await fetch(`${API_BASE}/api/performance/ca?${params}`);
return res.json();
}
// Top 10 articles par CA sur 30 jours
async function getTop10() {
const fin = new Date().toISOString().slice(0, 10);
const debut = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
const params = new URLSearchParams({ dateDebut: debut, dateFin: fin, limit: 10, groupBy: 'ca' });
const res = await fetch(`${API_BASE}/api/performance/hitparade?${params}`);
return res.json();
}
// Stock d'un article
async function getStock(artNoId) {
const res = await fetch(`${API_BASE}/api/stock/article/${artNoId}`);
return res.json();
}
// Valorisation globale du stock
async function getValorisationStock() {
const res = await fetch(`${API_BASE}/api/stock/valorisation`);
return res.json();
}
// Commandes en cours d'un fournisseur
async function getCommandesFournisseur(codeFou) {
const res = await fetch(`${API_BASE}/api/fournisseurs/${codeFou}/commandes`);
return res.json();
}
```
---
## Codes d'erreur HTTP
| Code | Description |
|------|-------------|
| `200` | Succès |
| `404` | Ressource introuvable (article, commande…) |
| `500` | Erreur serveur (détail dans le champ `error`) |
**Format erreur** :
```json
{ "error": "Article introuvable" }
```
---
## Notes importantes
- Les données sont en lecture seule — aucun endpoint d'écriture
- Les données sont décalées d'au plus 24h (sync à 3h00)
- Les champs dates sont au format ISO 8601 UTC
- La pagination commence à `page=1`
- Le champ `no_id` est l'identifiant interne SQL Server de l'article
- Les codes sites (`001`, `002`…) correspondent aux différents points de vente
+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 {