diff --git a/README.md b/README.md
index 4da0b546c..8586fe7a6 100644
--- a/README.md
+++ b/README.md
@@ -385,28 +385,28 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
-> **Version `1.6.2`** · branche `main` · généré le 2026-09-18
+> **Version `1.6.2`** · branche `pr-174` · généré le 2026-09-20
> Seuil de tolérance **5%**
9 corpus |
89 996 DPE analysés |
-59 497 DPE conformes |
-66,11 % réussite globale |
+59 522 DPE conformes |
+66,14 % réussite globale |
| | Corpus | Réussite | | DPE conformes |
| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
| 🔴 | **Généraliste**
`corpus_dpe.csv` | **45,97 %** | `█████████░░░░░░░░░░░` | 4 597 / 10 000 |
-| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,55 %** | `██████████████████░░` | 9 155 / 10 000 |
-| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,14 %** | `██████████████████░░` | 8 812 / 9 998 |
-| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,93 %** | `██████████████████░░` | 8 793 / 10 000 |
-| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,76 %** | `███████████████░░░░░` | 7 375 / 9 999 |
-| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,47 %** | `██████████████░░░░░░` | 6 947 / 10 000 |
-| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,27 %** | `████████████░░░░░░░░` | 6 226 / 9 999 |
-| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,37 %** | `██████████░░░░░░░░░░` | 4 837 / 10 000 |
+| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,62 %** | `██████████████████░░` | 9 162 / 10 000 |
+| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,16 %** | `██████████████████░░` | 8 814 / 9 998 |
+| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,99 %** | `██████████████████░░` | 8 799 / 10 000 |
+| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,78 %** | `███████████████░░░░░` | 7 377 / 9 999 |
+| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,51 %** | `██████████████░░░░░░` | 6 951 / 10 000 |
+| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,30 %** | `████████████░░░░░░░░` | 6 229 / 9 999 |
+| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,38 %** | `██████████░░░░░░░░░░` | 4 838 / 10 000 |
| 🔴 | **Individuel généré depuis l'immeuble (2026)**
`dpe_individuel_a_partir_dpe_immeuble_2026.csv` | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 |
🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %
@@ -418,24 +418,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
-5,61 ms moyenne |
-5,03 ms médiane |
-0,11 ms min |
-705,8 ms max |
+5,23 ms moyenne |
+4,61 ms médiane |
+0,10 ms min |
+577,0 ms max |
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
-| **Généraliste** | 4,50 ms | 4,09 ms | 0,20 ms | 34,8 ms | 8,12 ms | 11,3 ms |
-| **Appartement · chauffage individuel (2025)** | 3,71 ms | 3,46 ms | 1,32 ms | 26,1 ms | 6,22 ms | 7,99 ms |
-| **Logement individuel (2025)** | 5,30 ms | 4,84 ms | 1,52 ms | 42,5 ms | 9,73 ms | 13,2 ms |
-| **Maison individuelle (2025)** | 6,54 ms | 6,02 ms | 1,71 ms | 54,5 ms | 11,6 ms | 16,4 ms |
-| **Immeuble · chauffage individuel** | 7,17 ms | 6,22 ms | 0,11 ms | 137,7 ms | 14,3 ms | 22,3 ms |
-| **Appartement · chauffage collectif (2025)** | 3,98 ms | 3,76 ms | 1,22 ms | 37,6 ms | 6,35 ms | 8,00 ms |
-| **Immeuble · chauffage collectif** | 6,40 ms | 5,58 ms | 0,15 ms | 101,0 ms | 12,1 ms | 21,2 ms |
-| **Immeuble · chauffage mixte** | 6,01 ms | 5,01 ms | 0,22 ms | 705,8 ms | 12,4 ms | 19,8 ms |
-| **Individuel généré depuis l'immeuble (2026)** | 6,89 ms | 6,27 ms | 1,52 ms | 65,1 ms | 13,0 ms | 19,7 ms |
+| **Généraliste** | 4,37 ms | 3,96 ms | 0,22 ms | 35,5 ms | 7,93 ms | 10,8 ms |
+| **Appartement · chauffage individuel (2025)** | 3,49 ms | 3,20 ms | 1,13 ms | 30,2 ms | 6,09 ms | 7,79 ms |
+| **Logement individuel (2025)** | 4,96 ms | 4,38 ms | 1,34 ms | 43,1 ms | 9,48 ms | 12,9 ms |
+| **Maison individuelle (2025)** | 5,88 ms | 5,34 ms | 1,68 ms | 47,0 ms | 10,5 ms | 14,2 ms |
+| **Immeuble · chauffage individuel** | 6,66 ms | 5,73 ms | 0,10 ms | 83,7 ms | 13,6 ms | 21,0 ms |
+| **Appartement · chauffage collectif (2025)** | 4,09 ms | 3,75 ms | 1,26 ms | 37,7 ms | 6,88 ms | 9,51 ms |
+| **Immeuble · chauffage collectif** | 6,04 ms | 5,23 ms | 0,15 ms | 148,9 ms | 11,5 ms | 21,3 ms |
+| **Immeuble · chauffage mixte** | 5,79 ms | 4,77 ms | 0,18 ms | 577,0 ms | 12,4 ms | 20,2 ms |
+| **Individuel généré depuis l'immeuble (2026)** | 5,82 ms | 5,08 ms | 1,65 ms | 71,8 ms | 11,1 ms | 16,9 ms |
La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
diff --git a/dist/reports/corpus/corpus_list_main.json b/dist/reports/corpus/corpus_list_main.json
index 3e15dce40..290ee3339 100644
--- a/dist/reports/corpus/corpus_list_main.json
+++ b/dist/reports/corpus/corpus_list_main.json
@@ -10,5 +10,5 @@
"dpe_appartement_individuel_chauffage_collectif_2025.csv",
"dpe_individuel_a_partir_dpe_immeuble_2026.csv"
],
- "branches": ["main"]
+ "branches": ["main", "pr-174"]
}
diff --git a/docs/CORPUS-HISTORY.md b/docs/CORPUS-HISTORY.md
index 1322d9e8c..266dd71de 100644
--- a/docs/CORPUS-HISTORY.md
+++ b/docs/CORPUS-HISTORY.md
@@ -3,6 +3,122 @@
Une section par génération, la plus récente en premier. Ce fichier est alimenté
automatiquement par `npm run reports:readme`.
+## 1.6.2 — 2026-09-20 (`pr-174`)
+
+> **Version `1.6.2`** · branche `pr-174` · généré le 2026-09-20
+> Seuil de tolérance **5%**
+
+
+
+9 corpus |
+89 996 DPE analysés |
+59 522 DPE conformes |
+66,14 % réussite globale |
+
+
+
+| | Corpus | Réussite | | DPE conformes |
+| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
+| 🔴 | **Généraliste**
`corpus_dpe.csv` | **45,97 %** | `█████████░░░░░░░░░░░` | 4 597 / 10 000 |
+| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,62 %** | `██████████████████░░` | 9 162 / 10 000 |
+| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,16 %** | `██████████████████░░` | 8 814 / 9 998 |
+| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,99 %** | `██████████████████░░` | 8 799 / 10 000 |
+| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,78 %** | `███████████████░░░░░` | 7 377 / 9 999 |
+| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,51 %** | `██████████████░░░░░░` | 6 951 / 10 000 |
+| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,30 %** | `████████████░░░░░░░░` | 6 229 / 9 999 |
+| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,38 %** | `██████████░░░░░░░░░░` | 4 838 / 10 000 |
+| 🔴 | **Individuel généré depuis l'immeuble (2026)**
`dpe_individuel_a_partir_dpe_immeuble_2026.csv` | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 |
+
+🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %
+
+#### Temps d’exécution
+
+> Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs.
+> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
+
+
+
+5,23 ms moyenne |
+4,61 ms médiane |
+0,10 ms min |
+577,0 ms max |
+
+
+
+| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
+| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
+| **Généraliste** | 4,37 ms | 3,96 ms | 0,22 ms | 35,5 ms | 7,93 ms | 10,8 ms |
+| **Appartement · chauffage individuel (2025)** | 3,49 ms | 3,20 ms | 1,13 ms | 30,2 ms | 6,09 ms | 7,79 ms |
+| **Logement individuel (2025)** | 4,96 ms | 4,38 ms | 1,34 ms | 43,1 ms | 9,48 ms | 12,9 ms |
+| **Maison individuelle (2025)** | 5,88 ms | 5,34 ms | 1,68 ms | 47,0 ms | 10,5 ms | 14,2 ms |
+| **Immeuble · chauffage individuel** | 6,66 ms | 5,73 ms | 0,10 ms | 83,7 ms | 13,6 ms | 21,0 ms |
+| **Appartement · chauffage collectif (2025)** | 4,09 ms | 3,75 ms | 1,26 ms | 37,7 ms | 6,88 ms | 9,51 ms |
+| **Immeuble · chauffage collectif** | 6,04 ms | 5,23 ms | 0,15 ms | 148,9 ms | 11,5 ms | 21,3 ms |
+| **Immeuble · chauffage mixte** | 5,79 ms | 4,77 ms | 0,18 ms | 577,0 ms | 12,4 ms | 20,2 ms |
+| **Individuel généré depuis l'immeuble (2026)** | 5,82 ms | 5,08 ms | 1,65 ms | 71,8 ms | 11,1 ms | 16,9 ms |
+
+La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
+dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
+défavorable réel.
+
+## 1.6.2 — 2026-09-18 (`pr-174`)
+
+> **Version `1.6.2`** · branche `pr-174` · généré le 2026-09-18
+> Seuil de tolérance **5%**
+
+
+
+9 corpus |
+89 996 DPE analysés |
+59 492 DPE conformes |
+66,11 % réussite globale |
+
+
+
+| | Corpus | Réussite | | DPE conformes |
+| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
+| 🔴 | **Généraliste**
`corpus_dpe.csv` | **45,87 %** | `█████████░░░░░░░░░░░` | 4 587 / 10 000 |
+| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,62 %** | `██████████████████░░` | 9 162 / 10 000 |
+| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,16 %** | `██████████████████░░` | 8 814 / 9 998 |
+| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,99 %** | `██████████████████░░` | 8 799 / 10 000 |
+| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,68 %** | `███████████████░░░░░` | 7 367 / 9 999 |
+| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,51 %** | `██████████████░░░░░░` | 6 951 / 10 000 |
+| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,03 %** | `████████████░░░░░░░░` | 6 202 / 9 999 |
+| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,55 %** | `██████████░░░░░░░░░░` | 4 855 / 10 000 |
+| 🔴 | **Individuel généré depuis l'immeuble (2026)**
`dpe_individuel_a_partir_dpe_immeuble_2026.csv` | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 |
+
+🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %
+
+#### Temps d’exécution
+
+> Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs.
+> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
+
+
+
+5,31 ms moyenne |
+4,73 ms médiane |
+0,10 ms min |
+626,6 ms max |
+
+
+
+| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
+| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
+| **Généraliste** | 4,41 ms | 4,02 ms | 0,20 ms | 32,9 ms | 7,96 ms | 11,0 ms |
+| **Appartement · chauffage individuel (2025)** | 3,49 ms | 3,21 ms | 1,16 ms | 30,2 ms | 5,94 ms | 7,46 ms |
+| **Logement individuel (2025)** | 5,09 ms | 4,63 ms | 1,34 ms | 41,9 ms | 9,41 ms | 12,6 ms |
+| **Maison individuelle (2025)** | 6,14 ms | 5,68 ms | 1,72 ms | 70,0 ms | 10,8 ms | 14,1 ms |
+| **Immeuble · chauffage individuel** | 6,97 ms | 6,06 ms | 0,10 ms | 107,5 ms | 13,9 ms | 21,5 ms |
+| **Appartement · chauffage collectif (2025)** | 4,01 ms | 3,65 ms | 1,31 ms | 87,4 ms | 6,49 ms | 10,9 ms |
+| **Immeuble · chauffage collectif** | 5,68 ms | 5,01 ms | 0,20 ms | 114,5 ms | 10,4 ms | 18,6 ms |
+| **Immeuble · chauffage mixte** | 5,95 ms | 4,90 ms | 0,18 ms | 626,6 ms | 12,5 ms | 20,3 ms |
+| **Individuel généré depuis l'immeuble (2026)** | 6,01 ms | 5,38 ms | 1,54 ms | 64,5 ms | 11,2 ms | 17,4 ms |
+
+La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
+dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
+défavorable réel.
+
## 1.6.2 — 2026-09-18 (`main`)
> **Version `1.6.2`** · branche `main` · généré le 2026-09-18
diff --git a/src/3.2.1_mur.js b/src/3.2.1_mur.js
index 525bf58ec..51bb16220 100644
--- a/src/3.2.1_mur.js
+++ b/src/3.2.1_mur.js
@@ -122,7 +122,7 @@ function tv_umur(di, de, du, pc_id, zc, effetJoule) {
}
}
-function calc_umur0(di, de, du) {
+function calc_umur0(di, de, du, versionDpe) {
const umur0_avant = du.umur0_avant;
const umur_avant = du.umur_avant;
@@ -166,8 +166,17 @@ function calc_umur0(di, de, du) {
}
}
- // Si présence d'un doublage
- if (type_doublage > 2) {
+ /**
+ * Le doublage NE doit PAS être cumulé à une isolation ITE ou ITI.
+ * Il doit uniquement être pris en compte en l'absence d'isolation ITE/ITI (ex: ITR seule, non isolé, inconnu).
+ * Types avec ITE ou ITI : 3 (iti), 4 (ite), 6 (iti+ite), 7 (iti+itr), 8 (ite+itr)
+ * @see https://github.com/Open3CL/engine/issues/146
+ */
+ const type_isolation = parseInt(de.enum_type_isolation_id) || 1;
+ const hasIteOrIti = versionDpe >= 2.4 && [3, 4, 6, 7, 8].includes(type_isolation);
+
+ // Si présence d'un doublage et pas d'isolation ITE/ITI
+ if (type_doublage > 2 && !hasIteOrIti) {
let umur0Doublage;
// 3 - doublage indéterminé ou lame d'air inf 15 mm
@@ -212,7 +221,7 @@ function calc_umur0(di, de, du) {
di.umur0 = Math.min(2.5, di.umur0);
}
-export default function calc_mur(mur, zc, pc_id, effetJoule) {
+export default function calc_mur(mur, zc, pc_id, effetJoule, versionDpe) {
const de = mur.donnee_entree;
const du = {};
const di = {};
@@ -228,12 +237,12 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) {
const methode_saisie_u = requestInput(de, du, 'methode_saisie_u');
switch (methode_saisie_u) {
case 'non isolé':
- calc_umur0(di, de, du);
+ calc_umur0(di, de, du, versionDpe);
di.umur = Math.min(di.umur0, 2.5);
break;
case 'epaisseur isolation saisie justifiée par mesure ou observation':
case 'epaisseur isolation saisie justifiée à partir des documents justificatifs autorisés': {
- calc_umur0(di, de, du);
+ calc_umur0(di, de, du, versionDpe);
const epaisseurIsolation = requestInput(de, du, 'epaisseur_isolation', 'int') * 0.01;
if (epaisseurIsolation) {
@@ -251,7 +260,7 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) {
}
case "resistance isolation saisie justifiée observation de l'isolant installé et mesure de son épaisseur":
case 'resistance isolation saisie justifiée à partir des documents justificatifs autorisés': {
- calc_umur0(di, de, du);
+ calc_umur0(di, de, du, versionDpe);
const resistanceIsolation = requestInput(de, du, 'resistance_isolation', 'float');
if (resistanceIsolation) {
@@ -268,19 +277,19 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) {
break;
}
case 'isolation inconnue (table forfaitaire)':
- calc_umur0(di, de, du);
+ calc_umur0(di, de, du, versionDpe);
tv_umur(di, de, du, pc_id, zc, effetJoule);
di.umur = Math.min(di.umur, di.umur0);
break;
case "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)": {
- calc_umur0(di, de, du);
+ calc_umur0(di, de, du, versionDpe);
const pi_id = requestInputID(de, du, 'periode_isolation') || pc_id;
tv_umur(di, de, du, pi_id, zc, effetJoule);
di.umur = Math.min(di.umur, di.umur0);
break;
}
case 'année de construction saisie (table forfaitaire)': {
- calc_umur0(di, de, du);
+ calc_umur0(di, de, du, versionDpe);
// Si l'année d'isolation est connue, il faut l'utiliser et pas l'année de construction
let pi_id = de.enum_periode_isolation_id || pc_id;
if (!de.enum_periode_isolation_id) {
diff --git a/src/3.2.1_mur.spec.js b/src/3.2.1_mur.spec.js
index 56ce1f39a..aafb6a96d 100644
--- a/src/3.2.1_mur.spec.js
+++ b/src/3.2.1_mur.spec.js
@@ -819,3 +819,108 @@ describe('calc_mur - méthode de saisie de U inconnue', () => {
warn.mockRestore();
});
});
+
+/**
+ * Le doublage n'est plus cumulé à une isolation ITE ou ITI à partir de la version 2.4 du DPE.
+ * Les DPE antérieurs (2 à 2.3) ont été établis avec le cumul : on conserve ce comportement.
+ * @see https://github.com/Open3CL/engine/issues/146
+ */
+describe('calc_mur - doublage et isolation ITE/ITI (#146)', () => {
+ /** Umur0 issu de la table forfaitaire mockée. */
+ const UMUR0_TABLE = 2;
+ /** Umur0 avec doublage type 4 ou 5 cumulé : 1 / (1/2 + 0,21). */
+ const UMUR0_AVEC_DOUBLAGE = 1.4084507042253522;
+
+ /**
+ * @param enum_type_isolation_id {string}
+ * @param enum_type_doublage_id {string}
+ * @return {object}
+ */
+ const murIsoleAvecDoublage = (enum_type_isolation_id, enum_type_doublage_id = '5') => ({
+ donnee_entree: {
+ methode_saisie_u: 'epaisseur isolation saisie justifiée par mesure ou observation',
+ methode_saisie_u0:
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
+ enum_materiaux_structure_mur_id: '5',
+ epaisseur_structure: 30,
+ epaisseur_isolation: 10,
+ enum_type_doublage_id,
+ enum_type_isolation_id
+ }
+ });
+
+ describe('DPE en version 2.4 et supérieure', () => {
+ test.each([
+ ['iti', '3'],
+ ['ite', '4'],
+ ['iti+ite', '6'],
+ ['iti+itr', '7'],
+ ['ite+itr', '8']
+ ])("isolation %s : le doublage n'est pas cumulé", (_label, typeIsolation) => {
+ const mur = murIsoleAvecDoublage(typeIsolation);
+
+ calc_mur(mur, 'h1a', '1', '0', 2.4);
+
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_TABLE, 9);
+ });
+
+ test.each([
+ ['inconnu', '1'],
+ ['non isolé', '2'],
+ ['itr', '5'],
+ ["isolé mais type d'isolation inconnu", '9']
+ ])('isolation %s : le doublage est cumulé', (_label, typeIsolation) => {
+ const mur = murIsoleAvecDoublage(typeIsolation);
+
+ calc_mur(mur, 'h1a', '1', '0', 2.4);
+
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
+ });
+
+ test('type_isolation absent : le doublage est cumulé', () => {
+ const mur = murIsoleAvecDoublage(undefined);
+
+ calc_mur(mur, 'h1a', '1', '0', 2.5);
+
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
+ });
+ });
+
+ describe('DPE antérieurs à la version 2.4', () => {
+ test.each([[2], [2.1], [2.2], [2.3]])(
+ 'version %s : le doublage reste cumulé à une isolation ITI',
+ (versionDpe) => {
+ const mur = murIsoleAvecDoublage('3');
+
+ calc_mur(mur, 'h1a', '1', '0', versionDpe);
+
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
+ }
+ );
+
+ test('version absente : le doublage reste cumulé à une isolation ITE', () => {
+ const mur = murIsoleAvecDoublage('4');
+
+ calc_mur(mur, 'h1a', '1', '0');
+
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9);
+ });
+ });
+
+ test.each([
+ ['isolation inconnue (table forfaitaire)'],
+ [
+ "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)"
+ ],
+ ['année de construction saisie (table forfaitaire)'],
+ ['resistance isolation saisie justifiée à partir des documents justificatifs autorisés']
+ ])('methode_saisie_u "%s" : la version du DPE est prise en compte', (methode_saisie_u) => {
+ const mur = murIsoleAvecDoublage('3');
+ mur.donnee_entree.methode_saisie_u = methode_saisie_u;
+ mur.donnee_entree.resistance_isolation = 2.5;
+
+ calc_mur(mur, 'h1a', '1', '0', 2.4);
+
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_TABLE, 9);
+ });
+});
diff --git a/src/3_deperdition.js b/src/3_deperdition.js
index 95308c4b9..3ac94b76b 100644
--- a/src/3_deperdition.js
+++ b/src/3_deperdition.js
@@ -224,7 +224,12 @@ export default function calc_deperdition(cg, zc, th, effetJoule, dpe, Sh) {
const declaredPontsThermiques = structuredClone(pt_list);
const vt_list = logement.ventilation_collection.ventilation || [];
- mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule));
+ /**
+ * Version de la méthode appliquée au DPE. Absente des DPE partiels : les calculs qui en
+ * dépendent retombent alors sur le comportement historique.
+ */
+ const versionDpe = parseFloat(dpe.administratif?.enum_version_id);
+ mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule, versionDpe));
pb_list.forEach((pb) => calc_pb(pb, zc, pc, effetJoule, pb_list));
ph_list.forEach((ph) => calc_ph(ph, zc, pc, effetJoule));
bv_list.forEach((bv) => calc_bv(bv, zc));
diff --git a/src/3_deperdition.spec.js b/src/3_deperdition.spec.js
index 349ecf75c..5752fc705 100644
--- a/src/3_deperdition.spec.js
+++ b/src/3_deperdition.spec.js
@@ -125,6 +125,7 @@ describe('Upt - déperdition linéique des ponts thermiques', () => {
/** Fabrique un DPE minimal avec une enveloppe pré-calculée. */
function makeDpe({
+ enum_version_id = '2.4',
mur = [],
pb = [],
ph = [],
@@ -141,6 +142,7 @@ function makeDpe({
}
return {
numero_dpe: 'TEST',
+ administratif: enum_version_id === null ? undefined : { enum_version_id },
logement: {
sortie: { deperdition },
enveloppe: {
@@ -239,6 +241,51 @@ describe("calc_deperdition - agrégation des déperditions de l'enveloppe", () =
expect(calc_ventilation).toHaveBeenCalledTimes(1);
});
+ /**
+ * Certains calculs de Umur dépendent de la version de la méthode appliquée au DPE
+ * (cumul du doublage avec une isolation ITE/ITI avant la version 2.4).
+ * @see https://github.com/Open3CL/engine/issues/146
+ */
+ describe('version du DPE transmise au calcul des murs', () => {
+ /** @return {object[]} */
+ const murCollection = () => [
+ {
+ donnee_entree: { surface_paroi_opaque: 10, enum_type_adjacence_id: '1' },
+ donnee_intermediaire: { umur: 0.5, b: 1 }
+ }
+ ];
+
+ test.each([
+ ['2.3', 2.3],
+ ['2.4', 2.4],
+ ['2.5', 2.5]
+ ])('enum_version_id "%s" transmis à calc_mur', (enum_version_id, expected) => {
+ const dpe = makeDpe({
+ enum_version_id,
+ mur: murCollection(),
+ ventilation: [{ donnee_intermediaire: { hvent: 0, hperm: 0 } }],
+ deperdition_mur: 5
+ });
+
+ calc_deperdition({ enum_periode_construction_id: '1' }, 'h1a', 'th', '0', dpe, 100);
+
+ expect(calc_mur).toHaveBeenCalledWith(expect.anything(), 'h1a', '1', '0', expected);
+ });
+
+ test('bloc administratif absent : calc_mur reçoit NaN et conserve le comportement historique', () => {
+ const dpe = makeDpe({
+ enum_version_id: null,
+ mur: murCollection(),
+ ventilation: [{ donnee_intermediaire: { hvent: 0, hperm: 0 } }],
+ deperdition_mur: 5
+ });
+
+ calc_deperdition({ enum_periode_construction_id: '1' }, 'h1a', 'th', '0', dpe, 100);
+
+ expect(calc_mur).toHaveBeenCalledWith(expect.anything(), 'h1a', '1', '0', NaN);
+ });
+ });
+
/**
* calc_Sdep (surface déperditive transmise à la ventilation) exclut les parois à coefficient
* b nul et les parois adjacentes à un local non déperditif. Les parois à déperdition nulle
diff --git a/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js b/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js
index a03a1d3e2..61c171643 100755
--- a/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js
+++ b/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js
@@ -129,20 +129,29 @@ export class DeperditionMurService extends DeperditionService {
}
/**
- * Pour l’ensemble des parois, la présence d’un doublage apporte une résistance thermique supplémentaire
+ * Pour l’ensemble des parois, la présence d’un doublage apporte une résistance thermique supplémentaire.
+ * Cependant, lorsqu'une isolation ITE ou ITI est présente, le doublage NE doit PAS être cumulé.
+ * Le doublage n'est pris en compte que si aucune isolation ITE/ITI n'est appliquée.
+ * Types d'isolation avec ITE ou ITI : 3 (iti), 4 (ite), 6 (iti+ite), 7 (iti+itr), 8 (ite+itr)
+ * @see https://github.com/Open3CL/engine/issues/146
*/
- switch (murDE.enum_type_doublage_id) {
- case '3':
- // doublage indéterminé ou lame d'air inf 15 mm
- umur0 = 1 / (1 / umur0 + 0.1);
- break;
- case '4': // doublage indéterminé ou lame d'air sup 15 mm
- case '5': // doublage connu (plâtre brique bois)
- umur0 = 1 / (1 / umur0 + 0.21);
- break;
- default:
- // absence de doublage ou inconnu
- break;
+ const typeIsolation = parseInt(murDE.enum_type_isolation_id) || 1;
+ const hasIteOrIti = [3, 4, 6, 7, 8].includes(typeIsolation);
+
+ if (!hasIteOrIti) {
+ switch (murDE.enum_type_doublage_id) {
+ case '3':
+ // doublage indéterminé ou lame d'air inf 15 mm
+ umur0 = 1 / (1 / umur0 + 0.1);
+ break;
+ case '4': // doublage indéterminé ou lame d'air sup 15 mm
+ case '5': // doublage connu (plâtre brique bois)
+ umur0 = 1 / (1 / umur0 + 0.21);
+ break;
+ default:
+ // absence de doublage ou inconnu
+ break;
+ }
}
/**