From cd2e77e9093cd55c479bd91c6114349d9c5c583d Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 31 Jul 2026 12:09:57 +0200 Subject: [PATCH] fix Qlik month dimension selection --- docs/qlik-integration.md | 10 +++- src/lib/qlik-playwright.ts | 120 +++++++++++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/docs/qlik-integration.md b/docs/qlik-integration.md index 3b8fe0b..f9c4664 100644 --- a/docs/qlik-integration.md +++ b/docs/qlik-integration.md @@ -108,6 +108,14 @@ Un seul cube `[Article Code, Mois]` couvre les 12 mois : ni passe N-1 ni rattrapage — c'est exactement ce qu'ils compensaient. Les totaux deviennent la **somme des 12 mois de la fenêtre**. +Sur l'app de production, les champs `Date`, `Date calendrier` et `Date_Key` +existent mais leur sélection ne réduit pas `Sum(quantite)` : ils restent à +100 % du total historique. L'extracteur essaie d'abord ces champs puis utilise +la dimension maître `Mois` comme chemin fiable. Il crée un list object, retrouve +les 12 valeurs `YYYY-MM`, les sélectionne par `qElemNumber`, puis exécute le +cube mensuel. Cette méthode ne dépend ni du champ sous-jacent ni de son format +dual et conserve le chemin rapide (une sélection articles + un cube paginé). + Garde-fous : « Quantité N » est incluse dans le cube pour **calibrage**, et le log compare les deux sur les mois de l'année en cours, seul périmètre où la master measure est juste : @@ -174,7 +182,7 @@ n'a aucun effet. Candidats essayés dans l'ordre : `QLIK_DATE_FIELD` (si défini Si aucun ne filtre, l'extraction échoue explicitement plutôt que de produire une fenêtre fausse. Chaque sélection d'un candidat rejeté est effacée avant l'essai suivant ; autrement un premier candidat à zéro contaminait tous les contrôles -suivants. +suivants. Avant cet échec, la dimension maître `Mois` est testée elle aussi. ## La passe de rattrapage a été supprimée diff --git a/src/lib/qlik-playwright.ts b/src/lib/qlik-playwright.ts index 3a394a0..2d6b433 100644 --- a/src/lib/qlik-playwright.ts +++ b/src/lib/qlik-playwright.ts @@ -631,6 +631,101 @@ export async function fetchNetworkMetricsPlaywright( return h; }; + /** + * Sélectionne les 12 mois via la dimension maître « Mois ». + * + * Dans l'app Magasins Vision Consolidée, les champs Date exposés + * existent mais sont dissociés des faits de vente : leur sélection + * laisse Sum(quantite) à 100 %. La dimension maître utilisée par le + * cube, elle, porte les vraies valeurs associées. Un list object + * permet de sélectionner ses qElemNumber sans connaître le nom du + * champ sous-jacent ni le format dual exact. + */ + const selectionnerFenetreViaDimensionMois = async ( + moisAttendus: Set, + ): Promise<{ total: number; trouves: string[] } | null> => { + let objet: { handle: number; id: string } | null = null; + try { + const cree = await rpcWithRetry("createMonthList", "CreateSessionObject", { + qProp: { + qInfo: { qType: "cf-month-list" }, + qListObjectDef: { + qLibraryId: moisDimId, + qShowAlternatives: true, + qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 1, qHeight: 100 }], + }, + }, + }, doc); + const ret = cree.qReturn as { qHandle: number; qGenericId?: string; qId?: string }; + objet = { + handle: ret.qHandle, + id: String(ret.qGenericId ?? ret.qId ?? ""), + }; + + const layout = await rpcWithRetry("layoutMonthList", "GetLayout", {}, objet.handle); + const lo = (layout.qLayout as { + qListObject?: { + qSize?: { qcy?: number }; + qDataPages?: Array<{ + qArea?: { qTop?: number }; + qMatrix?: Array>; + }>; + }; + }).qListObject; + const size = Number(lo?.qSize?.qcy) || 0; + const cellules: Array<{ qText?: string; qElemNumber?: number }> = []; + for (const page of lo?.qDataPages ?? []) { + for (const row of page.qMatrix ?? []) if (row[0]) cellules.push(row[0]); + } + for (let top = cellules.length; top < size; top += 500) { + const data = await rpcWithRetry("monthListData", "GetListObjectData", { + qPath: "/qListObjectDef", + qPages: [{ qTop: top, qLeft: 0, qWidth: 1, qHeight: Math.min(500, size - top) }], + }, objet.handle); + const matrix = (data.qDataPages as Array<{ + qMatrix?: Array>; + }>)?.[0]?.qMatrix ?? []; + for (const row of matrix) if (row[0]) cellules.push(row[0]); + if (matrix.length === 0) break; + } + + const parMois = new Map(); + for (const cellule of cellules) { + const mois = normMois(String(cellule.qText ?? "").trim()); + const elem = Number(cellule.qElemNumber); + if (moisAttendus.has(mois) && Number.isInteger(elem) && elem >= 0) { + parMois.set(mois, elem); + } + } + const trouves = [...parMois.keys()].sort(); + console.log( + "[qlik-pw][expr] dimension Mois : " + trouves.length + "/" + moisAttendus.size + + " valeurs trouvées " + JSON.stringify(trouves), + ); + if (trouves.length !== moisAttendus.size) return null; + + const selection = await rpcWithRetry("selectMonthList", "SelectListObjectValues", { + qPath: "/qListObjectDef", + qValues: [...parMois.values()], + qToggleMode: false, + qSoftLock: true, + }, objet.handle); + console.log("[qlik-pw][expr] sélection dimension Mois → " + JSON.stringify(selection.qReturn)); + await sleep(qlikSettleMs); + const total = await totalDeControle(); + return { total, trouves }; + } catch (e) { + console.log("[qlik-pw][expr] sélection dimension Mois impossible : " + String((e as Error)?.message || e)); + return null; + } finally { + if (objet?.id) { + try { + await rpc("DestroySessionObject", { qId: objet.id }, doc); + } catch { /* objet limité à la session */ } + } + } + }; + const moisParExpressions = async (): Promise => { const timings: Record = {}; const marqueOut = out.length; @@ -642,11 +737,11 @@ export async function fetchNetworkMetricsPlaywright( if (codes.length && fh !== -1) { await timed("clearCode", "Clear", () => rpc("Clear", {}, fh), timings); await sleep(qlikSettleMs); - await timed("selectCodeAll", "SelectValues", () => rpc("SelectValues", { + await timed("selectCodeAll", "SelectValues", () => rpcWithRetry("selectCodeAll", "SelectValues", { qFieldValues: codes.map((c) => ({ qText: c })), qToggleMode: false, qSoftLock: true, - }, fh), timings); + }, fh, () => { aggCode15Retries++; }), timings); await sleep(qlikSettleMs); } @@ -665,6 +760,7 @@ export async function fetchNetworkMetricsPlaywright( const allSerials = monthlyPayload.flatMap((m) => m.serials); const allYmd = monthlyPayload.flatMap((m) => m.ymd); + const moisFenetre = new Set(monthlyPayload.map((m) => m.label)); let champDate: string | null = null; let totalFenetre = 0; for (const cand of champsDateCandidats) { @@ -690,11 +786,24 @@ export async function fetchNetworkMetricsPlaywright( await rpc("Clear", {}, handleCand); await sleep(qlikSettleMs); } + if (!champDate) { + const selectionMois = await selectionnerFenetreViaDimensionMois(moisFenetre); + const ratio = selectionMois && sansFiltre > 0 ? selectionMois.total / sansFiltre : 0; + if (selectionMois) { + console.log( + "[qlik-pw][expr] dimension « Mois » → " + Math.round(selectionMois.total) + + " (" + Math.round(ratio * 100) + "% du total sans filtre)", + ); + } + if (selectionMois && selectionMois.total > 0 && ratio < 0.995) { + champDate = "dimension Mois"; + totalFenetre = selectionMois.total; + } + } if (!champDate) { console.error( - "[qlik-pw][expr] aucun champ date ne filtre les faits (essayés : " + - JSON.stringify(champsDateCandidats) + ") → impossible de borner la fenêtre 12 mois. " + - "Forcer le bon champ avec QLIK_DATE_FIELD.", + "[qlik-pw][expr] ni les champs date ni la dimension Mois ne filtrent les faits " + + "(champs essayés : " + JSON.stringify(champsDateCandidats) + ") → impossible de borner la fenêtre 12 mois.", ); return false; } @@ -702,7 +811,6 @@ export async function fetchNetworkMetricsPlaywright( // Seuls les mois de la fenêtre nous intéressent ; la dimension // Mois peut en exposer d'autres si une sélection déborde. - const moisFenetre = new Set(monthlyPayload.map((m) => m.label)); let totalQte = 0; let calibrees = 0, calibrOk = 0; let horsFenetre = 0;