feat(articles): expose and filter code centrale (artcentrale)
The code centrale is LaFoir'Fouille's 11-digit product identifier
(prefix 1000x). Only products referenced by the central purchasing
office have one: ~355 800 of 427 700 articles.
The column was already synced from SQL Server and returned by the
detail route via `a.*`, but was absent from the list route and could
not be filtered on.
- list route: select artcentrale, add `artcentrale` (partial match)
and `has_artcentrale=1|0` query params
- filter matches on prefix AND length 11, so malformed values with a
valid prefix ('10000', '10000192289P') are excluded
- COALESCE in the predicate: without it, NOT (NULL LIKE '1000%') is
NULL and the 61 442 NULL rows would vanish from has_artcentrale=0
- normalise '' to null in both routes so they agree
- sync: use safeStr so empty values are stored as NULL, not ''
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
62249e6c13
commit
f00b9c4b14
3 files changed
+30
-5
No files matched your search
+10
-1
@@ -32,9 +32,17 @@ GET /api/articles
|
|||||||
| `ean` | string | EAN / GTIN | — |
|
| `ean` | string | EAN / GTIN | — |
|
||||||
| `codefou` | string | Code fournisseur (partiel) | — |
|
| `codefou` | string | Code fournisseur (partiel) | — |
|
||||||
| `actif` | `1`/`0`| `1` = actif, `0` = suspendu | — |
|
| `actif` | `1`/`0`| `1` = actif, `0` = suspendu | — |
|
||||||
|
| `artcentrale` | string | Code centrale (partiel) | — |
|
||||||
|
| `has_artcentrale` | `1`/`0` | `1` = référencé centrale, `0` = non référencé | — |
|
||||||
| `page` | int | Numéro de page | 1 |
|
| `page` | int | Numéro de page | 1 |
|
||||||
| `limit` | int | Lignes par page (max 500) | 50 |
|
| `limit` | int | Lignes par page (max 500) | 50 |
|
||||||
|
|
||||||
|
> **Code centrale** (`artcentrale`) : identifiant LaFoir'Fouille sur 11 chiffres, préfixe `1000x`
|
||||||
|
> (ex. `10000137005`). Seuls les produits référencés par la centrale en possèdent un —
|
||||||
|
> environ 355 800 articles sur 427 700. Pour les autres, le champ vaut `null`.
|
||||||
|
> `has_artcentrale=1` ne retient que les codes conformes : quelques valeurs parasites
|
||||||
|
> (`10000`, `10000192289P`) ont le bon préfixe mais pas la bonne longueur, et sont exclues.
|
||||||
|
|
||||||
**Réponse** :
|
**Réponse** :
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -54,6 +62,7 @@ GET /api/articles
|
|||||||
"suspendu": null,
|
"suspendu": null,
|
||||||
"suividatecreation": "2020-01-15T00:00:00.000Z",
|
"suividatecreation": "2020-01-15T00:00:00.000Z",
|
||||||
"suividatemodif": "2024-06-01T00:00:00.000Z",
|
"suividatemodif": "2024-06-01T00:00:00.000Z",
|
||||||
|
"artcentrale": "10000137005",
|
||||||
"prix_vente_mini": 15.00,
|
"prix_vente_mini": 15.00,
|
||||||
"prix_vente_maxi": 25.00,
|
"prix_vente_maxi": 25.00,
|
||||||
"eco_ttc": 0.10,
|
"eco_ttc": 0.10,
|
||||||
@@ -85,7 +94,7 @@ GET /api/articles/:id
|
|||||||
**Réponse** :
|
**Réponse** :
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"article": { "no_id": 12345, "codein": "...", "libelle1": "...", "pa": 10.50, "..." : "..." },
|
"article": { "no_id": 12345, "codein": "...", "libelle1": "...", "artcentrale": "10000137005", "pa": 10.50, "..." : "..." },
|
||||||
"gtins": [
|
"gtins": [
|
||||||
{ "gtin": "3760000000001", "preferentiel": 1 }
|
{ "gtin": "3760000000001", "preferentiel": 1 }
|
||||||
],
|
],
|
||||||
|
|||||||
+19
-3
@@ -2,11 +2,12 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getPool } = require('../config/database');
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
// GET /api/articles?search=&codein=&ean=&actif=&codefou=&page=&limit=
|
// GET /api/articles?search=&codein=&ean=&actif=&codefou=&artcentrale=&has_artcentrale=&page=&limit=
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const { search = '', codein = '', ean = '', actif = '', codefou = '', page = 1, limit = 50 } = req.query;
|
const { search = '', codein = '', ean = '', actif = '', codefou = '',
|
||||||
|
artcentrale = '', has_artcentrale = '', page = 1, limit = 50 } = req.query;
|
||||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
||||||
const offsetNum = (pageNum - 1) * limitNum;
|
const offsetNum = (pageNum - 1) * limitNum;
|
||||||
@@ -16,6 +17,7 @@ router.get('/', async (req, res) => {
|
|||||||
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2, a.LIB_TICKET,
|
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2, a.LIB_TICKET,
|
||||||
a.TAX_CODE, a.ACH_CODE, a.UTILISABLE, a.ACTIF, a.SUSPENDU,
|
a.TAX_CODE, a.ACH_CODE, a.UTILISABLE, a.ACTIF, a.SUSPENDU,
|
||||||
a.SUIVIDATECREATION, a.SUIVIDATEMODIF,
|
a.SUIVIDATECREATION, a.SUIVIDATEMODIF,
|
||||||
|
NULLIF(TRIM(a.ARTCENTRALE), '') AS ARTCENTRALE,
|
||||||
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI, ai.ECO_TTC,
|
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI, ai.ECO_TTC,
|
||||||
ai.ON_WEB, ai.INTERDIT_REMISE, ai.NOMPHOTO,
|
ai.ON_WEB, ai.INTERDIT_REMISE, ai.NOMPHOTO,
|
||||||
ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||||
@@ -50,9 +52,22 @@ router.get('/', async (req, res) => {
|
|||||||
AND ($4 = '%%' OR EXISTS (
|
AND ($4 = '%%' OR EXISTS (
|
||||||
SELECT 1 FROM ARTFOU1 f WHERE f.ART_NO_ID = a.NO_ID AND f.CODE LIKE $4
|
SELECT 1 FROM ARTFOU1 f WHERE f.ART_NO_ID = a.NO_ID AND f.CODE LIKE $4
|
||||||
))
|
))
|
||||||
|
AND ($6 = '%%' OR TRIM(a.ARTCENTRALE) LIKE $6)
|
||||||
|
-- code centrale LaFoir'Fouille : préfixe 1000x sur 11 chiffres.
|
||||||
|
-- COALESCE car les articles non référencés valent '' ou NULL.
|
||||||
|
AND ($7 = '' OR (
|
||||||
|
$7 = '1' AND TRIM(COALESCE(a.ARTCENTRALE, '')) LIKE '1000%'
|
||||||
|
AND LENGTH(TRIM(COALESCE(a.ARTCENTRALE, ''))) = 11
|
||||||
|
) OR (
|
||||||
|
$7 = '0' AND NOT (
|
||||||
|
TRIM(COALESCE(a.ARTCENTRALE, '')) LIKE '1000%'
|
||||||
|
AND LENGTH(TRIM(COALESCE(a.ARTCENTRALE, ''))) = 11
|
||||||
|
)
|
||||||
|
))
|
||||||
ORDER BY a.LIBELLE1
|
ORDER BY a.LIBELLE1
|
||||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||||
`, [`%${search}%`, `%${codein}%`, `%${ean}%`, `%${codefou}%`, actif]);
|
`, [`%${search}%`, `%${codein}%`, `%${ean}%`, `%${codefou}%`, actif,
|
||||||
|
`%${artcentrale}%`, has_artcentrale]);
|
||||||
|
|
||||||
const articles = result.rows.map(a => {
|
const articles = result.rows.map(a => {
|
||||||
const photoCode = a.nomphoto ? a.nomphoto.replace(/\.[^.]+$/, '') : null;
|
const photoCode = a.nomphoto ? a.nomphoto.replace(/\.[^.]+$/, '') : null;
|
||||||
@@ -123,6 +138,7 @@ router.get('/:id', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
article: {
|
article: {
|
||||||
...art,
|
...art,
|
||||||
|
artcentrale: art.artcentrale?.trim() || null,
|
||||||
photo_url: photoCode ? `/api/articles/${req.params.id}/photo` : null,
|
photo_url: photoCode ? `/api/articles/${req.params.id}/photo` : null,
|
||||||
photo_url_large: photoCode ? `/api/articles/${req.params.id}/photo?size=large` : null,
|
photo_url_large: photoCode ? `/api/articles/${req.params.id}/photo?size=large` : null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async function syncArticles(force) {
|
|||||||
suividatecreation: r.SUIVIDATECREATION,
|
suividatecreation: r.SUIVIDATECREATION,
|
||||||
suividatemodif: r.SUIVIDATEMODIF,
|
suividatemodif: r.SUIVIDATEMODIF,
|
||||||
nom_no_id: r.NOM_NO_ID,
|
nom_no_id: r.NOM_NO_ID,
|
||||||
artcentrale: r.ARTCENTRALE ?? null,
|
artcentrale: safeStr(r.ARTCENTRALE),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const count = await batchUpsert(pg, 'articles', rows, ['no_id'], ARTICLES_COLS);
|
const count = await batchUpsert(pg, 'articles', rows, ['no_id'], ARTICLES_COLS);
|
||||||
|
|||||||
Reference in new issue
Block a user