Le type réel de articles.artcentrale varie selon l'installation FF Nancy
(varchar ou numérique). La comparaison avec des codes centraux (chaînes)
passe désormais par TRIM(...::text), comme ailleurs dans ce fichier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
La Grille part de NOTRE catalogue : on extrait nos codes centraux puis on
demande à Qlik les métriques de ces codes. Un produit vendu par le réseau
mais absent de FF Nancy était donc invisible par construction.
Cette page inverse le sens : elle interroge Qlik d'abord, par libellé ou par
code centrale, et affiche aussi les produits que nous ne référençons pas.
- searchNetworkProductsPlaywright() : sélection par SelectValues sur
« Article Code » (mode code) ou par SearchListObjectFor +
AcceptListObjectSearch sur la dimension « Article » (mode plein texte,
avec repli sur des noms de champs si la master dimension échoue).
Cube [Article Code, Article, Mois] × 6 mesures, passe N-1 incluse pour
obtenir 12 mois glissants contigus. Garde-fou à 300 produits/recherche.
- /api/qlik/search : job asynchrone (POST + polling GET) comme la sync,
une seule recherche à la fois pour ménager le moteur Qlik. Met les
résultats en cache dans qlik_network_metrics et enrichit avec le
catalogue local (badge « Chez moi » / « Réseau uniquement », stock).
- Page /recherche-reseau : tableau triable, filtres, fiche produit avec la
courbe 12 mois, export Excel.
- Composants de tendance extraits de heatmap-grid vers features/network
pour être partagés (comportement inchangé).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
L'instantané grid_rows fige la ligne au moment du calcul, mais deux données
évoluent indépendamment et doivent refléter l'état courant :
- Métriques réseau Qlik : qlik_network_metrics est alimentée par les syncs et
les recherches réseau, hors du calcul de grille. Relues à chaque appel et
exposées dans `network` (null quand le produit n'en a pas). Les colonnes
caReseau/qteReseau/... de la ligne sont réalignées dessus.
- Gamme serveur : codeGamme peut être surchargée par un snapshot de session.
L'API expose `codeGammeServeur`, la gamme NON modifiée telle qu'elle est en
base PostgreSQL, relue à chaque appel. codeGammeInit est gardé aligné.
Ajout de pgGetGammesByCodeins() : variante sans jointure artfou1, nécessaire
car la recherche de l'API est transversale (pas de fournisseur connu).
Ce n'est pas un recalcul : deux lectures indexées bornées à la page courante
(500 lignes max). Mesuré à ~20 ms, soit le même coût que sans enrichissement.
Paramètre enrich=0 pour servir l'instantané brut.
Paramètres → nouvelle section « API CollectFlow — Connexion » : URL de base
déduite de l'origine, en-têtes d'authentification, liste des endpoints et des
paramètres, exemples curl copiables, lien vers openapi.json.
Vérifié sur PostgreSQL local avec un instantané volontairement périmé :
gamme snapshot A → serveur C, caReseau 111 → 990000, produit sans données
réseau → network null, produit sans gamme → codeGammeServeur null, enrich=0
redonnant bien les valeurs figées, et toujours zéro recalcul.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
Analyse de cohérence de stock sur le cas 487673 site 579 : 24 réceptions,
31 ventes brutes, 7 retours clients → 24 ventes nettes, stock final 3.
Le « 38 » affiché = 31 + 7 : signature de l'ancienne formule SUM(ABS)
qui additionnait les retours. La formule nette SUM(-qtemvt) déjà en place
donne 24 — le filtre mntmvtttc <> 0 (hypothèse de lignes fantômes 0 €)
était inutile : aucune ligne de ce type dans les données réelles. Retiré
pour rester strictement aligné sur le calcul de la Grille.
Gestion de stock : ORDER BY cs.qte (colonne affichée) au lieu de
cs.stockdispo (colonne différente) dans pgGetStockNegatif.
Cas réel : codein 487673 site 579 affichait 38 vendus au lieu de 24,
avec un CA pourtant exact (215,76 €) — mvtart contient des lignes
genremvt=3 portant une quantité mais aucun montant TTC (corrections /
régularisations), que les endpoints officiels de l'API excluent.
Ajout du filtre mntmvtttc IS NOT NULL AND <> 0 dans l'agrégation :
une vente réelle a toujours un montant TTC non nul. Validé sur
PostgreSQL local en reproduisant le cas : sans filtre 38/215,76 €
(= symptôme), avec filtre 24/215,76 € (= réalité). Le CA et les
requêtes Analytics ne changent pas (ces lignes valent 0 €).
La version précédente agrégeait les ventes avec les jointures fournisseur
(LATERAL par ligne de mouvement + fouident) dans la même passe : si
fouident contient des codes dupliqués, chaque mouvement était compté
plusieurs fois → chiffres faux sur les deux magasins, et le LATERAL
s'exécutait par mouvement (très lent).
Restructuration, validée sur 12 000 mouvements réels de l'API chargés
dans un PostgreSQL 16 local :
- CTE ventes : mvtart JOIN articles uniquement (1:1), SUM(-qtemvt) /
SUM(-mntmvtttc) / SUM(margemvt) par (codein, site) — le calcul
canonique de la Grille (pgGetMensuelByFournisseur), signes vérifiés
sur les mouvements réels (ventes négatives, genremvt=3).
- CTE attrs : libellé/fournisseur/nomenclature joints APRÈS agrégation,
dédupliqués par DISTINCT ON — aucune jointure ne peut plus fausser
les sommes (totaux invariants aux doublons artfou1/fouident injectés).
- Même principe pour pgGetCaByFournisseur, pgGetStockNegatif,
pgGetSansVente6Mois : sous-requête artfou1 DISTINCT ON au lieu du
LATERAL par ligne.
La requête Hit Parade divergeait du calcul canonique de la Grille :
- SUM(ABS(qtemvt)) additionnait les retours clients comme des ventes au
lieu de les déduire → quantités et CA gonflés sur le magasin ayant des
retours/avoirs sur la période (visible sur 579).
- ABS(SUM(mntmvtttc)) inversait le signe d'un CA net négatif.
- GROUP BY no_id + jointure artfou1 preference=1 non dédupliquée pouvait
produire plusieurs lignes par (codein, site), écrasées par le pivot.
Corrections :
- Hit Parade : ventes nettes SUM(-qtemvt) / SUM(-mntmvtttc), agrégation
par (codein, site), fournisseur préféré via LATERAL ... LIMIT 1,
stock agrégé par codein ; pivot en accumulation (+=) par sécurité.
- Analytics (même classe de bug) : pgGetCaByFournisseur et
pgGetCaByNomenclature passent en CA net ; jointure fournisseur
dédupliquée (LATERAL LIMIT 1) pour éviter le double comptage.
- Gestion de stock : pgGetStockNegatif et pgGetSansVente6Mois dédupliquent
le fournisseur préféré (LATERAL LIMIT 1) pour éviter les lignes en double.
Audit complet du mapping par magasin (292/579) côté client : aucun champ
inversé (hit-parade, analytics, dashboard, grid, heatmap). Build OK.
Ajoute un onglet "Cadencier / Alertes" dans la page Commandes auto.
Depuis la liste des fournisseurs, on active un fournisseur en alerte de
commande sur un magasin (292/579) avec une fréquence en X semaines,
gérée indépendamment par magasin.
L'échéance est calculée à partir de la dernière réception réelle
(mvtart, genremvt 1/2, fournisseur principal artfou1.preference=1) +
X semaines. Statut: à commander / bientôt / OK, avec temps restant.
- Table commande_cadences (schema + db-init)
- pgGetDerniereReceptionParFournisseur (mvtart par fournisseur/site)
- Server actions CRUD (list/upsert/toggle/remove)
- UI à onglets + cadencier (ajout, édition intervalle, activer/supprimer)
- Remove unsupported `eslint` key from next.config.ts (Next.js 16)
- Wrap dashboard DB enrichment in try/catch so the dashboard renders
even when PostgreSQL is unavailable, preventing the
"Impossible de charger le tableau de bord" error
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Enrich DashboardTopItem with stock and fournisseur fields via PostgreSQL,
then display them under each article in the dashboard top CA/Qte/Marge tables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Exclude products with a stock entry in the last 6 months (NOT EXISTS subquery)
- Add derniere_entree subquery to SQL (last genremvt 1/2 movement date)
- Display derniere_entree column in table and Excel export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New SQL query pgGetSansVente6Mois returns products with available
stock (qte > 0) whose last sale was over 6 months ago or never.
Third tab added with filtering by supplier, search, sort, and Excel export.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous ::boolean cast caused query failure when suspendu column
is not a native boolean type. Casting to ::text first handles all
column types (char, varchar, int, boolean).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add WHERE NOT COALESCE(af.suspendu::boolean, false) AND NOT COALESCE(a.suspendu::boolean, false)
to pgGetArticlesByFournisseur — products flagged as suspended in artfou1
or articles table are no longer shown in the grid.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of unreliable preference=1, use artfou1.no_id DESC (most recently
added artfou1 entry) to identify the last supplier for each product.
Shows an amber warning icon in the Désignation column when the last
supplier ≠ current supplier view, with tooltip showing who to attribute
the sales to.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>