mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
fix(qlik): piloter le champ Période du modèle au lieu de lutter contre lui
Le dump des expressions donne enfin la cause de fond :
« Quantité N » = Sum({<Type_Cal={'N'}>} quantite)
« Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}…>} quantite)
« Magasin Ventes Nb N » = Count({<Type_Cal={'N'}>} Distinct ventes_code_site)
L'app n'est pas un modèle « faits + calendrier ». Un pont de périodes duplique
les lignes de faits : Type_Cal='N' marque celles de la période analysée, 'COMP'
celles de la période de comparaison, et le champ Période (4 valeurs, une
sélectionnée) décide de quelle période il s'agit. Tout en découle :
- sélectionner Date ne change rien (mesuré : 100 % du total avant comme après),
le périmètre venant du pont de périodes et non du champ date ;
- « Quantité N » ne couvre que la période courante, d'où août→décembre vides ;
- « Quantité COMP » ne couvre que les mois ayant un comparable, d'où
janvier→juillet de l'année précédente ;
- Sum(quantite) brut additionne les lignes N ET COMP : il double compte, il ne
pouvait pas servir de substitut.
choisirPeriode() essaie donc chaque valeur de Période, mesure sur un cube
[Mois] × Quantité N combien de mois de la fenêtre elle rend réellement
disponibles, et garde la meilleure. Aucun libellé n'est deviné : c'est la
couverture mesurée qui décide, et elle est tracée. Si aucune valeur ne couvre
les 12 mois, les mois manquants restent absents du cache.
Aussi :
- Les mois réellement couverts reçoivent un zéro EXPLICITE quand l'article n'a
pas vendu ; la tendance ne retient que les mois présents comme clés. Un article
vendu depuis mai voyait sa tendance calculée sur deux points (« +4100 % »).
- Les diagnostics du chemin d'extraction sont répétés en fin de sync
([qlik-pw][diag]) : ils étaient au début d'un log de plusieurs milliers de
lignes.
- QLIK_USE_EXPR passe à désactivé par défaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
3 files changed
+249
-46
No files matched your search
@@ -78,6 +78,50 @@ QLIK_FIELD_FOURNISSEUR=<nom exact du champ fournisseur>
|
||||
|
||||
Sans champ libellé exploitable, la recherche fonctionne encore par code centrale.
|
||||
|
||||
## Le modèle est piloté par `Période` / `Type_Cal` — pas par `Date`
|
||||
|
||||
**C'est l'explication de fond**, obtenue en dumpant les expressions réelles :
|
||||
|
||||
```
|
||||
« Quantité N » = Sum({<Type_Cal={'N'}>} quantite)
|
||||
« CA N » = Sum({<Type_Cal={'N'}>} $(vCA_vat))
|
||||
« Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}$(vConstantCOMP)>} quantite)
|
||||
« Magasin Ventes Nb N » = Count({<Type_Cal={'N'}>} Distinct ventes_code_site)
|
||||
```
|
||||
|
||||
L'app n'est pas un modèle « faits + calendrier » classique. Un **pont de périodes**
|
||||
duplique les lignes de faits : `Type_Cal='N'` marque celles de la période
|
||||
analysée, `'COMP'` celles de la période de comparaison. Et c'est le champ
|
||||
**`Période`** (4 valeurs, une sélectionnée) qui décide de quelle période il s'agit.
|
||||
|
||||
Tout ce qu'on observait en découle :
|
||||
|
||||
| Symptôme | Cause |
|
||||
|---|---|
|
||||
| Sélectionner `Date` ne change rien (100 % du total avant/après) | Le périmètre vient du pont de périodes, pas du champ date |
|
||||
| Août→décembre vides sur tous les articles | « Quantité N » ne couvre que la période courante (janvier→juillet) |
|
||||
| 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 |
|
||||
|
||||
La solution n'est donc pas de contourner le modèle mais de **le piloter** :
|
||||
`choisirPeriode()` essaie chaque valeur de `Période`, mesure sur un cube
|
||||
`[Mois] × Quantité N` combien de mois de la fenêtre elle rend réellement
|
||||
disponibles, et garde la meilleure. Aucun libellé n'est deviné — c'est la
|
||||
couverture mesurée qui décide :
|
||||
|
||||
```
|
||||
[qlik-pw][diag] valeurs de « Période » : ["Année en cours","12 mois glissants","Mois","Semaine"]
|
||||
[qlik-pw][diag] Période « Année en cours » → 7/12 mois de la fenêtre [...]
|
||||
[qlik-pw][diag] Période « 12 mois glissants » → 12/12 mois de la fenêtre [...]
|
||||
[qlik-pw][diag] Période retenue : « 12 mois glissants » (12/12 mois)
|
||||
```
|
||||
|
||||
Si aucune valeur ne couvre les 12 mois, l'extraction le dit et les mois manquants
|
||||
restent **absents** du cache — jamais remplis d'une valeur plausible.
|
||||
|
||||
`QLIK_USE_EXPR=1` réactive l'ancien essai par agrégation directe des faits ; il
|
||||
est désactivé par défaut puisque `Sum(quantite)` double compte.
|
||||
|
||||
## ⚠️ Les master measures « N » ignorent la sélection Date
|
||||
|
||||
**Constat de production, vérifié arithmétiquement.** Avec août 2025 seul
|
||||
|
||||
@@ -56,16 +56,18 @@ export const TREND_COLOR: Record<NetworkTrend["direction"], string> = {
|
||||
*
|
||||
* La fenêtre est reconstruite à partir de la date du jour (et non des clés
|
||||
* présentes dans `qteByMonth`) : c'est le seul moyen de garantir une tendance
|
||||
* réellement glissante. Deux conséquences voulues :
|
||||
* - un mois en cours (partiel) ou un mois plus vieux que 12 mois présent dans
|
||||
* le cache est **ignoré** — sinon la pente est faussée par un mois tronqué ;
|
||||
* - un mois de la fenêtre **absent** du cache vaut 0 : côté Qlik, un produit
|
||||
* non vendu sur un mois ne produit tout simplement pas de ligne.
|
||||
* réellement glissante. Un mois en cours (partiel), ou plus vieux que 12 mois,
|
||||
* présent dans le cache est donc **ignoré** — sinon la pente est faussée par un
|
||||
* mois tronqué.
|
||||
*
|
||||
* Seule exception au remplissage par zéro : les mois **antérieurs au premier
|
||||
* mois réellement extrait**. Ils traduisent une extraction plus courte que 12
|
||||
* mois (`QLIK_SYNC_MONTHS_BACK`), pas une absence de ventes — les inventer à 0
|
||||
* simulerait une croissance qui n'existe pas.
|
||||
* Un mois **absent** du cache n'est PAS supposé nul : l'extraction ne couvre pas
|
||||
* toujours les 12 mois (les mesures Qlik « N » ne savent pas remonter au-delà de
|
||||
* l'année en cours). La sync écrit un zéro **explicite** sur chaque mois qu'elle
|
||||
* a réellement couvert sans vente ; la tendance ne retient donc que les mois
|
||||
* présents comme clés, et ignore purement et simplement les autres.
|
||||
*
|
||||
* Sans cette distinction, un article vendu seulement depuis mai voyait sa
|
||||
* tendance calculée sur deux points — « +4 100 % · Forte hausse ».
|
||||
*
|
||||
* Utilise tous les points (robuste au bruit d'un mois isolé). L'indicateur `pct` est la
|
||||
* variation modélisée sur la période (pente × durée) rapportée à la moyenne.
|
||||
@@ -81,11 +83,10 @@ export function computeNetworkTrend(
|
||||
if (presents.length === 0) return empty;
|
||||
|
||||
const fenetre = buildRolling12QlikMonths(now);
|
||||
// Aucun mois extrait ne tombe dans la fenêtre glissante (cache périmé) :
|
||||
// rien de fiable à tracer.
|
||||
const premierMoisExtrait = presents[0];
|
||||
const labels = fenetre.filter((m) => m >= premierMoisExtrait);
|
||||
if (labels.length === 0 || !labels.some((m) => qteByMonth[m] != null)) return empty;
|
||||
// Seuls les mois RÉELLEMENT extraits comptent : ceux que la sync n'a pas
|
||||
// couverts sont absents des clés et sont écartés, plutôt que comptés à zéro.
|
||||
const labels = fenetre.filter((m) => qteByMonth[m] != null);
|
||||
if (labels.length === 0) return empty;
|
||||
|
||||
const values = labels.map((l) => Number(qteByMonth[l]) || 0);
|
||||
const n = values.length;
|
||||
|
||||
+190
-32
@@ -14,7 +14,7 @@
|
||||
|
||||
import "server-only";
|
||||
import { chromium, type Browser } from "playwright-core";
|
||||
import { getQlikConfig, qlikNtlmSession, type QlikConfig, type NetworkMetric } from "@/lib/qlik-client";
|
||||
import { getQlikConfig, qlikNtlmSession, type QlikConfig, type NetworkMetric, type QlikMonthMetrics } from "@/lib/qlik-client";
|
||||
import { buildGridNetworkQlikDateFilter, getMonthRanges, type QlikDateFilter } from "@/lib/qlik-date-range";
|
||||
|
||||
let browserPromise: Promise<Browser> | null = null;
|
||||
@@ -46,7 +46,6 @@ interface InPageResult {
|
||||
monthly?: Record<string, Record<string, { qte: number; ca?: number; nbMag?: number; caMag?: number; margePct?: number }>>;
|
||||
size?: number;
|
||||
error?: string;
|
||||
diag?: Record<string, unknown>;
|
||||
// Compteurs agrégés pour le résumé timing final côté Node (cf. qlik-playwright).
|
||||
months?: number;
|
||||
batches?: number;
|
||||
@@ -54,6 +53,8 @@ interface InPageResult {
|
||||
fallbackCount?: number;
|
||||
/** true si le résultat vient d'un point de contrôle (extraction interrompue). */
|
||||
partiel?: boolean;
|
||||
/** Diagnostics clés du chemin par expressions, répétés en fin de sync. */
|
||||
diagExpr?: string[];
|
||||
}
|
||||
|
||||
function envNumber(name: string, defaultValue: number, min: number, max: number): number {
|
||||
@@ -124,7 +125,11 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// Agrégations directes des champs de faits. Les master measures « N » ignorent
|
||||
// la sélection Date (mesures « cumul année en cours ») : elles ne peuvent pas
|
||||
// produire une fenêtre 12 mois glissants. Surchargeables si le modèle change.
|
||||
const qlikUseExpr = !["0", "false", "no", "off"].includes((process.env.QLIK_USE_EXPR ?? "").trim().toLowerCase());
|
||||
// Désactivé par défaut : `Sum(quantite)` ignore `Type_Cal` et additionne donc
|
||||
// les lignes de la période courante ET celles de la période de comparaison —
|
||||
// il double compte. Les master measures, elles, portent le bon filtre ; il
|
||||
// suffit de caler `Période` (cf. `choisirPeriode`). Réactivable pour essai.
|
||||
const qlikUseExpr = ["1", "true", "yes", "on"].includes((process.env.QLIK_USE_EXPR ?? "").trim().toLowerCase());
|
||||
const qlikExprQte = (process.env.QLIK_EXPR_QTE ?? "Sum(quantite)").trim();
|
||||
const qlikExprCa = (process.env.QLIK_EXPR_CA ?? "Sum(ca_ht)").trim();
|
||||
const qlikExprNbMag = (process.env.QLIK_EXPR_NBMAG ?? "Count(DISTINCT [Magasin Code])").trim();
|
||||
@@ -295,6 +300,11 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// Quantité réseau par code central et par mois (label "YYYY-MM").
|
||||
// Accumulée en parallèle du total, sans toucher l'agrégation existante.
|
||||
const monthlyByCode: Record<string, Record<string, { qte: number; ca?: number; nbMag?: number; caMag?: number; margePct?: number }>> = {};
|
||||
// Diagnostics clés, RÉPÉTÉS en fin de sync : le log d'une sync fait
|
||||
// des milliers de lignes de timings, et ce qui explique un échec se
|
||||
// trouve tout au début — là où personne ne va le chercher.
|
||||
const diagnostics: string[] = [];
|
||||
const diag = (msg: string) => { diagnostics.push(msg); console.log("[qlik-pw][expr] " + msg); };
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -623,6 +633,127 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cale le champ `Période` du modèle sur la fenêtre 12 mois glissants.
|
||||
*
|
||||
* L'app n'est pas un modèle « faits + calendrier » classique : TOUTES
|
||||
* ses mesures sont bornées par un type de calendrier —
|
||||
*
|
||||
* « Quantité N » = Sum({<Type_Cal={'N'}>} quantite)
|
||||
* « Quantité COMP » = Sum({<Type_Cal={'$(vPeriod_comp)'}…>} quantite)
|
||||
*
|
||||
* `Type_Cal` marque les lignes de la période analysée (`N`) et celles
|
||||
* de la période de comparaison (`COMP`), et c'est le champ `Période`
|
||||
* (4 valeurs, une sélectionnée) qui décide de quelle période il s'agit.
|
||||
*
|
||||
* Cela explique tout ce qu'on observait :
|
||||
* - sélectionner `Date` ne change RIEN (le périmètre vient du pont de
|
||||
* périodes, pas du champ date) — mesuré : 100 % du total avant comme
|
||||
* après ;
|
||||
* - « Quantité N » ne renvoie que les mois de la période courante
|
||||
* (janvier→juillet avec « année en cours »), d'où août→décembre
|
||||
* vides ;
|
||||
* - « Quantité COMP » ne couvre que les mois ayant un comparable dans
|
||||
* cette période, d'où janvier→juillet de l'année précédente ;
|
||||
* - `Sum(quantite)` brut mélange les lignes N et COMP : il double
|
||||
* compte, et ne peut pas se substituer aux mesures.
|
||||
*
|
||||
* La solution n'est donc pas de contourner le modèle mais de le
|
||||
* piloter : on essaie chaque valeur de `Période`, on mesure combien de
|
||||
* mois de la fenêtre elle rend réellement disponibles, et on garde la
|
||||
* meilleure. Aucun libellé n'est deviné — c'est la couverture mesurée
|
||||
* qui décide.
|
||||
*/
|
||||
const choisirPeriode = async (): Promise<string | null> => {
|
||||
const fenetre = new Set(monthlyPayload.map((m) => m.label));
|
||||
let lo: { qHandle: number; qGenericId?: string; qId?: string } | null = null;
|
||||
try {
|
||||
const obj = await rpc("CreateSessionObject", { qProp: {
|
||||
qInfo: { qType: "cf-periodes" },
|
||||
qListObjectDef: {
|
||||
qDef: { qFieldDefs: ["Période"] },
|
||||
qInitialDataFetch: [{ qTop: 0, qLeft: 0, qWidth: 1, qHeight: 50 }],
|
||||
},
|
||||
} }, doc);
|
||||
lo = obj.qReturn as { qHandle: number; qGenericId?: string; qId?: string };
|
||||
const lay = await rpc("GetLayout", {}, lo.qHandle);
|
||||
const matrix = ((lay.qLayout as { qListObject?: { qDataPages?: Array<{ qMatrix?: Array<Array<{ qText?: string; qElemNumber?: number }>> }> } })
|
||||
.qListObject?.qDataPages?.[0]?.qMatrix) ?? [];
|
||||
const valeurs = matrix
|
||||
.map((r) => ({ texte: String(r[0]?.qText ?? ""), elem: r[0]?.qElemNumber ?? -1 }))
|
||||
.filter((v) => v.texte && v.elem >= 0);
|
||||
diag("valeurs de « Période » : " + JSON.stringify(valeurs.map((v) => v.texte)));
|
||||
if (valeurs.length === 0) return null;
|
||||
|
||||
let meilleure: { texte: string; elem: number; couverts: string[] } | null = null;
|
||||
for (const v of valeurs) {
|
||||
await rpc("SelectListObjectValues", {
|
||||
qPath: "/qListObjectDef",
|
||||
qValues: [v.elem],
|
||||
qToggleMode: false,
|
||||
qSoftLock: true,
|
||||
}, lo.qHandle);
|
||||
await sleep(qlikSettleMs);
|
||||
|
||||
// Cube [Mois] × « Quantité N » : quels mois cette période rend-elle vivants ?
|
||||
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 };
|
||||
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) {
|
||||
meilleure = { texte: v.texte, elem: v.elem, couverts };
|
||||
}
|
||||
if (couverts.length >= fenetre.size) break; // couverture complète, inutile de continuer
|
||||
}
|
||||
|
||||
if (!meilleure || meilleure.couverts.length === 0) {
|
||||
diag("aucune valeur de « Période » ne couvre la fenêtre — extraction sur la période par défaut");
|
||||
return null;
|
||||
}
|
||||
await rpc("SelectListObjectValues", {
|
||||
qPath: "/qListObjectDef",
|
||||
qValues: [meilleure.elem],
|
||||
qToggleMode: false,
|
||||
qSoftLock: true,
|
||||
}, lo.qHandle);
|
||||
await sleep(qlikSettleMs);
|
||||
diag("Période retenue : « " + meilleure.texte + " » (" + meilleure.couverts.length + "/" + fenetre.size + " mois)");
|
||||
if (meilleure.couverts.length < fenetre.size) {
|
||||
diag(
|
||||
"⚠ couverture incomplète : les mois manquants resteront ABSENTS du cache. " +
|
||||
"Aucune valeur de « Période » ne porte 12 mois glissants dans cette app.",
|
||||
);
|
||||
}
|
||||
return meilleure.texte;
|
||||
} catch (e) {
|
||||
diag("choix de « Période » impossible : " + String((e as Error)?.message || e));
|
||||
return null;
|
||||
} finally {
|
||||
// Le list object est conservé jusqu'au bout : le détruire
|
||||
// annulerait la sélection qu'il porte.
|
||||
void lo;
|
||||
}
|
||||
};
|
||||
|
||||
const moisParExpressions = async (): Promise<boolean> => {
|
||||
const timings: Record<string, number> = {};
|
||||
const marqueOut = out.length;
|
||||
@@ -649,9 +780,9 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// ce soit. Un champ valide doit donner un total non nul ET
|
||||
// strictement inférieur au total sans filtre.
|
||||
const sansFiltre = await totalDeControle();
|
||||
console.log("[qlik-pw][expr] contrôle " + exprQte + " sans filtre date = " + Math.round(sansFiltre));
|
||||
diag("contrôle « " + exprQte + " » sans filtre date = " + Math.round(sansFiltre));
|
||||
if (sansFiltre <= 0) {
|
||||
console.error("[qlik-pw][expr] " + exprQte + " renvoie 0 même sans filtre → expression ou champ invalide (QLIK_EXPR_QTE)");
|
||||
diag("ÉCHEC : « " + exprQte + " » renvoie 0 même sans filtre → expression ou champ invalide (corriger QLIK_EXPR_QTE)");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -661,26 +792,23 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
for (const cand of champsDateCandidats) {
|
||||
const valeurs = /key/i.test(cand) ? allYmd : allSerials;
|
||||
if (!(await selectionnerFenetreSur(cand, valeurs))) {
|
||||
console.log("[qlik-pw][expr] champ « " + cand + " » : inexistant");
|
||||
diag(" champ « " + cand + " » : inexistant");
|
||||
continue;
|
||||
}
|
||||
const filtre = await totalDeControle();
|
||||
const ratio = sansFiltre > 0 ? filtre / sansFiltre : 0;
|
||||
console.log(
|
||||
"[qlik-pw][expr] champ « " + cand + " » → " + Math.round(filtre) +
|
||||
" (" + Math.round(ratio * 100) + "% du total sans filtre)",
|
||||
);
|
||||
diag(" champ « " + cand + " » → " + Math.round(filtre) + " (" + Math.round(ratio * 100) + "% du total sans filtre)");
|
||||
if (filtre > 0 && ratio < 0.995) { champDate = cand; break; }
|
||||
}
|
||||
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.",
|
||||
diag(
|
||||
"ÉCHEC : aucun champ date ne filtre les faits (essayés : " +
|
||||
JSON.stringify(champsDateCandidats) + "). 0% = la sélection ne matche rien, " +
|
||||
"100% = elle n'a aucun effet. Forcer le bon champ avec QLIK_DATE_FIELD.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
console.log("[qlik-pw][expr] champ date retenu : « " + champDate + " »");
|
||||
diag("champ date retenu : « " + champDate + " »");
|
||||
|
||||
// Seuls les mois de la fenêtre nous intéressent ; la dimension
|
||||
// Mois peut en exposer d'autres si une sélection déborde.
|
||||
@@ -698,7 +826,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// Échantillon AVANT filtrage : si le mois ne tombe pas dans
|
||||
// la fenêtre, c'est le format de la dimension Mois qui est
|
||||
// en cause, pas les mesures.
|
||||
console.log("[qlik-pw][expr] échantillon brut: " + JSON.stringify({
|
||||
diag("échantillon brut: " + JSON.stringify({
|
||||
code, moisBrut: mois, moisNormalise: mm, dansLaFenetre: moisFenetre.has(mm),
|
||||
ca, qte, nbMag, marge, "Quantité N (master)": qteMaster,
|
||||
}));
|
||||
@@ -723,15 +851,15 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
|
||||
aggMonths = monthlyPayload.length;
|
||||
aggBatches++;
|
||||
console.log(
|
||||
"[qlik-pw][expr] " + got.value + " lignes (" + horsFenetre + " hors fenêtre), quantité totale=" + Math.round(totalQte) +
|
||||
", calibrage année " + yearN + " : " + calibrOk + "/" + calibrees + " mois conformes à « Quantité N »" +
|
||||
" timings=" + timingSummary(timings),
|
||||
diag(
|
||||
got.value + " lignes (" + horsFenetre + " hors fenêtre), quantité totale=" + Math.round(totalQte) +
|
||||
", calibrage année " + yearN + " : " + calibrOk + "/" + calibrees + " mois conformes à « Quantité N »",
|
||||
);
|
||||
console.log("[qlik-pw][expr] timings=" + timingSummary(timings));
|
||||
|
||||
if (totalQte <= 0) {
|
||||
console.error(
|
||||
"[qlik-pw][expr] quantité totale nulle sur la fenêtre alors que le contrôle global valait " +
|
||||
diag(
|
||||
"ÉCHEC : quantité totale nulle sur la fenêtre alors que le contrôle global valait " +
|
||||
Math.round(sansFiltre) + " — " + horsFenetre + " ligne(s) écartées comme hors fenêtre. " +
|
||||
"Si ce nombre est élevé, c'est le format de la dimension Mois qui ne correspond pas aux libellés attendus.",
|
||||
);
|
||||
@@ -747,7 +875,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("[qlik-pw][expr] échec : " + String((e as Error)?.message || e) + " → repli sur les master measures");
|
||||
diag("ÉCHEC : " + String((e as Error)?.message || e) + " → repli sur les master measures");
|
||||
out.length = marqueOut;
|
||||
for (const k of Object.keys(monthlyByCode)) delete monthlyByCode[k];
|
||||
return false;
|
||||
@@ -995,10 +1123,18 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
};
|
||||
|
||||
if (!noDateMode && monthDim) {
|
||||
// ÉTAPE 1, avant toute extraction : caler le champ `Période` du
|
||||
// modèle sur la fenêtre. Toutes les mesures de l'app étant bornées
|
||||
// par `Type_Cal` (lui-même piloté par `Période`), c'est cette
|
||||
// sélection — et elle seule — qui détermine les mois accessibles.
|
||||
await choisirPeriode();
|
||||
}
|
||||
|
||||
if (!noDateMode && monthDim && useExpr && (await moisParExpressions())) {
|
||||
// Chemin nominal : un seul cube, tous les mois de la fenêtre, des
|
||||
// mesures qui respectent la sélection Date. Ni passe N-1 ni
|
||||
// rattrapage nécessaires — c'est justement ce qu'ils compensaient.
|
||||
// Chemin alternatif : agrégation directe des faits. Conservé
|
||||
// derrière QLIK_USE_EXPR mais désormais secondaire — `Sum(quantite)`
|
||||
// mélange les lignes N et COMP du pont de périodes et double compte.
|
||||
await pousserCheckpoint("expressions");
|
||||
} else if (!noDateMode && monthDim) {
|
||||
// Repli : master measures « année en cours » + passe N-1 + rattrapage.
|
||||
@@ -1147,12 +1283,12 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// mensuel est validé et en production depuis.
|
||||
|
||||
ws.close();
|
||||
resolve({ ok: true, rows: out, monthly: monthlyByCode, size: out.length, months: aggMonths, batches: aggBatches, code15Retries: aggCode15Retries, fallbackCount: aggFallbackCount });
|
||||
resolve({ ok: true, rows: out, monthly: monthlyByCode, size: out.length, months: aggMonths, batches: aggBatches, code15Retries: aggCode15Retries, fallbackCount: aggFallbackCount, diagExpr: diagnostics });
|
||||
} catch (e) {
|
||||
try { ws.close(); } catch { /* noop */ }
|
||||
// On remonte ce qui a été extrait : les codes déjà traités ont
|
||||
// toutes leurs données, seul le reliquat manque.
|
||||
resolve({ ok: false, error: String((e as Error)?.message || e), rows: out, monthly: monthlyByCode, size: out.length });
|
||||
resolve({ ok: false, error: String((e as Error)?.message || e), rows: out, monthly: monthlyByCode, size: out.length, diagExpr: diagnostics });
|
||||
}
|
||||
})();
|
||||
}),
|
||||
@@ -1216,6 +1352,10 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// Les logs détaillés par étape restent en place (timings batch/mois) ;
|
||||
// ici on consolide les compteurs agrégés et la durée totale.
|
||||
const totalMs = Date.now() - totalStart;
|
||||
if (result.diagExpr?.length) {
|
||||
console.log("[qlik-pw][diag] ─── diagnostic du chemin par expressions ───");
|
||||
for (const d of result.diagExpr) console.log("[qlik-pw][diag] " + d);
|
||||
}
|
||||
console.log(
|
||||
`[qlik-pw][summary] rows=${result.size ?? 0} months=${result.months ?? 0} batches=${result.batches ?? 0} code15Retries=${result.code15Retries ?? 0} fallbackCount=${result.fallbackCount ?? 0} totalMs=${totalMs}`,
|
||||
);
|
||||
@@ -1262,15 +1402,33 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// colonne « Tendance / Réseau » de la Grille. Inchangé.
|
||||
// - `metricsByMonth` : toutes les mesures du mois, pour la fiche produit.
|
||||
const monthly = result.monthly ?? {};
|
||||
|
||||
// Mois RÉELLEMENT couverts par l'extraction = union des mois vus sur
|
||||
// l'ensemble des codes. Distinguer « couvert, zéro vente » de « non
|
||||
// extrait » est indispensable : sans cela, un article vendu seulement
|
||||
// depuis mai voyait sa tendance calculée sur deux points (« +4 100 % »),
|
||||
// et un mois non extrait passait pour une absence de vente.
|
||||
const couverts = new Set<string>();
|
||||
for (const parMois of Object.values(monthly)) {
|
||||
for (const mois of Object.keys(parMois)) couverts.add(mois);
|
||||
}
|
||||
const moisCouverts = [...couverts].sort();
|
||||
console.log(`[qlik-pw] mois couverts par l'extraction (${moisCouverts.length}) : ${JSON.stringify(moisCouverts)}`);
|
||||
|
||||
for (const [code, metric] of out) {
|
||||
const mm = monthly[code];
|
||||
if (!mm || Object.keys(mm).length === 0) continue;
|
||||
const mm = monthly[code] ?? {};
|
||||
const qteByMonth: Record<string, number> = {};
|
||||
for (const [mois, v] of Object.entries(mm)) {
|
||||
const metricsByMonth: Record<string, QlikMonthMetrics> = {};
|
||||
for (const mois of moisCouverts) {
|
||||
const v = mm[mois];
|
||||
// Zéro EXPLICITE sur un mois couvert sans vente : c'est une
|
||||
// information, au même titre qu'une vente. Un mois non couvert
|
||||
// reste absent et sera ignoré par la tendance.
|
||||
qteByMonth[mois] = Number(v?.qte) || 0;
|
||||
metricsByMonth[mois] = v ?? { qte: 0 };
|
||||
}
|
||||
metric.qteByMonth = qteByMonth;
|
||||
metric.metricsByMonth = mm;
|
||||
metric.metricsByMonth = metricsByMonth;
|
||||
}
|
||||
// Diagnostic CONCLUSIF : combien de codes ont des mois qui VARIENT vs identiques.
|
||||
// Si presque tous varient → la mesure respecte le mois (feature OK).
|
||||
|
||||
Reference in new issue
Block a user