mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 15:08:12 +02:00
fix(qlik): effacer Période puis extraire année par année — la méthode de l'app
Deux remarques de l'utilisateur ont débloqué le sujet : « sur ce tableau j'ai 2024, 2025, 2026 et je sors mois par mois », et « tu mets 2026 et tu as la comparaison N-1 ». Type_Cal='N' ne désigne donc pas « l'année civile en cours » mais L'ANNÉE SÉLECTIONNÉE. Il suffit d'enchaîner les années couvertes par la fenêtre (2025 puis 2026) pour reconstituer 12 mois glissants — chaque passe livrant les 5 mesures, et non la seule quantité comme « Quantité COMP ». Ce mécanisme existait déjà (monthDimPath(yearN - 1)) mais restait sans effet : [qlik-pw] (mois) passe N-1 terminée : 299905 → 299905 points mensuels La sélection « Période », héritée de l'ouverture de l'app et jamais choisie par nous (observé « Période:1/4 » dès le premier diagnostic de la session), épinglait le contexte sur l'année en cours et annulait la sélection d'année. - choisirPeriode() teste désormais EN PREMIER l'état sans aucune sélection de Période — l'état dans lequel un utilisateur voit 2024/2025/2026 — et efface la sélection dès que la couverture est incomplète. - L'orchestration boucle sur les années de la fenêtre au lieu d'une passe principale plus une passe N-1 dérivée de COMP. - Chaque passe n'écrit que les mois DE LA FENÊTRE et les totaux s'additionnent sur ces mois : deux années ne peuvent pas se doubler puisqu'elles ne partagent aucun mois. « Quantité COMP » n'est plus utilisée. - L'agrégation directe des faits reste en secours, et le garde-fou d'intégrité reste actif : sans les 12 mois, le cache n'est pas écrit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
2 files changed
+141
-76
No files matched your search
@@ -103,7 +103,36 @@ Tout ce qu'on observait en découle :
|
|||||||
| Janvier→juillet de l'année précédente renseignés | Ce sont les mois que « Quantité COMP » sait atteindre |
|
| Janvier→juillet de l'année précédente renseignés | Ce sont les mois que « Quantité COMP » sait atteindre |
|
||||||
| `Sum(quantite)` brut inexploitable | Il additionne les lignes N **et** COMP : il double compte |
|
| `Sum(quantite)` brut inexploitable | Il additionne les lignes N **et** COMP : il double compte |
|
||||||
|
|
||||||
### Mesuré : aucune `Période` ne porte 12 mois glissants
|
### La bonne méthode : effacer `Période`, puis extraire ANNÉE par ANNÉE
|
||||||
|
|
||||||
|
C'est ce que fait un utilisateur dans l'app : il sélectionne **2026** et obtient
|
||||||
|
l'année plus sa comparaison N-1 ; il sélectionne 2025 et obtient 2025. Les
|
||||||
|
données de 2024, 2025 et 2026 sont toutes accessibles mois par mois. `Type_Cal='N'`
|
||||||
|
désigne simplement **l'année sélectionnée**.
|
||||||
|
|
||||||
|
Il suffit donc d'enchaîner les années couvertes par la fenêtre (2025 puis 2026)
|
||||||
|
pour reconstituer les 12 mois glissants — chaque passe donnant les 5 mesures, et
|
||||||
|
non la seule quantité comme « Quantité COMP ».
|
||||||
|
|
||||||
|
Ce mécanisme existait déjà dans le code (« passe N-1 ») mais restait **sans
|
||||||
|
effet** :
|
||||||
|
|
||||||
|
```
|
||||||
|
[qlik-pw] (mois) passe N-1 terminée : 299905 → 299905 points mensuels
|
||||||
|
^^^^^^^^^^^^^^ aucun gain
|
||||||
|
```
|
||||||
|
|
||||||
|
La sélection `Période` **héritée de l'ouverture de l'app** (observé
|
||||||
|
« Période:1/4 » alors que rien n'avait été sélectionné) épinglait le contexte sur
|
||||||
|
l'année en cours et annulait la sélection d'année. `choisirPeriode()` teste donc
|
||||||
|
en **premier** l'état sans aucune sélection de `Période`, et l'efface dès que la
|
||||||
|
couverture est incomplète.
|
||||||
|
|
||||||
|
Chaque passe n'écrit que les mois **de la fenêtre**, et les totaux s'additionnent
|
||||||
|
sur ces mois : deux années ne peuvent pas se doubler puisqu'elles ne partagent
|
||||||
|
aucun mois.
|
||||||
|
|
||||||
|
### Mesuré : aucune valeur de `Période` ne porte à elle seule 12 mois
|
||||||
|
|
||||||
`choisirPeriode()` essaie chaque valeur et mesure la couverture réelle sur un
|
`choisirPeriode()` essaie chaque valeur et mesure la couverture réelle sur un
|
||||||
cube `[Mois] × Quantité N`. Résultat en production :
|
cube `[Mois] × Quantité N`. Résultat en production :
|
||||||
|
|||||||
+111
-75
@@ -793,6 +793,48 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
diag("valeurs de « Période » : " + JSON.stringify(valeurs.map((v) => v.texte)));
|
diag("valeurs de « Période » : " + JSON.stringify(valeurs.map((v) => v.texte)));
|
||||||
if (valeurs.length === 0) return 0;
|
if (valeurs.length === 0) return 0;
|
||||||
|
|
||||||
|
/** Couverture réelle : quels mois de la fenêtre « Quantité N » rend-elle vivants ? */
|
||||||
|
const mesurerCouverture = async (): Promise<string[]> => {
|
||||||
|
const sonde = await rpc("CreateSessionObject", { qProp: {
|
||||||
|
qInfo: { qType: "cf-periode-sonde" },
|
||||||
|
qHyperCubeDef: {
|
||||||
|
qDimensions: [{ qLibraryId: moisDimId }],
|
||||||
|
qMeasures: [{ qLibraryId: rQte }],
|
||||||
|
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 2, qHeight: 60 }],
|
||||||
|
},
|
||||||
|
} }, doc);
|
||||||
|
const sRet = sonde.qReturn as { qHandle: number; qGenericId?: string; qId?: string };
|
||||||
|
try {
|
||||||
|
const sl = await rpc("GetLayout", {}, sRet.qHandle);
|
||||||
|
const sm = ((sl.qLayout as { qHyperCube?: { qDataPages?: Array<{ qMatrix?: Array<Array<{ qText?: string; qNum?: number }>> }> } })
|
||||||
|
.qHyperCube?.qDataPages?.[0]?.qMatrix) ?? [];
|
||||||
|
return sm
|
||||||
|
.filter((r) => (Number(r[1]?.qNum) || 0) > 0)
|
||||||
|
.map((r) => normMois(String(r[0]?.qText ?? "")))
|
||||||
|
.filter((m) => fenetre.has(m))
|
||||||
|
.sort();
|
||||||
|
} finally {
|
||||||
|
const sid = String(sRet.qGenericId ?? sRet.qId ?? "");
|
||||||
|
if (sid) { try { await rpc("DestroySessionObject", { qId: sid }, doc); } catch { /* noop */ } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// CANDIDAT ZÉRO : aucune sélection de « Période ».
|
||||||
|
//
|
||||||
|
// L'app pose une valeur par défaut à l'ouverture (observé :
|
||||||
|
// « Période:1/4 » alors que nous n'avions rien sélectionné).
|
||||||
|
// C'est cette valeur héritée qui bornait les mesures — et c'est
|
||||||
|
// sans elle qu'un utilisateur voit 2024, 2025 et 2026 mois par
|
||||||
|
// mois dans l'app. On teste donc l'état libre EN PREMIER.
|
||||||
|
await rpc("ClearField", { qFieldName: "Période" }, doc).catch(() => rpc("Clear", {}, lo!.qHandle));
|
||||||
|
await sleep(qlikSettleMs);
|
||||||
|
const sansSelection = await mesurerCouverture();
|
||||||
|
diag(" SANS sélection de Période → " + sansSelection.length + "/" + fenetre.size + " mois " + JSON.stringify(sansSelection));
|
||||||
|
if (sansSelection.length >= fenetre.size) {
|
||||||
|
diag("Période laissée LIBRE : les mesures couvrent déjà les " + fenetre.size + " mois");
|
||||||
|
return sansSelection.length;
|
||||||
|
}
|
||||||
|
|
||||||
let meilleure: { texte: string; elem: number; couverts: string[] } | null = null;
|
let meilleure: { texte: string; elem: number; couverts: string[] } | null = null;
|
||||||
for (const v of valeurs) {
|
for (const v of valeurs) {
|
||||||
await rpc("SelectListObjectValues", {
|
await rpc("SelectListObjectValues", {
|
||||||
@@ -803,30 +845,8 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
}, lo.qHandle);
|
}, lo.qHandle);
|
||||||
await sleep(qlikSettleMs);
|
await sleep(qlikSettleMs);
|
||||||
|
|
||||||
// Cube [Mois] × « Quantité N » : quels mois cette période rend-elle vivants ?
|
const couverts = await mesurerCouverture();
|
||||||
const sonde = await rpc("CreateSessionObject", { qProp: {
|
diag(" Période « " + v.texte + " » → " + couverts.length + "/" + fenetre.size + " mois de la fenêtre " + JSON.stringify(couverts));
|
||||||
qInfo: { qType: "cf-periode-sonde" },
|
|
||||||
qHyperCubeDef: {
|
|
||||||
qDimensions: [{ qLibraryId: moisDimId }],
|
|
||||||
qMeasures: [{ qLibraryId: rQte }],
|
|
||||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 2, qHeight: 60 }],
|
|
||||||
},
|
|
||||||
} }, doc);
|
|
||||||
const sRet = sonde.qReturn as { qHandle: number; qGenericId?: string; qId?: string };
|
|
||||||
let couverts: string[] = [];
|
|
||||||
try {
|
|
||||||
const sl = await rpc("GetLayout", {}, sRet.qHandle);
|
|
||||||
const sm = ((sl.qLayout as { qHyperCube?: { qDataPages?: Array<{ qMatrix?: Array<Array<{ qText?: string; qNum?: number }>> }> } })
|
|
||||||
.qHyperCube?.qDataPages?.[0]?.qMatrix) ?? [];
|
|
||||||
couverts = sm
|
|
||||||
.filter((r) => (Number(r[1]?.qNum) || 0) > 0)
|
|
||||||
.map((r) => normMois(String(r[0]?.qText ?? "")))
|
|
||||||
.filter((m) => fenetre.has(m));
|
|
||||||
} finally {
|
|
||||||
const sid = String(sRet.qGenericId ?? sRet.qId ?? "");
|
|
||||||
if (sid) { try { await rpc("DestroySessionObject", { qId: sid }, doc); } catch { /* noop */ } }
|
|
||||||
}
|
|
||||||
diag(" Période « " + v.texte + " » → " + couverts.length + "/" + fenetre.size + " mois de la fenêtre " + JSON.stringify(couverts.sort()));
|
|
||||||
if (!meilleure || couverts.length > meilleure.couverts.length) {
|
if (!meilleure || couverts.length > meilleure.couverts.length) {
|
||||||
meilleure = { texte: v.texte, elem: v.elem, couverts };
|
meilleure = { texte: v.texte, elem: v.elem, couverts };
|
||||||
}
|
}
|
||||||
@@ -1144,16 +1164,24 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
// récupérer directement ses mois (seul moyen d'obtenir août→déc N-1, que
|
// récupérer directement ses mois (seul moyen d'obtenir août→déc N-1, que
|
||||||
// COMP ne peut pas fournir puisque l'année N ne va pas jusque-là).
|
// COMP ne peut pas fournir puisque l'année N ne va pas jusque-là).
|
||||||
let loggedSampleRow = false;
|
let loggedSampleRow = false;
|
||||||
const monthDimPath = async (selectYear: number | null): Promise<void> => {
|
const monthDimPath = async (selectYear: number): Promise<void> => {
|
||||||
const isMainPass = selectYear === null;
|
const moisFenetre = new Set(monthlyPayload.map((m) => m.label));
|
||||||
|
|
||||||
/** Traitement d'une ligne du cube [Article Code, Mois] — partagé par les deux chemins. */
|
/**
|
||||||
|
* Traitement d'une ligne du cube [Article Code, Mois].
|
||||||
|
*
|
||||||
|
* Une passe = UNE année explicitement sélectionnée, exactement ce
|
||||||
|
* que fait un utilisateur dans l'app (« je sélectionne 2026 et
|
||||||
|
* j'obtiens N + la comparaison N-1 »). Chaque passe n'écrit que
|
||||||
|
* les mois DE LA FENÊTRE, et les totaux s'additionnent sur ces
|
||||||
|
* mois-là : deux années différentes ne peuvent donc pas se
|
||||||
|
* doubler, puisqu'elles ne partagent aucun mois.
|
||||||
|
*/
|
||||||
const onRowMois = (code: string, mois: string, ca: number, qte: number, nbMag: number, caMag: number, marge: number, qteComp: number) => {
|
const onRowMois = (code: string, mois: string, ca: number, qte: number, nbMag: number, caMag: number, marge: number, qteComp: number) => {
|
||||||
if (!code || code === "-") return;
|
if (!code || code === "-") return;
|
||||||
// Les totaux réseau ne viennent QUE de la passe principale
|
|
||||||
// (sinon on doublerait CA/Qté avec la passe N-1).
|
|
||||||
if (isMainPass) out.push([code, ca, qte, nbMag, caMag, marge]);
|
|
||||||
const mm = normMois(mois);
|
const mm = normMois(mois);
|
||||||
|
if (!moisFenetre.has(mm)) return;
|
||||||
|
out.push([code, ca, qte, nbMag, caMag, marge]);
|
||||||
const y = parseInt(mm.slice(0, 4), 10);
|
const y = parseInt(mm.slice(0, 4), 10);
|
||||||
if (!loggedSampleRow) {
|
if (!loggedSampleRow) {
|
||||||
loggedSampleRow = true;
|
loggedSampleRow = true;
|
||||||
@@ -1166,38 +1194,19 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
// Les 5 mesures du mois courant de la ligne. Le cube les renvoie
|
// Les 5 mesures du mois courant de la ligne. Le cube les renvoie
|
||||||
// déjà toutes : les conserver ne coûte aucune requête Qlik en plus.
|
// déjà toutes : les conserver ne coûte aucune requête Qlik en plus.
|
||||||
const full = { qte, ca, nbMag, caMag, margePct: marge };
|
const full = { qte, ca, nbMag, caMag, margePct: marge };
|
||||||
if (isMainPass && y === yearN) {
|
// « Quantité COMP » n'est plus nécessaire : chaque année de la
|
||||||
// Mois de l'année N + son comparable N-1 (COMP = même mois, N-1).
|
// fenêtre est extraite directement par sa propre passe, ce qui
|
||||||
monthlyByCode[code][mm] = full;
|
// donne les 5 mesures et non la seule quantité.
|
||||||
const mmPrev = String(y - 1) + mm.slice(4); // "2026-03" → "2025-03"
|
void y; void qteComp;
|
||||||
// COMP ne porte QUE la quantité : les autres mesures restent
|
monthlyByCode[code][mm] = full;
|
||||||
// absentes tant que la passe N-1 ne les a pas complétées.
|
|
||||||
// On n'écrit rien si COMP est vide : un mois ABSENT peut être
|
|
||||||
// rattrapé plus tard, un mois à 0 se lit comme « pas de vente »
|
|
||||||
// et masque définitivement le trou d'extraction.
|
|
||||||
if (monthlyByCode[code][mmPrev] === undefined && qteComp > 0) {
|
|
||||||
monthlyByCode[code][mmPrev] = { qte: qteComp };
|
|
||||||
}
|
|
||||||
} else if (!isMainPass) {
|
|
||||||
// Passe année sélectionnée : valeurs directes, prioritaires sur COMP.
|
|
||||||
monthlyByCode[code][mm] = full;
|
|
||||||
} else if (qte > 0 || ca > 0) {
|
|
||||||
// Passe principale, mois HORS année N : « Quantité N » n'est pas
|
|
||||||
// censée le couvrir. Écrire son 0 fabriquerait une absence de
|
|
||||||
// vente là où il n'y a qu'une mesure hors périmètre.
|
|
||||||
monthlyByCode[code][mm] = full;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (yfh !== -1) {
|
if (yfh === -1) throw new Error("champ « Année » introuvable : impossible d'extraire année par année");
|
||||||
await rpc("Clear", {}, yfh);
|
await rpc("Clear", {}, yfh);
|
||||||
await sleep(qlikSettleMs);
|
await sleep(qlikSettleMs);
|
||||||
if (selectYear !== null) {
|
const ysel = await rpc("SelectValues", { qFieldValues: [{ qNum: selectYear, qText: String(selectYear) }], qToggleMode: false, qSoftLock: true }, yfh);
|
||||||
const ysel = await rpc("SelectValues", { qFieldValues: [{ qNum: selectYear, qText: String(selectYear) }], qToggleMode: false, qSoftLock: true }, yfh);
|
console.log("[qlik-pw] (mois) sélection Année=" + selectYear + " → " + JSON.stringify(ysel.qReturn));
|
||||||
console.log("[qlik-pw] (mois) sélection Année=" + selectYear + " → " + JSON.stringify(ysel.qReturn));
|
await sleep(qlikSettleMs);
|
||||||
await sleep(qlikSettleMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const allSerials = monthlyPayload.flatMap((m) => m.serials);
|
const allSerials = monthlyPayload.flatMap((m) => m.serials);
|
||||||
if (dfh !== -1 && allSerials.length) {
|
if (dfh !== -1 && allSerials.length) {
|
||||||
await rpc("Clear", {}, dfh);
|
await rpc("Clear", {}, dfh);
|
||||||
@@ -1405,22 +1414,49 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
return attendus.every((m) => vus.has(m));
|
return attendus.every((m) => vus.has(m));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (couvertureMaster >= attendus.length && monthDim) {
|
// ÉTAPE 2 — extraire ANNÉE PAR ANNÉE, comme dans l'app.
|
||||||
// Une valeur de `Période` couvre les 12 mois : les master
|
//
|
||||||
// measures sont alors exactes sur toute la fenêtre.
|
// Dans l'interface, on sélectionne « 2026 » et on obtient l'année
|
||||||
diag("extraction par master measures (Période couvrant " + couvertureMaster + " mois)");
|
// plus sa comparaison N-1. C'est le mécanisme natif du modèle :
|
||||||
await monthDimPath(null);
|
// `Type_Cal='N'` désigne l'année sélectionnée. Il suffit donc
|
||||||
await pousserCheckpoint("master measures");
|
// d'enchaîner les années couvertes par la fenêtre — ici 2025 puis
|
||||||
} else if (monthDim && useExpr && (await moisParExpressions())) {
|
// 2026 — pour reconstituer les 12 mois glissants.
|
||||||
// Aucune période ne couvre 12 mois : on retombe sur
|
//
|
||||||
// l'agrégation directe des faits.
|
// Ce mécanisme existait déjà (« passe N-1 ») mais restait sans
|
||||||
|
// effet : la sélection `Période` héritée de l'ouverture de l'app
|
||||||
|
// épinglait le contexte sur l'année en cours et annulait la
|
||||||
|
// sélection d'année. `choisirPeriode()` l'a effacée juste avant.
|
||||||
|
const annees = [...new Set(attendus.map((m) => Number(m.slice(0, 4))))].sort();
|
||||||
|
diag("extraction année par année : " + JSON.stringify(annees) + " (Période libre)");
|
||||||
|
|
||||||
|
let extraitParAnnee = false;
|
||||||
|
if (monthDim) {
|
||||||
|
try {
|
||||||
|
for (const annee of annees) {
|
||||||
|
const avant = Object.values(monthlyByCode).reduce((n, m) => n + Object.keys(m).length, 0);
|
||||||
|
await monthDimPath(annee);
|
||||||
|
const apres = Object.values(monthlyByCode).reduce((n, m) => n + Object.keys(m).length, 0);
|
||||||
|
diag(" année " + annee + " → " + (apres - avant) + " points mensuels ajoutés");
|
||||||
|
await pousserCheckpoint("année " + annee);
|
||||||
|
}
|
||||||
|
extraitParAnnee = couvre12();
|
||||||
|
if (!extraitParAnnee) {
|
||||||
|
diag("l'extraction année par année ne couvre pas les 12 mois — essai de l'agrégation directe");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
diag("extraction année par année interrompue : " + String((e as Error)?.message || e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extraitParAnnee) {
|
||||||
|
if (!(monthDim && useExpr && (await moisParExpressions()))) {
|
||||||
|
throw new Error(
|
||||||
|
"extraction 12 mois refusée : ni l'extraction année par année " +
|
||||||
|
"(Période libre, couverture master = " + couvertureMaster + "/" + attendus.length + ") " +
|
||||||
|
"ni l'agrégation directe des faits n'ont couvert la fenêtre",
|
||||||
|
);
|
||||||
|
}
|
||||||
await pousserCheckpoint("expressions vérifiées");
|
await pousserCheckpoint("expressions vérifiées");
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"extraction 12 mois refusée : aucune valeur de « Période » ne couvre la fenêtre " +
|
|
||||||
"(meilleure = " + couvertureMaster + "/" + attendus.length + " mois) et l'agrégation " +
|
|
||||||
"directe des faits n'a pas validé ses contrôles",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!couvre12()) {
|
if (!couvre12()) {
|
||||||
|
|||||||
Reference in new issue
Block a user