diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..7c5be26 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -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 diff --git a/src/routes/mouvements.js b/src/routes/mouvements.js index 49009dc..63be703 100644 --- a/src/routes/mouvements.js +++ b/src/routes/mouvements.js @@ -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 { diff --git a/src/routes/stock.js b/src/routes/stock.js index 33ca6a0..a3b0b31 100644 --- a/src/routes/stock.js +++ b/src/routes/stock.js @@ -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 {