Qlik: Clear/SelectValues sur le CHAMP Periode, pas sur un objet de session

Le journal complet (enfin lisible) donne la cause, et elle est unique :

    choix de « Periode » impossible :
      {"code":-32601,"parameter":"Clear","message":"Method not found"}
    extraction par date brute interrompue :
      {"code":-32601,"parameter":"Clear","message":"Method not found"}

Clear et SelectValues sont des methodes de CHAMP. Je les appelais sur le
list object « Periode ». Consequence : la cartographie des periodes ET le
chemin d'extraction par date brute n'ont jamais tourne — d'ou la
couverture master annoncee a 0/12 alors que la master vit (total mesure :
11 591 263).

- Handle du champ « Periode » obtenu comme ceux d'Annee et de Date ; le
  list object ne sert plus qu'a ENUMERER les valeurs.
- appliquerPeriode selectionne par texte et ne propage plus d'erreur :
  une periode qu'on n'arrive pas a poser doit degrader le resultat, pas
  interrompre l'extraction.
- Chaque candidat de la cartographie est isole : une valeur en echec est
  notee et ignoree, elle n'emporte plus toute la carte.

Sonde fournisseur : « J009 » laissait 1 207 205 articles visibles, soit
tout le catalogue — la selection ne restreignait rien, et la
« couverture » de 97 % ne faisait que constater que le catalogue contient
nos codes. Une selection qui laisse plus de 3x les codes demandes est
desormais rejetee immediatement (elle coutait 3 x 24 s de lecture).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
Claude committed 2026-08-01 07:00:55 +00:00
1 parent d89d2b3232
commit fee267004f
1 file changed
+80 -45
+80 -45
View File
@@ -345,12 +345,27 @@ export async function fetchNetworkMetricsPlaywright(
let dfh = -1; let dfh = -1;
let fh = -1; let fh = -1;
let yfh = -1; let yfh = -1;
/**
* Handle du CHAMP « Période ».
*
* ⚠️ `Clear` et `SelectValues` sont des méthodes de **champ**. Les
* appeler sur un objet de session renvoie
* `{"code":-32601,"parameter":"Clear","message":"Method not found"}`,
* ce qui a fait avorter en production la cartographie des périodes ET
* le chemin d'extraction par date brute. Le list object ne sert donc
* plus qu'à ÉNUMÉRER les valeurs ; les sélections passent par ici.
*/
let pfh = -1;
const df = await rpc("GetField", { qFieldName: "Date" }, doc); const df = await rpc("GetField", { qFieldName: "Date" }, doc);
dfh = (df.qReturn as { qHandle: number }).qHandle; dfh = (df.qReturn as { qHandle: number }).qHandle;
try { try {
const yf = await rpc("GetField", { qFieldName: "Année" }, doc); const yf = await rpc("GetField", { qFieldName: "Année" }, doc);
yfh = (yf.qReturn as { qHandle: number }).qHandle; yfh = (yf.qReturn as { qHandle: number }).qHandle;
} catch { yfh = -1; } } catch { yfh = -1; }
try {
const pf = await rpc("GetField", { qFieldName: "Période" }, doc);
pfh = (pf.qReturn as { qHandle: number }).qHandle;
} catch { pfh = -1; }
if (codes.length) { if (codes.length) {
const gf = await rpc("GetField", { qFieldName: "Article Code" }, doc); const gf = await rpc("GetField", { qFieldName: "Article Code" }, doc);
fh = (gf.qReturn as { qHandle: number }).qHandle; fh = (gf.qReturn as { qHandle: number }).qHandle;
@@ -898,6 +913,20 @@ export async function fetchNetworkMetricsPlaywright(
} }
const visibles = await lireCodesVisibles(); const visibles = await lireCodesVisibles();
// Un vrai périmètre fournisseur est de l'ordre de grandeur des
// codes demandés. Mesuré en production : « J009 » laissait
// 1 207 205 articles visibles, soit tout le catalogue — la
// sélection n'avait rien restreint, et la « couverture » de 97 %
// ne faisait que constater que le catalogue contient nos codes.
if (visibles.size > attendus.size * 3) {
diag(
" champ « " + champ + " » = « " + fournisseurCode + " » → " + visibles.size +
" articles visibles pour " + attendus.size + " demandés : la sélection ne restreint rien, ignorée",
);
await rpc("Clear", {}, h);
await sleep(qlikSettleMs);
continue;
}
let couverts = 0; let couverts = 0;
for (const c of attendus) if (visibles.has(c)) couverts++; for (const c of attendus) if (visibles.has(c)) couverts++;
const couverture = attendus.size > 0 ? couverts / attendus.size : 0; const couverture = attendus.size > 0 ? couverts / attendus.size : 0;
@@ -917,36 +946,35 @@ export async function fetchNetworkMetricsPlaywright(
return false; return false;
}; };
/**
* Le list object « Période », gardé vivant : le détruire annulerait la
* sélection qu'il porte.
*/
let periodeLo: { qHandle: number } | null = null;
/** Valeur de `Période` à poser avant la passe d'une année donnée. */ /** Valeur de `Période` à poser avant la passe d'une année donnée. */
const periodeParAnnee = new Map<number, { elem: number | null; texte: string }>(); const periodeParAnnee = new Map<number, { texte: string | null }>();
/** Pose (ou efface, si `elem` est nul) la sélection `Période`. */ /**
const appliquerPeriode = async (elem: number | null): Promise<void> => { * Pose (ou efface, si `texte` est nul) la sélection `Période`.
// Effacer doit rester possible même si le list object n'a jamais pu *
// être créé : sinon la `Période` héritée de l'ouverture de l'app * Passe par le HANDLE DU CHAMP, comme pour `Année` et `Date` : c'est la
// resterait posée sans que personne ne l'ait choisie. * seule API qui expose `Clear` et `SelectValues`. Ne propage jamais
if (elem == null) { * d'erreur — une `Période` qu'on n'arrive pas à poser doit dégrader le
await rpc("ClearField", { qFieldName: "Période" }, doc) * résultat, pas interrompre l'extraction (leçon de production : un
.catch(() => (periodeLo ? rpc("Clear", {}, periodeLo.qHandle) : undefined)); * `Clear` en échec a fait avorter deux chemins entiers).
*/
const appliquerPeriode = async (texte: string | null): Promise<boolean> => {
if (pfh === -1) return texte == null;
try {
await rpc("Clear", {}, pfh);
await sleep(qlikSettleMs); await sleep(qlikSettleMs);
return; if (texte == null) return true;
} const sel = await rpc("SelectValues", {
if (!periodeLo) return; qFieldValues: [{ qText: texte }],
{
await rpc("SelectListObjectValues", {
qPath: "/qListObjectDef",
qValues: [elem],
qToggleMode: false, qToggleMode: false,
qSoftLock: true, qSoftLock: true,
}, periodeLo.qHandle); }, pfh);
await sleep(qlikSettleMs);
return sel.qReturn !== false;
} catch (e) {
diag(" sélection Période « " + (texte ?? "(aucune)") + " » impossible : " + String((e as Error)?.message || e));
return false;
} }
await sleep(qlikSettleMs);
}; };
/** /**
@@ -1053,10 +1081,6 @@ export async function fetchNetworkMetricsPlaywright(
* sans année revient à juger « Année à date » sur la seule * sans année revient à juger « Année à date » sur la seule
* année en cours (6/12) et à la rejeter à tort. * année en cours (6/12) et à la rejeter à tort.
*/ */
// Le list object doit survivre : le détruire annulerait la
// sélection qu'il porte. On le garde pour `appliquerPeriode`.
periodeLo = { qHandle: lo.qHandle };
// ─── Une `Période` PAR ANNÉE ───────────────────────────── // ─── Une `Période` PAR ANNÉE ─────────────────────────────
// //
// C'est le point que la version globale manquait : la bonne // C'est le point que la version globale manquait : la bonne
@@ -1082,30 +1106,41 @@ export async function fetchNetworkMetricsPlaywright(
await sleep(qlikSettleMs); await sleep(qlikSettleMs);
} }
const candidats: Array<{ texte: string; elem: number | null }> = [ // `null` = état sans `Période`, candidat comme un autre.
{ texte: "(aucune)", elem: null }, const candidats: Array<string | null> = [null, ...valeurs.map((v) => v.texte)];
...valeurs.map((v) => ({ texte: v.texte, elem: v.elem as number | null })), let meilleure: { texte: string | null; servis: string[] } | null = null;
];
let meilleure: { texte: string; elem: number | null; servis: string[] } | null = null;
for (const c of candidats) { for (const c of candidats) {
await appliquerPeriode(c.elem); const libelle = c ?? "(aucune)";
const mois = await mesurerCouverture(); // Un candidat qui échoue ne doit pas emporter toute la
const servis = mois.fenetre.filter((m) => attendusAnnee.includes(m)); // cartographie : on le note et on passe au suivant.
diag( let servis: string[] = [];
" Période « " + c.texte + " » → sert " + servis.length + "/" + attendusAnnee.length + try {
" mois attendus " + JSON.stringify(servis) + " ; master exposée : " + resume(mois.tous), const pose = await appliquerPeriode(c);
); if (!pose && c !== null) {
diag(" Période « " + libelle + " » → non sélectionnable, ignorée");
continue;
}
const mois = await mesurerCouverture();
servis = mois.fenetre.filter((m) => attendusAnnee.includes(m));
diag(
" Période « " + libelle + " » → sert " + servis.length + "/" + attendusAnnee.length +
" mois attendus " + JSON.stringify(servis) + " ; master exposée : " + resume(mois.tous),
);
} catch (e) {
diag(" Période « " + libelle + " » → erreur " + String((e as Error)?.message || e));
continue;
}
if (!meilleure || servis.length > meilleure.servis.length) { if (!meilleure || servis.length > meilleure.servis.length) {
meilleure = { texte: c.texte, elem: c.elem, servis }; meilleure = { texte: c, servis };
} }
if (servis.length >= attendusAnnee.length) break; // année complète if (servis.length >= attendusAnnee.length) break; // année complète
} }
if (meilleure) { if (meilleure) {
periodeParAnnee.set(annee, { elem: meilleure.elem, texte: meilleure.texte }); periodeParAnnee.set(annee, { texte: meilleure.texte });
for (const m of meilleure.servis) couvertsGlobal.add(m); for (const m of meilleure.servis) couvertsGlobal.add(m);
diag( diag(
" → année " + annee + " sera extraite sous Période « " + meilleure.texte + " » (" + " → année " + annee + " sera extraite sous Période « " + (meilleure.texte ?? "(aucune)") + " » (" +
meilleure.servis.length + "/" + attendusAnnee.length + " mois)", meilleure.servis.length + "/" + attendusAnnee.length + " mois)",
); );
} }
@@ -1762,8 +1797,8 @@ export async function fetchNetworkMetricsPlaywright(
// une année révolue (cf. choisirPeriode). // une année révolue (cf. choisirPeriode).
const periodeAnnee = periodeParAnnee.get(selectYear); const periodeAnnee = periodeParAnnee.get(selectYear);
if (periodeAnnee) { if (periodeAnnee) {
console.log("[qlik-pw] (mois) Période « " + periodeAnnee.texte + " » pour l'année " + selectYear); console.log("[qlik-pw] (mois) Période « " + (periodeAnnee.texte ?? "(aucune)") + " » pour l'année " + selectYear);
await appliquerPeriode(periodeAnnee.elem); await appliquerPeriode(periodeAnnee.texte);
} }
await rpc("Clear", {}, yfh); await rpc("Clear", {}, yfh);