Relevés utilisés imbriqués dans les méta-périodes : trace d'index légale tirée par Odoo
Contexte
ADR-0027 a posé
GET /facturation/meta-periodes : Odoo tire d'electricore (cœur read-only,
ADR-0012), payload physique enveloppé en JSON, source_hash
par ligne + contract_version. Ce contrat exposait les quantités (énergie par cadran,
TURPE, CTA, taux accise, verdicts jumeaux) mais pas les relevés d'index eux-mêmes —
#282 écrivait même
explicitement « pas de snapshot des index bruts » côté Odoo.
Or deux exigences imposent qu'Odoo dispose des relevés bornants :
- Légale — les index doivent figurer sur la facture (cf. Traçabilité des index,
core/CONTEXT.md). - Opérationnelle — ils doivent persister entre le pull et la génération de la facture, et s'afficher en espace usager.
L'artefact existe déjà : le frame releves_utilises du bundle ContexteMensuel
(Relevés utilisés, #233) —
le sous-ensemble bornant réellement consommé par le calcul d'énergie, avec releve_id,
nature_index et registres réels. Il n'était ni exposé ni persisté. Grill du 19/06/2026.
Décision
-
Bloc imbriqué. Chaque méta-période de
/facturation/meta-periodesporte un tableaureleves_utilises(les relevés bornant ses sous-périodes du mois). Évolution additive,contract_versionbumpée. Endpoint JSON-only → l'imbrication est triviale. -
Nested-only, pas de second endpoint. Le grain méta-période = une
souscription.periodeOdoo ; les relevés sont ses enfants. Un consommateur purement audit lit déjà/releves(le modèle de relevés canonique, tous les relevés). On n'ajoute pas de/releves-utilisesséparé (qui forcerait Odoo à deux pulls + un join). -
Objet relevé =
{ releve_id, date_releve, nature_index, registres index réels }. Registres réels uniquement (jamais de cadran synthétisé) ;nature_index= mention légale ;releve_id= handle de reprise (#191).source/ordre_indexne sont pas exposés (subsumés parreleve_id). Tous les registres réels du compteur sont portés — les 7 cadrans canoniques (base/hp/hcC5 et les 4 quadrantshph/hch/hpb/hcbC4/Tempo), dérivés de la source uniquecadrans.py— seuls les non-nuls ressortant. Note d'implémentation (rc2) : le périmètre C4 a été inclus dès l'origine plutôt que différé, car le mart ne synthétise jamais (pivot_cadransne pose un index que pour les cadrans réellement portés par le flux) — exposer tous les registres non nuls reste donc « réels uniquement », sans cas C5 vs C4. -
Label d'origine de relevé. Chaque relevé exposé porte
origine_releve—périodique(télérelevé R151/R64) ouévénementiel(relevé pris à un événement C15) — et, pour un événementiel,evenement(code C15Nature_Evenementbrut, ex.MCT). Dérivé à l'exposition de lasource+ de l'evenement_declencheur, ce dernier porté nativement par le martrelevesdepuis les relevés C15 (non forward-fillé), au même titre que RSC/FTA/niveau_ouverture_services(#324) : dbt porte l'attribution, le cœur dérive le label. Axe orthogonal ànature_index. -
Invariant « vide ssi incalculable ».
releves_utilisesnon vide ⟺qualite ∈ {réelle, estimée};qualite = incalculable⟹[]. Plein-ou-rien, miroir d'ADR-0033 / ADR-0036 : un mois incalculable n'est pas facturé en réel, donc ne porte aucune paire d'index à imprimer ; la cause est déjà signalée parqualite+statut_communication. Le salvage des sous-périodes valides d'un mois mixte (MCT mid-mois) est différé à #322 — l'exposer plus tard sera un changement additif, pas une régression. -
source_hashétendu au tableau imbriqué (canonicalisation déterministe incluantreleve_id,date_releve,nature_index, registres). Toute dérive d'un index imprimé, de sa nature ou de son identité flippe le déclencheur — même à delta kWh constant (reset compteur, correction ±k aux deux bornes, swap de relevé). C'est ce qui rendsource_hash= déclencheur +releve_id= reprise fidèle à la promesse de régularisation. -
releve_idreformaté en hash court au seam dbt :mint_releve_idpasse de la concaténation verbeuse (source|pdl|timestamp+tz|discriminant) àsubstr(md5(<mêmes composantes>), 1, 16). Les composantes d'identité (source, pdl, date, discriminant) sont inchangées → stabilitéCORR ≡ BRUTet reprise intactes ; reste une clé métier dérivée (ADR-0028). Fait maintenant carreleve_idn'est encore exposé dans aucun contrat (coût aval nul, hors régénération golden + parité). -
Stockage Odoo (délégué) — révise ADR-0027 / #282. Odoo stocke une copie de travail des relevés sur un sous-modèle de
souscription.periode(persiste pull→facture, alimente l'espace usager), rafraîchie à chaque pull et gardée parsource_hash. Le gel légal immuable resteaccount.moveau posting — inchangé. Le cœur reste sans état (ADR-0028) : il ne persiste rien ; la persistance vit dans le système de référence (ADR-0027), pas dans la lib.
Raison
Le besoin légal et l'espace usager imposent qu'Odoo détienne les relevés bornants entre
le pull et la facturation ; le cœur ne peut pas les garder (sans état) ni les ré-fournir à la
demande sans risque de dérive mid-vol. Le grain méta-période est le grain naturel de stockage
(une période Odoo) : imbriquer évite un second flux à corréler. La rejouabilité reste
recalcul + dérive : source_hash (étendu) signale qu'une source a bougé, releve_id
(stable) retrouve exactement le relevé — la détection appartient à Odoo (re-pull + compare),
la lib n'introspecte pas ce qu'elle a émis.
Considered options
- Endpoint séparé
/releves-utilises— rejeté : deux pulls + join pour un modèle qui stocke les relevés en enfants de la période ; l'audit pur a déjà/releves. source_hashsur agrégats seuls — rejeté : aveugle à une dérive d'index absolu à delta constant, alors que l'index absolu est imprimé sur la facture.- Tableau présent-partiel pour les mois incalculables — rejeté : un relevé orphelin est ambigu et non facturable, et casse l'invariant ; le salvage est le territoire de #322.
releve_iden concaténation (statu quo) — rejeté : verbeux (timestamp+tz), pénible à stocker/afficher ; le hash préserve déterminisme et stabilité sans la verbosité.
Conséquences
api/services/meta_periodes_service.py: slicerreleves_utilisespar(ref_situation_contractuelle, fenêtre [debut, fin])et l'attacher ; étendreCOLONNES_CONTRATet la canonicalisationsource_hash; bumpCONTRAT_VERSION.- dbt :
mint_releve_id→ hash ; régénérer golden + test de parité #291 ; les consommateurs internes dereleve_id(bundle) suivent sans changement de sémantique. docs/contrat-meta-periodes.mdétendu (blocreleves_utilises+ invariant).- #282 amendé : ce bloc devient l'endpoint à livrer ; « Odoo stocke une copie de travail des index » remplace « pas de snapshot des index bruts ».
souscriptions_odoo(hors périmètre electricore) : sous-modèle enfant desouscription.periode- rendu espace usager — tracé côté addon.
Statut
Accepté (grill 19/06/2026). Étend ADR-0027,
révise sa clause « pas de snapshot des index » côté Odoo. Glossaire :
core/CONTEXT.md (Traçabilité des index, Relevés
utilisés). Implémentation : issues à créer (/to-issues).