@open3cl/engine 1.8.11 → 1.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,10 @@
1
1
  import { bug_for_bug_compat, convertExpression, tv, tvColumnLines } from './utils.js';
2
2
  import enums from './enums.js';
3
3
  import { updateGenerateurBouilleurs } from './13.2_generateur_combustion_bouilleur.js';
4
- import { updateGenerateurChaudieres } from './13.2_generateur_combustion_chaudiere.js';
4
+ import {
5
+ getChaudiereFioulDefautId,
6
+ updateGenerateurChaudieres
7
+ } from './13.2_generateur_combustion_chaudiere.js';
5
8
  import { updateGenerateurPacs } from './13.2_generateur_pac.js';
6
9
  import getFicheTechnique from './ficheTechnique.js';
7
10
  import { evaluateFormula as evaluateTvFormula } from './formula.js';
@@ -128,6 +131,35 @@ export function tv_generateur_combustion(dpe, di, de, type, GV, tbase, methodeSa
128
131
  enumTypeGenerateurId = checkEcsVsChauffageForMixteGeneration(dpe, de, typeGenerateurKey);
129
132
  }
130
133
 
134
+ /**
135
+ * Système collectif par défaut : chaudière fioul standard datant de la construction du bâtiment.
136
+ * Si tv_generateur_combustion_id pointe vers une chaudière fioul compatible avec le système par défaut,
137
+ * la période de la chaudière est déterminée à partir de l'année de construction du bâtiment.
138
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
139
+ */
140
+ if (
141
+ ((type === 'ecs' && enumTypeGenerateurId === '84') ||
142
+ (type === 'ch' && enumTypeGenerateurId === '119')) &&
143
+ de.tv_generateur_combustion_id
144
+ ) {
145
+ const rowDpe = tv('generateur_combustion', {
146
+ tv_generateur_combustion_id: de.tv_generateur_combustion_id
147
+ });
148
+ const chaudiereFioulDefautId = getChaudiereFioulDefautId(
149
+ dpe,
150
+ type,
151
+ rowDpe?.[typeGenerateurKey]?.split('|')[0]
152
+ );
153
+
154
+ if (
155
+ chaudiereFioulDefautId &&
156
+ rowDpe?.[typeGenerateurKey]?.split('|').includes(enumTypeGenerateurId)
157
+ ) {
158
+ enumTypeGenerateurId = chaudiereFioulDefautId;
159
+ de[typeGenerateurKey] = chaudiereFioulDefautId;
160
+ }
161
+ }
162
+
131
163
  /**
132
164
  * Si le type de générateur est
133
165
  * 84 - ECS - système collectif par défaut en abscence d'information : chaudière fioul pénalisante
@@ -43,7 +43,8 @@ vi.mock('./13.2_generateur_combustion_bouilleur.js', () => ({
43
43
  updateGenerateurBouilleurs: vi.fn()
44
44
  }));
45
45
  vi.mock('./13.2_generateur_combustion_chaudiere.js', () => ({
46
- updateGenerateurChaudieres: vi.fn()
46
+ updateGenerateurChaudieres: vi.fn(),
47
+ getChaudiereFioulDefautId: vi.fn()
47
48
  }));
48
49
  vi.mock('./13.2_generateur_pac.js', () => ({
49
50
  updateGenerateurPacs: vi.fn()
@@ -67,7 +68,8 @@ const {
67
68
  const { tv, tvColumnLines } = await import('./utils.js');
68
69
  const { conventionPuissanceNominale } = await import('./13.2_generateur_combustion_pn.js');
69
70
  const { updateGenerateurBouilleurs } = await import('./13.2_generateur_combustion_bouilleur.js');
70
- const { updateGenerateurChaudieres } = await import('./13.2_generateur_combustion_chaudiere.js');
71
+ const { updateGenerateurChaudieres, getChaudiereFioulDefautId } =
72
+ await import('./13.2_generateur_combustion_chaudiere.js');
71
73
  const { updateGenerateurPacs } = await import('./13.2_generateur_pac.js');
72
74
  const { default: getFicheTechnique } = await import('./ficheTechnique.js');
73
75
 
@@ -77,6 +79,7 @@ beforeEach(() => {
77
79
  vi.mocked(tvColumnLines).mockReturnValue([]);
78
80
  vi.mocked(updateGenerateurBouilleurs).mockReset();
79
81
  vi.mocked(updateGenerateurChaudieres).mockReset();
82
+ vi.mocked(getChaudiereFioulDefautId).mockReset();
80
83
  vi.mocked(updateGenerateurPacs).mockReset();
81
84
  vi.mocked(getFicheTechnique).mockReset();
82
85
  vi.mocked(conventionPuissanceNominale).mockReset();
@@ -317,6 +320,87 @@ describe('tv_generateur_combustion - compatibilité bug_for_bug_compat', () => {
317
320
  vi.restoreAllMocks();
318
321
  });
319
322
 
323
+ /**
324
+ * Système collectif par défaut : chaudière fioul standard datant de la construction du bâtiment.
325
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
326
+ */
327
+ describe('système collectif par défaut : chaudière selon l’année de construction', () => {
328
+ test('CH 119 + ligne compatible : type et ligne de la période de construction retenus', () => {
329
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('79');
330
+ tv.mockImplementation((table, matcher) => {
331
+ if (matcher.tv_generateur_combustion_id === '16') {
332
+ return { tv_generateur_combustion_id: '16', enum_type_generateur_ch_id: '75|119' };
333
+ }
334
+ return { ...ROW_DEFAUT, tv_generateur_combustion_id: '20' };
335
+ });
336
+ const dpe = { logement: {} };
337
+ const di = { pn: 20000 };
338
+ const de = {
339
+ enum_type_generateur_ch_id: '119',
340
+ tv_generateur_combustion_id: '16',
341
+ ratio_virtualisation: 1,
342
+ presence_ventouse: 0
343
+ };
344
+ tv_generateur_combustion(dpe, di, de, 'ch', 200, -9, 1);
345
+
346
+ expect(getChaudiereFioulDefautId).toHaveBeenCalledWith(dpe, 'ch', '75');
347
+ expect(de.enum_type_generateur_ch_id).toBe('79');
348
+ // La ligne est recherchée avec le type de la période de construction
349
+ expect(tv).toHaveBeenCalledWith(
350
+ 'generateur_combustion',
351
+ expect.objectContaining({ enum_type_generateur_ch_id: '79' })
352
+ );
353
+ expect(de.tv_generateur_combustion_id).toBe(20);
354
+ });
355
+
356
+ test('ECS 84 + ligne compatible : type de la période de construction retenu', () => {
357
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('39');
358
+ tv.mockImplementation((table, matcher) => {
359
+ if (matcher.tv_generateur_combustion_id === '16') {
360
+ return { tv_generateur_combustion_id: '16', enum_type_generateur_ecs_id: '35|84' };
361
+ }
362
+ return { ...ROW_DEFAUT, tv_generateur_combustion_id: '20' };
363
+ });
364
+ const de = {
365
+ enum_type_generateur_ecs_id: '84',
366
+ tv_generateur_combustion_id: '16',
367
+ ratio_virtualisation: 1,
368
+ presence_ventouse: 0
369
+ };
370
+ tv_generateur_combustion({}, { pn: 20000 }, de, 'ecs', 200, -9, 1);
371
+
372
+ expect(getChaudiereFioulDefautId).toHaveBeenCalledWith({}, 'ecs', '35');
373
+ expect(de.enum_type_generateur_ecs_id).toBe('39');
374
+ expect(de.tv_generateur_combustion_id).toBe(20);
375
+ });
376
+
377
+ test('ligne non compatible avec le système par défaut : type inchangé', () => {
378
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('39');
379
+ tv.mockReturnValue({ ...ROW_DEFAUT, enum_type_generateur_ecs_id: '57|104' });
380
+ const de = {
381
+ enum_type_generateur_ecs_id: '84',
382
+ tv_generateur_combustion_id: '42',
383
+ ratio_virtualisation: 1,
384
+ presence_ventouse: 0
385
+ };
386
+ tv_generateur_combustion({}, { pn: 20000 }, de, 'ecs', 200, -9, 1);
387
+ expect(de.enum_type_generateur_ecs_id).toBe('84');
388
+ });
389
+
390
+ test('année de construction inconnue : type inchangé', () => {
391
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue(undefined);
392
+ tv.mockReturnValue({ ...ROW_DEFAUT, enum_type_generateur_ch_id: '75|119' });
393
+ const de = {
394
+ enum_type_generateur_ch_id: '119',
395
+ tv_generateur_combustion_id: '16',
396
+ ratio_virtualisation: 1,
397
+ presence_ventouse: 0
398
+ };
399
+ tv_generateur_combustion({}, { pn: 20000 }, de, 'ch', 200, -9, 1);
400
+ expect(de.enum_type_generateur_ch_id).toBe('119');
401
+ });
402
+ });
403
+
320
404
  test('type 84 sans tv_generateur_combustion_id : retour au chemin forfaitaire nominal', () => {
321
405
  state.bug = true;
322
406
  tv.mockReturnValue({ ...ROW_DEFAUT });
@@ -1,5 +1,93 @@
1
1
  import getFicheTechnique from './ficheTechnique.js';
2
2
 
3
+ /**
4
+ * Ids des chaudières fioul classiques / standard pour les différentes périodes d'installation
5
+ * (seuil = première année de la période)
6
+ */
7
+ const CHAUDIERES_FIOUL_ECS = {
8
+ 1948: '35',
9
+ 1970: '36',
10
+ 1976: '37',
11
+ 1981: '38',
12
+ 1991: '39',
13
+ 2015: '40'
14
+ };
15
+
16
+ const CHAUDIERES_FIOUL_CH = {
17
+ 1948: '75',
18
+ 1970: '76',
19
+ 1976: '77',
20
+ 1981: '78',
21
+ 1991: '79',
22
+ 2015: '80'
23
+ };
24
+
25
+ /**
26
+ * Retourne l'identifiant associé au plus grand seuil inférieur ou égal à l'année donnée.
27
+ * Pour une année antérieure au premier seuil, l'identifiant du premier seuil est retenu.
28
+ * @param values {Record<string, string>}
29
+ * @param annee {number}
30
+ * @returns {string}
31
+ */
32
+ export function findIdForAnnee(values, annee) {
33
+ const entries = Object.entries(values).sort((a, b) => b[0] - a[0]);
34
+
35
+ for (const [threshold, value] of entries) {
36
+ if (annee >= Number(threshold)) {
37
+ return value;
38
+ }
39
+ }
40
+
41
+ return entries[entries.length - 1][1];
42
+ }
43
+
44
+ /**
45
+ * Système collectif par défaut en absence d'information : chaudière fioul pénalisante
46
+ * (enum_type_generateur_ch_id = 119 / enum_type_generateur_ecs_id = 84)
47
+ *
48
+ * "En présence d'une installation de production collective de chauffage et d'ECS, si aucune information n'est
49
+ * communiquée sur les équipements collectifs, un calcul par défaut se fera avec une chaudière atmosphérique mixte
50
+ * standard datant de la construction du bâtiment. L'énergie utilisée par le système sera du fioul."
51
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
52
+ *
53
+ * Retourne l'identifiant de la chaudière fioul dont la période correspond à l'année de construction du bâtiment,
54
+ * ou undefined si l'année de construction n'est pas connue.
55
+ *
56
+ * Si la ligne tv_generateur_combustion_id saisie correspond à une chaudière fioul dont la période est
57
+ * postérieure ou égale à celle de la construction (chaudière remplacée), l'information est connue et
58
+ * doit être utilisée : "Dans le cas où certaines de ces informations sont connues sur l'installation
59
+ * collective, elles pourront être utilisées" (§17.2.1.1). Seule une période saisie antérieure à la
60
+ * construction, non crédible, est redressée.
61
+ *
62
+ * @param dpe {FullDpe}
63
+ * @param type {'ch' | 'ecs'}
64
+ * @param typeSaisiId {string|undefined} type de chaudière correspondant à la ligne tv saisie
65
+ * @returns {string|undefined}
66
+ */
67
+ export function getChaudiereFioulDefautId(dpe, type, typeSaisiId) {
68
+ const anneeConstruction = parseInt(
69
+ dpe?.logement?.caracteristique_generale?.annee_construction,
70
+ 10
71
+ );
72
+
73
+ if (Number.isNaN(anneeConstruction)) {
74
+ return undefined;
75
+ }
76
+
77
+ const chaudieres = type === 'ecs' ? CHAUDIERES_FIOUL_ECS : CHAUDIERES_FIOUL_CH;
78
+ const chaudiereConstructionId = findIdForAnnee(chaudieres, anneeConstruction);
79
+ const ids = Object.values(chaudieres);
80
+
81
+ if (
82
+ ids.includes(typeSaisiId) &&
83
+ ids.indexOf(typeSaisiId) >= ids.indexOf(chaudiereConstructionId)
84
+ ) {
85
+ return typeSaisiId;
86
+ }
87
+
88
+ return chaudiereConstructionId;
89
+ }
90
+
3
91
  /**
4
92
  * Pour les générateurs "Autre système à combustion", les calculs sont faits comme pour les chaudières standard
5
93
  * @param dpe {FullDpe}
@@ -27,14 +115,7 @@ export function updateGenerateurChaudieres(dpe, de, type) {
27
115
  2001: '49',
28
116
  2015: '50'
29
117
  },
30
- 79: {
31
- 1948: '35',
32
- 1970: '36',
33
- 1976: '37',
34
- 1981: '38',
35
- 1991: '39',
36
- 2015: '40'
37
- },
118
+ 79: CHAUDIERES_FIOUL_ECS,
38
119
  80: {
39
120
  1948: '15',
40
121
  1978: '16',
@@ -44,14 +125,7 @@ export function updateGenerateurChaudieres(dpe, de, type) {
44
125
  2018: '20',
45
126
  2019: '21'
46
127
  },
47
- 81: {
48
- 1948: '35',
49
- 1970: '36',
50
- 1976: '37',
51
- 1981: '38',
52
- 1991: '39',
53
- 2015: '40'
54
- }
128
+ 81: CHAUDIERES_FIOUL_ECS
55
129
  };
56
130
  } else {
57
131
  /**
@@ -71,14 +145,7 @@ export function updateGenerateurChaudieres(dpe, de, type) {
71
145
  2001: '89',
72
146
  2015: '90'
73
147
  },
74
- 114: {
75
- 1948: '75',
76
- 1970: '76',
77
- 1976: '77',
78
- 1981: '78',
79
- 1991: '79',
80
- 2015: '80'
81
- },
148
+ 114: CHAUDIERES_FIOUL_CH,
82
149
  115: {
83
150
  1948: '55',
84
151
  1978: '56',
@@ -88,14 +155,7 @@ export function updateGenerateurChaudieres(dpe, de, type) {
88
155
  2018: '60',
89
156
  2019: '61'
90
157
  },
91
- 116: {
92
- 1948: '75',
93
- 1970: '76',
94
- 1976: '77',
95
- 1981: '78',
96
- 1991: '79',
97
- 2015: '80'
98
- }
158
+ 116: CHAUDIERES_FIOUL_CH
99
159
  };
100
160
  }
101
161
 
@@ -9,7 +9,8 @@ vi.mock('./ficheTechnique.js', () => ({
9
9
  default: vi.fn()
10
10
  }));
11
11
 
12
- const { updateGenerateurChaudieres } = await import('./13.2_generateur_combustion_chaudiere.js');
12
+ const { updateGenerateurChaudieres, findIdForAnnee, getChaudiereFioulDefautId } =
13
+ await import('./13.2_generateur_combustion_chaudiere.js');
13
14
  const { default: getFicheTechnique } = await import('./ficheTechnique.js');
14
15
 
15
16
  beforeEach(() => {
@@ -83,3 +84,84 @@ describe('updateGenerateurChaudieres - substitution par une chaudière équivale
83
84
  ]);
84
85
  });
85
86
  });
87
+
88
+ describe('findIdForAnnee - sélection de la période', () => {
89
+ const values = { 1948: 'a', 1970: 'b', 1991: 'c' };
90
+
91
+ test('année égale à un seuil : période du seuil retenue', () => {
92
+ expect(findIdForAnnee(values, 1970)).toBe('b');
93
+ });
94
+
95
+ test('année entre deux seuils : période du seuil inférieur retenue', () => {
96
+ expect(findIdForAnnee(values, 1990)).toBe('b');
97
+ });
98
+
99
+ test('année postérieure au dernier seuil : période la plus récente', () => {
100
+ expect(findIdForAnnee(values, 2030)).toBe('c');
101
+ });
102
+
103
+ test('année antérieure au premier seuil : période la plus ancienne', () => {
104
+ expect(findIdForAnnee(values, 1900)).toBe('a');
105
+ });
106
+ });
107
+
108
+ /**
109
+ * Système collectif par défaut (ch 119 / ecs 84) : chaudière atmosphérique mixte standard
110
+ * datant de la construction du bâtiment, énergie fioul.
111
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
112
+ */
113
+ describe('getChaudiereFioulDefautId - chaudière fioul par défaut selon l’année de construction', () => {
114
+ const dpe = (annee) => ({
115
+ logement: { caracteristique_generale: { annee_construction: annee } }
116
+ });
117
+
118
+ test.each([
119
+ [1900, '75', '35'],
120
+ [1969, '75', '35'],
121
+ [1970, '76', '36'],
122
+ [1975, '76', '36'],
123
+ [1976, '77', '37'],
124
+ [1981, '78', '38'],
125
+ [1990, '78', '38'],
126
+ [1991, '79', '39'],
127
+ [2014, '79', '39'],
128
+ [2015, '80', '40'],
129
+ [2024, '80', '40']
130
+ ])('construction %s : chaudière ch %s / ecs %s', (annee, chId, ecsId) => {
131
+ expect(getChaudiereFioulDefautId(dpe(annee), 'ch')).toBe(chId);
132
+ expect(getChaudiereFioulDefautId(dpe(annee), 'ecs')).toBe(ecsId);
133
+ });
134
+
135
+ test('année de construction sous forme de chaîne : prise en compte', () => {
136
+ expect(getChaudiereFioulDefautId(dpe('1995'), 'ch')).toBe('79');
137
+ });
138
+
139
+ /**
140
+ * "Dans le cas où certaines de ces informations sont connues sur l'installation collective, elles
141
+ * pourront être utilisées et complétées par les valeurs par défaut" (§17.2.1.1).
142
+ * Une chaudière saisie d'une période postérieure ou égale à la construction (chaudière remplacée)
143
+ * est conservée ; une période antérieure à la construction, non crédible, est redressée.
144
+ */
145
+ test.each([
146
+ // [annee, type saisi, attendu, type]
147
+ [1930, '77', '77', 'ch'], // 2162E1020576I : chaudière 1976-1980 dans un bâtiment de 1930
148
+ [1970, '78', '78', 'ch'], // 2262E0012158B : chaudière 1981-1990 dans un bâtiment de 1970
149
+ [1930, '39', '39', 'ecs'], // 2262E0038009I : chaudière 1991-2014 dans un bâtiment de 1930
150
+ [1995, '79', '79', 'ch'], // même période que la construction
151
+ [1996, '75', '79', 'ch'], // 2368E2119912U : chaudière < 1970 dans un bâtiment de 1996
152
+ [2000, '36', '39', 'ecs'], // chaudière 1970-1975 dans un bâtiment de 2000
153
+ [1996, '99', '79', 'ch'], // type saisi hors chaudières fioul standard
154
+ [1996, undefined, '79', 'ch']
155
+ ])(
156
+ 'construction %s, type saisi %s : chaudière %s retenue (%s)',
157
+ (annee, typeSaisi, attendu, type) => {
158
+ expect(getChaudiereFioulDefautId(dpe(annee), type, typeSaisi)).toBe(attendu);
159
+ }
160
+ );
161
+
162
+ test('année de construction absente : aucune chaudière par défaut', () => {
163
+ expect(getChaudiereFioulDefautId(dpe(undefined), 'ch')).toBeUndefined();
164
+ expect(getChaudiereFioulDefautId({}, 'ecs')).toBeUndefined();
165
+ expect(getChaudiereFioulDefautId(undefined, 'ecs')).toBeUndefined();
166
+ });
167
+ });
@@ -15,6 +15,7 @@ import {
15
15
  import { conso_aux_gen } from './15_conso_aux.js';
16
16
  import { scopOrCop } from './12.4_pac.js';
17
17
  import getFicheTechnique from './ficheTechnique.js';
18
+ import { getChaudiereFioulDefautId } from './13.2_generateur_combustion_chaudiere.js';
18
19
 
19
20
  function tv_pertes_stockage(di, de, VsCollectif) {
20
21
  let vb;
@@ -257,10 +258,19 @@ export default function calc_gen_ecs(dpe, gen_ecs, ecs_di, ecs_de, GV, ca_id, zc
257
258
  });
258
259
 
259
260
  if (row) {
260
- // On prend par défaut le premier type de générateur pour effectuer les calculs de rendement
261
261
  const typeGenerateurEcs = row.enum_type_generateur_ecs_id?.split('|');
262
-
263
- if (typeGenerateurEcs && typeGenerateurEcs.length) {
262
+ // Chaudière fioul standard datant de la construction du bâtiment (§17.2.1.1)
263
+ const chaudiereFioulDefautId = getChaudiereFioulDefautId(
264
+ dpe,
265
+ 'ecs',
266
+ typeGenerateurEcs?.[0]
267
+ );
268
+
269
+ if (chaudiereFioulDefautId && typeGenerateurEcs?.includes('84')) {
270
+ // La chaudière fioul par défaut dépend de l'année de construction du bâtiment
271
+ de.enum_type_generateur_ecs_id = chaudiereFioulDefautId;
272
+ } else if (typeGenerateurEcs && typeGenerateurEcs.length) {
273
+ // On prend par défaut le premier type de générateur pour effectuer les calculs de rendement
264
274
  de.enum_type_generateur_ecs_id = typeGenerateurEcs[0];
265
275
  }
266
276
  }
@@ -61,6 +61,10 @@ vi.mock('./ficheTechnique.js', () => ({
61
61
  default: vi.fn()
62
62
  }));
63
63
 
64
+ vi.mock('./13.2_generateur_combustion_chaudiere.js', () => ({
65
+ getChaudiereFioulDefautId: vi.fn()
66
+ }));
67
+
64
68
  const {
65
69
  default: calc_gen_ecs,
66
70
  rg_chauffe_eau_gaz,
@@ -73,6 +77,7 @@ const { tvColumnIDs, requestInput, requestInputID, tv, getVolumeStockageFromDesc
73
77
  const { tv_generateur_combustion } = await import('./13.2_generateur_combustion.js');
74
78
  const { scopOrCop } = await import('./12.4_pac.js');
75
79
  const { default: getFicheTechnique } = await import('./ficheTechnique.js');
80
+ const { getChaudiereFioulDefautId } = await import('./13.2_generateur_combustion_chaudiere.js');
76
81
 
77
82
  /**
78
83
  * 14. Générateur d'ECS : rendements et consommations
@@ -632,6 +637,41 @@ describe('calc_gen_ecs - consommation par générateur', () => {
632
637
  expect(g.donnee_intermediaire.conso_ecs).toBeCloseTo(37.03703703703703, 9);
633
638
  });
634
639
 
640
+ /**
641
+ * Type 84 : chaudière fioul standard datant de la construction du bâtiment.
642
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
643
+ */
644
+ test('type 84 combustion compatible 84 : chaudière de la période de construction retenue', () => {
645
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('39');
646
+ vi.mocked(tv).mockReturnValue({ enum_type_generateur_ecs_id: '35|84' });
647
+ const dpe = { logement: {} };
648
+ const g = gen({
649
+ type_energie: 'fioul domestique',
650
+ enum_type_generateur_ecs_id: '84',
651
+ tv_generateur_combustion_id: '16',
652
+ enum_methode_saisie_carac_sys_id: '1'
653
+ });
654
+ calc_gen_ecs(dpe, g, ecs_di, ecs_de, 0, '1', '1', 'immeuble');
655
+
656
+ expect(getChaudiereFioulDefautId).toHaveBeenCalledWith(dpe, 'ecs', '35');
657
+ // 39 au lieu du premier type de la ligne (35)
658
+ expect(g.donnee_entree.enum_type_generateur_ecs_id).toBe('39');
659
+ });
660
+
661
+ test('type 84 combustion non compatible 84 (générateur décrit) : premier type conservé', () => {
662
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('39');
663
+ vi.mocked(tv).mockReturnValue({ enum_type_generateur_ecs_id: '57|104' });
664
+ const g = gen({
665
+ type_energie: 'gaz naturel',
666
+ enum_type_generateur_ecs_id: '84',
667
+ tv_generateur_combustion_id: '13',
668
+ enum_methode_saisie_carac_sys_id: '1'
669
+ });
670
+ calc_gen_ecs({}, g, ecs_di, ecs_de, 0, '1', '1', 'immeuble');
671
+
672
+ expect(g.donnee_entree.enum_type_generateur_ecs_id).toBe('57');
673
+ });
674
+
635
675
  /**
636
676
  * Réseau de chaleur : redressement de reseau_distribution_isole (bug_for_bug_compat).
637
677
  */
@@ -213,6 +213,28 @@ function tv_k(pt_di, di, de, du, pc_id, logement) {
213
213
  return;
214
214
  }
215
215
 
216
+ /**
217
+ * 3.4 Calcul des déperditions par les ponts thermiques (issue #212)
218
+ * « Les ponts thermiques des parois au niveau des circulations communes ne sont pas pris en compte. »
219
+ * La règle porte sur les parois : elle s'applique au plancher de la liaison comme au mur.
220
+ * 14 à 18 - Circulations communes / halls d'entrée
221
+ * 22 - Local non déperditif (local à usage d'habitation chauffé)
222
+ *
223
+ * Ambiguïté : la méthode ne tranche pas explicitement le cas du plancher en adjacence 22 et les moteurs
224
+ * certifiés divergent (3cl-2024.6.1.0 exporte k = 0, Tribu 1.4.25.1 et BBS 2024.6.1.0 conservent la
225
+ * valeur forfaitaire sur les « planchers mitoyens »). Comme pour le mur ci-dessus, on s'appuie sur la
226
+ * valeur du DPE : k = 0 si le DPE a retenu k = 0, sinon la valeur forfaitaire est recalculée.
227
+ */
228
+ if (
229
+ ['14', '15', '16', '17', '18', '22'].includes(
230
+ String(plancher.donnee_entree.enum_type_adjacence_id)
231
+ ) &&
232
+ pt_di.k === 0
233
+ ) {
234
+ di.k = 0;
235
+ return;
236
+ }
237
+
216
238
  /**
217
239
  * 3.4.3 Plancher haut / mur
218
240
  * Les ponts thermiques des planchers haut en structure légère sont négligés.
@@ -889,3 +889,131 @@ describe('calc_pont_thermique - cas limites', () => {
889
889
  expect(pt.donnee_intermediaire.k).toBeUndefined();
890
890
  });
891
891
  });
892
+
893
+ /**
894
+ * Issue #212 : l'exclusion des parois non déperditives s'applique aussi au plancher des liaisons
895
+ * plancher bas / mur et plancher haut lourd / mur, pas seulement au mur.
896
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4
897
+ */
898
+ describe('calc_pont_thermique - plancher sur paroi non déperditive (issue #212)', () => {
899
+ /** Mur extérieur lourd isolé ITI + plancher lourd isolé ITE avec l'adjacence donnée. */
900
+ function liaison(type_liaison, adjacencePlancher, k = 0.9) {
901
+ const mur = [
902
+ {
903
+ donnee_entree: { reference: 'M1', enum_type_adjacence_id: '1', type_isolation: 'iti' },
904
+ donnee_utilisateur: {}
905
+ }
906
+ ];
907
+ const plancher = {
908
+ donnee_entree: {
909
+ reference: 'P1',
910
+ enum_type_adjacence_id: adjacencePlancher,
911
+ enum_type_plancher_bas_id: '1',
912
+ enum_type_plancher_haut_id: '1',
913
+ type_isolation: 'ite'
914
+ },
915
+ donnee_utilisateur: {}
916
+ };
917
+ const pt = pontThermique(
918
+ {
919
+ type_liaison,
920
+ enum_type_liaison_id: type_liaison === 'plancher bas / mur' ? '1' : '3',
921
+ reference_1: 'P1',
922
+ reference_2: 'M1',
923
+ description: 'plancher / mur'
924
+ },
925
+ k
926
+ );
927
+ return { mur, plancher, pt };
928
+ }
929
+
930
+ test('cas 2557E3382433Y : plancher bas sur local habitation chauffé (22), mur extérieur, k DPE nul : k = 0', () => {
931
+ const { mur, plancher, pt } = liaison('plancher bas / mur', '22', 0);
932
+
933
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
934
+
935
+ expect(pt.donnee_intermediaire.k).toBe(0);
936
+ expect(tv).not.toHaveBeenCalled();
937
+ });
938
+
939
+ test('plancher mitoyen (22) avec k DPE non nul (Tribu / BBS) : valeur forfaitaire recalculée', () => {
940
+ tv.mockReturnValue({ k: '0.71', tv_pont_thermique_id: '12' });
941
+ const { mur, plancher, pt } = liaison('plancher bas / mur', '22', 0.71);
942
+
943
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
944
+
945
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
946
+ enum_type_liaison_id: '1',
947
+ isolation_mur: '^iti$',
948
+ isolation_plancher: '^ite$'
949
+ });
950
+ expect(pt.donnee_intermediaire.k).toBe(0.71);
951
+ });
952
+
953
+ test('cas 2534E3387900I : plancher haut lourd sous local habitation chauffé (22), mur extérieur : k = 0', () => {
954
+ const { mur, plancher, pt } = liaison('plancher haut lourd / mur', '22', 0);
955
+
956
+ calc_pont_thermique(pt, '1', logement({ mur, ph: [plancher] }));
957
+
958
+ expect(pt.donnee_intermediaire.k).toBe(0);
959
+ expect(tv).not.toHaveBeenCalled();
960
+ });
961
+
962
+ test.each(['14', '15', '16', '17', '18'])(
963
+ 'plancher bas sur circulation commune / hall (%s) avec k DPE nul : k = 0 sans accès table',
964
+ (adjacence) => {
965
+ const { mur, plancher, pt } = liaison('plancher bas / mur', adjacence, 0);
966
+
967
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
968
+
969
+ expect(pt.donnee_intermediaire.k).toBe(0);
970
+ expect(tv).not.toHaveBeenCalled();
971
+ }
972
+ );
973
+
974
+ test('plancher haut lourd sous circulation (14) avec k DPE nul : k = 0', () => {
975
+ const { mur, plancher, pt } = liaison('plancher haut lourd / mur', '14', 0);
976
+
977
+ calc_pont_thermique(pt, '1', logement({ mur, ph: [plancher] }));
978
+
979
+ expect(pt.donnee_intermediaire.k).toBe(0);
980
+ expect(tv).not.toHaveBeenCalled();
981
+ });
982
+
983
+ test('plancher bas sur hall (17) avec k DPE non nul : valeur forfaitaire conservée (même règle que pour le mur)', () => {
984
+ tv.mockReturnValue({ k: '0.71', tv_pont_thermique_id: '12' });
985
+ const { mur, plancher, pt } = liaison('plancher bas / mur', '17', 0.71);
986
+
987
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
988
+
989
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
990
+ enum_type_liaison_id: '1',
991
+ isolation_mur: '^iti$',
992
+ isolation_plancher: '^ite$'
993
+ });
994
+ expect(pt.donnee_intermediaire.k).toBe(0.71);
995
+ });
996
+
997
+ test('contre-test : plancher bas sur sous-sol non chauffé (6) : pont thermique compté (pas de b sur les PT)', () => {
998
+ tv.mockReturnValue({ k: '0.71', tv_pont_thermique_id: '12' });
999
+ const { mur, plancher, pt } = liaison('plancher bas / mur', '6', 0);
1000
+
1001
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
1002
+
1003
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
1004
+ enum_type_liaison_id: '1',
1005
+ isolation_mur: '^iti$',
1006
+ isolation_plancher: '^ite$'
1007
+ });
1008
+ expect(pt.donnee_intermediaire.k).toBe(0.71);
1009
+ });
1010
+
1011
+ test('contre-test : plancher bas sur local tertiaire (20) : pont thermique compté', () => {
1012
+ tv.mockReturnValue({ k: '0.71', tv_pont_thermique_id: '12' });
1013
+ const { mur, plancher, pt } = liaison('plancher bas / mur', '20', 0.71);
1014
+
1015
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
1016
+
1017
+ expect(pt.donnee_intermediaire.k).toBe(0.71);
1018
+ });
1019
+ });
@@ -5,6 +5,7 @@ import { conso_ch } from './9_conso_ch.js';
5
5
  import { calc_generateur_combustion_ch } from './13.2_generateur_combustion_ch.js';
6
6
  import { scopOrCop } from './12.4_pac.js';
7
7
  import { updateGenerateurCombustion } from './13.2_generateur_combustion.js';
8
+ import { getChaudiereFioulDefautId } from './13.2_generateur_combustion_chaudiere.js';
8
9
 
9
10
  const ENUM_MATERIAUX_STRUCTURE_MUR_ANCIEN_IDS = ['2', '3', '4', '6', '8', '9', '14', '21'];
10
11
  const ENUM_CLASSES_INERTIE_LOURDES_IDS = ['1', '2'];
@@ -161,10 +162,15 @@ export function checkForGeneratorType(dpe, de, di, du) {
161
162
  });
162
163
 
163
164
  if (row) {
164
- // On prend par défaut le premier type de générateur pour effectuer les calculs de rendement
165
165
  const typeGenerateurCh = row.enum_type_generateur_ch_id?.split('|');
166
-
167
- if (typeGenerateurCh && typeGenerateurCh.length) {
166
+ // Chaudière fioul standard datant de la construction du bâtiment (§17.2.1.1)
167
+ const chaudiereFioulDefautId = getChaudiereFioulDefautId(dpe, 'ch', typeGenerateurCh?.[0]);
168
+
169
+ if (chaudiereFioulDefautId && typeGenerateurCh?.includes('119')) {
170
+ // La chaudière fioul par défaut dépend de l'année de construction du bâtiment
171
+ de.enum_type_generateur_ch_id = chaudiereFioulDefautId;
172
+ } else if (typeGenerateurCh && typeGenerateurCh.length) {
173
+ // On prend par défaut le premier type de générateur pour effectuer les calculs de rendement
168
174
  de.enum_type_generateur_ch_id = typeGenerateurCh[0];
169
175
  }
170
176
  }
@@ -57,6 +57,10 @@ vi.mock('./13.2_generateur_combustion.js', () => ({
57
57
  updateGenerateurCombustion: vi.fn()
58
58
  }));
59
59
 
60
+ vi.mock('./13.2_generateur_combustion_chaudiere.js', () => ({
61
+ getChaudiereFioulDefautId: vi.fn()
62
+ }));
63
+
60
64
  const {
61
65
  calc_Qrec_gen_j,
62
66
  calc_Qrec_gen_ecs_j,
@@ -72,6 +76,7 @@ const { conso_ch } = await import('./9_conso_ch.js');
72
76
  const { calc_generateur_combustion_ch } = await import('./13.2_generateur_combustion_ch.js');
73
77
  const { scopOrCop } = await import('./12.4_pac.js');
74
78
  const { updateGenerateurCombustion } = await import('./13.2_generateur_combustion.js');
79
+ const { getChaudiereFioulDefautId } = await import('./13.2_generateur_combustion_chaudiere.js');
75
80
 
76
81
  beforeEach(() => {
77
82
  vi.mocked(requestInput).mockReset();
@@ -335,6 +340,47 @@ describe('checkForGeneratorType - classification du générateur', () => {
335
340
  checkForGeneratorType({}, de, {}, du);
336
341
  expect(de.enum_type_generateur_ch_id).toBe('119');
337
342
  });
343
+
344
+ /**
345
+ * Chaudière fioul par défaut : chaudière standard datant de la construction du bâtiment.
346
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
347
+ */
348
+ describe('cas 119 : chaudière fioul par défaut selon l’année de construction', () => {
349
+ beforeEach(() => {
350
+ vi.mocked(getChaudiereFioulDefautId).mockReset();
351
+ vi.mocked(requestInputID).mockReturnValue('119');
352
+ });
353
+
354
+ test('ligne combustion compatible 119 : chaudière de la période de construction retenue', () => {
355
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('79');
356
+ vi.mocked(tv).mockReturnValue({ enum_type_generateur_ch_id: '75|119' });
357
+ const dpe = { logement: {} };
358
+ const de = { tv_generateur_combustion_id: 16 };
359
+ const du = {};
360
+ checkForGeneratorType(dpe, de, {}, du);
361
+
362
+ expect(getChaudiereFioulDefautId).toHaveBeenCalledWith(dpe, 'ch', '75');
363
+ // 79 au lieu du premier type de la ligne (75)
364
+ expect(de.enum_type_generateur_ch_id).toBe('79');
365
+ expect(du.isCombustionGenerator).toBe(true);
366
+ });
367
+
368
+ test('ligne combustion non compatible 119 (générateur décrit) : premier type conservé', () => {
369
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue('79');
370
+ vi.mocked(tv).mockReturnValue({ enum_type_generateur_ch_id: '97|139' });
371
+ const de = { tv_generateur_combustion_id: 13 };
372
+ checkForGeneratorType({}, de, {}, {});
373
+ expect(de.enum_type_generateur_ch_id).toBe('97');
374
+ });
375
+
376
+ test('année de construction inconnue : premier type de la ligne conservé', () => {
377
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue(undefined);
378
+ vi.mocked(tv).mockReturnValue({ enum_type_generateur_ch_id: '75|119' });
379
+ const de = { tv_generateur_combustion_id: 16 };
380
+ checkForGeneratorType({}, de, {}, {});
381
+ expect(de.enum_type_generateur_ch_id).toBe('75');
382
+ });
383
+ });
338
384
  });
339
385
 
340
386
  /**
package/README.md CHANGED
@@ -370,27 +370,27 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
370
370
 
371
371
  <!-- CORPUS:START -->
372
372
 
373
- > **Version `1.8.8`** · à publier · branche `main` · généré le 2026-10-02
373
+ > **Version `1.8.12`** · à publier · branche `main` · généré le 2026-10-02
374
374
  > Seuil de tolérance **5%**
375
375
 
376
376
  <table>
377
377
  <tr>
378
378
  <td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
379
379
  <td align="center"><strong>89 990</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>59 879</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>66,54 %</strong><br/><sub>réussite globale</sub></td>
380
+ <td align="center"><strong>59 886</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>66,55 %</strong><br/><sub>réussite globale</sub></td>
382
382
  </tr>
383
383
  </table>
384
384
 
385
385
  | | Corpus | Réussite | | DPE conformes |
386
386
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,28 %** | `█████████░░░░░░░░░░░` | 4 628 / 10 000 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,31 %** | `█████████░░░░░░░░░░░` | 4 631 / 10 000 |
388
388
  | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **91,80 %** | `██████████████████░░` | 9 180 / 10 000 |
389
389
  | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **88,70 %** | `██████████████████░░` | 8 863 / 9 992 |
390
390
  | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 840 / 10 000 |
391
391
  | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **74,14 %** | `███████████████░░░░░` | 7 413 / 9 999 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **70,61 %** | `██████████████░░░░░░` | 7 061 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,50 %** | `████████████░░░░░░░░` | 6 249 / 9 999 |
392
+ | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **70,63 %** | `██████████████░░░░░░` | 7 063 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **62,52 %** | `█████████████░░░░░░░` | 6 251 / 9 999 |
394
394
  | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **49,06 %** | `██████████░░░░░░░░░░` | 4 906 / 10 000 |
395
395
  | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,39 %** | `█████░░░░░░░░░░░░░░░` | 2 739 / 10 000 |
396
396
 
@@ -403,24 +403,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>5,31 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,65 ms</strong><br/><sub>médiane</sub></td>
408
- <td align="center"><strong>0,12 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>893,4 ms</strong><br/><sub>max</sub></td>
406
+ <td align="center"><strong>5,49 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,81 ms</strong><br/><sub>médiane</sub></td>
408
+ <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
409
+ <td align="center"><strong>842,5 ms</strong><br/><sub>max</sub></td>
410
410
  </tr>
411
411
  </table>
412
412
 
413
413
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
414
414
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
415
- | **Généraliste** | 4,56 ms | 4,09 ms | 0,21 ms | 37,2 ms | 8,41 ms | 12,0 ms |
416
- | **Appartement · chauffage individuel (2025)** | 4,01 ms | 3,65 ms | 1,47 ms | 33,8 ms | 6,95 ms | 9,38 ms |
417
- | **Logement individuel (2025)** | 4,98 ms | 4,43 ms | 1,62 ms | 38,0 ms | 9,30 ms | 12,8 ms |
418
- | **Maison individuelle (2025)** | 5,92 ms | 5,33 ms | 1,85 ms | 40,3 ms | 10,6 ms | 13,7 ms |
419
- | **Immeuble · chauffage individuel** | 6,70 ms | 5,72 ms | 0,13 ms | 136,8 ms | 13,4 ms | 22,2 ms |
420
- | **Appartement · chauffage collectif (2025)** | 4,60 ms | 4,36 ms | 1,46 ms | 37,2 ms | 7,27 ms | 9,45 ms |
421
- | **Immeuble · chauffage collectif** | 5,31 ms | 4,38 ms | 0,12 ms | 99,5 ms | 9,95 ms | 17,7 ms |
422
- | **Immeuble · chauffage mixte** | 5,38 ms | 4,34 ms | 0,20 ms | 893,4 ms | 11,2 ms | 18,5 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 6,36 ms | 5,53 ms | 1,67 ms | 50,2 ms | 12,4 ms | 19,1 ms |
415
+ | **Généraliste** | 4,60 ms | 4,18 ms | 0,22 ms | 34,3 ms | 8,29 ms | 11,3 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 3,94 ms | 3,52 ms | 1,41 ms | 29,4 ms | 6,94 ms | 10,8 ms |
417
+ | **Logement individuel (2025)** | 5,12 ms | 4,56 ms | 1,59 ms | 38,6 ms | 9,60 ms | 13,1 ms |
418
+ | **Maison individuelle (2025)** | 6,45 ms | 5,83 ms | 1,90 ms | 41,6 ms | 11,7 ms | 15,6 ms |
419
+ | **Immeuble · chauffage individuel** | 6,64 ms | 5,67 ms | 0,11 ms | 74,6 ms | 13,5 ms | 21,9 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,74 ms | 4,39 ms | 1,57 ms | 34,6 ms | 7,84 ms | 11,9 ms |
421
+ | **Immeuble · chauffage collectif** | 5,91 ms | 5,13 ms | 0,14 ms | 145,5 ms | 11,0 ms | 19,2 ms |
422
+ | **Immeuble · chauffage mixte** | 5,97 ms | 4,83 ms | 0,21 ms | 842,5 ms | 12,5 ms | 21,1 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,01 ms | 5,13 ms | 1,68 ms | 57,9 ms | 11,4 ms | 17,9 ms |
424
424
 
425
425
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
426
426
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.11';
36
+ const LIB_VERSION = '1.8.13';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
@@ -175,7 +175,10 @@ export class DeperditionPontThermiqueService extends DeperditionService {
175
175
  return this.defaultValue(pontThermiqueDE);
176
176
  }
177
177
 
178
- if (!this.plancherBasHasPontThermique(plancherBasDE)) {
178
+ if (
179
+ !this.plancherHasPontThermiqueAdjacence(plancherBasDE) ||
180
+ !this.plancherBasHasPontThermique(plancherBasDE)
181
+ ) {
179
182
  return 0;
180
183
  }
181
184
 
@@ -206,7 +209,10 @@ export class DeperditionPontThermiqueService extends DeperditionService {
206
209
  return this.defaultValue(pontThermiqueDE);
207
210
  }
208
211
 
209
- if (!this.plancherHautHasPontThermique(plancherHautDE)) {
212
+ if (
213
+ !this.plancherHasPontThermiqueAdjacence(plancherHautDE) ||
214
+ !this.plancherHautHasPontThermique(plancherHautDE)
215
+ ) {
210
216
  return 0;
211
217
  }
212
218
 
@@ -391,6 +397,26 @@ export class DeperditionPontThermiqueService extends DeperditionService {
391
397
  return true;
392
398
  }
393
399
 
400
+ /**
401
+ * Retourne true si l'adjacence du plancher (bas ou haut) permet un pont thermique avec le mur (issue #212)
402
+ *
403
+ * 3.4 Calcul des déperditions par les ponts thermiques
404
+ * Les ponts thermiques des parois au niveau des circulations communes ne sont pas pris en compte.
405
+ * La règle porte sur les parois en général : elle s'applique au plancher de la liaison comme au mur.
406
+ * 14 - Circulation sans ouverture directe sur l'extérieur
407
+ * 15 - Circulation avec ouverture directe sur l'extérieur
408
+ * 16 - Circulation avec bouche ou gaine de désenfumage ouverte en permanence
409
+ * 17 - Hall d'entrée avec dispositif de fermeture automatique
410
+ * 18 - Hall d'entrée sans dispositif de fermeture automatique
411
+ * 22 - Local non déperditif (local à usage d'habitation chauffé) : le plancher n'est pas déperditif
412
+ *
413
+ * @param plancherDE {PlancherBasDE|PlancherHautDE}
414
+ * @return {boolean}
415
+ */
416
+ plancherHasPontThermiqueAdjacence(plancherDE) {
417
+ return ![14, 15, 16, 17, 18, 22].includes(parseInt(plancherDE.enum_type_adjacence_id));
418
+ }
419
+
394
420
  /**
395
421
  * Retourne true si le plancher haut est concerné par un pont thermique
396
422
  * @param plancherHautDE {PlancherHautDE}
@@ -0,0 +1,125 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { DeperditionPontThermiqueService } from './deperdition-pont-thermique.service.js';
3
+
4
+ /**
5
+ * Tests unitaires isolés de `DeperditionPontThermiqueService` (nouvelle architecture).
6
+ *
7
+ * Le store des tables de valeurs et les services de déperdition des parois sont remplacés par des
8
+ * doubles de test : seule la logique propre au service (exclusions de ponts thermiques) est vérifiée.
9
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4
10
+ */
11
+
12
+ let tvStore;
13
+ let murService;
14
+ let plancherHautService;
15
+ let plancherBasService;
16
+
17
+ /** @type {DeperditionPontThermiqueService} */
18
+ let service;
19
+
20
+ const ctx = {};
21
+
22
+ beforeEach(() => {
23
+ tvStore = {
24
+ getKForPlancher: vi.fn(() => 0.71),
25
+ getKForMur: vi.fn(() => 0.82),
26
+ getKForMenuiserie: vi.fn(() => 0.3),
27
+ getKForMurById: vi.fn(() => 0.5)
28
+ };
29
+ murService = { typeIsolation: vi.fn(() => 3) };
30
+ plancherHautService = { typeIsolation: vi.fn(() => 4) };
31
+ plancherBasService = { typeIsolation: vi.fn(() => 4) };
32
+ service = new DeperditionPontThermiqueService(
33
+ tvStore,
34
+ murService,
35
+ plancherHautService,
36
+ plancherBasService
37
+ );
38
+ });
39
+
40
+ /** Enveloppe : mur extérieur lourd + plancher (bas ou haut) lourd avec l'adjacence donnée. */
41
+ function enveloppe(adjacencePlancher, adjacenceMur = '1') {
42
+ const plancher = {
43
+ donnee_entree: {
44
+ reference: 'P1',
45
+ enum_type_adjacence_id: adjacencePlancher,
46
+ enum_type_plancher_bas_id: '1',
47
+ enum_type_plancher_haut_id: '1'
48
+ }
49
+ };
50
+ return {
51
+ mur_collection: {
52
+ mur: [
53
+ {
54
+ donnee_entree: {
55
+ reference: 'M1',
56
+ enum_type_adjacence_id: adjacenceMur,
57
+ enum_materiaux_structure_mur_id: '1'
58
+ }
59
+ }
60
+ ]
61
+ },
62
+ plancher_bas_collection: { plancher_bas: [plancher] },
63
+ plancher_haut_collection: { plancher_haut: [plancher] }
64
+ };
65
+ }
66
+
67
+ function pontThermique(enumTypeLiaisonId) {
68
+ return {
69
+ enum_methode_saisie_pont_thermique_id: '1',
70
+ enum_type_liaison_id: enumTypeLiaisonId,
71
+ reference_1: 'P1',
72
+ reference_2: 'M1',
73
+ description: 'plancher / mur'
74
+ };
75
+ }
76
+
77
+ describe('DeperditionPontThermiqueService - adjacence du plancher (issue #212)', () => {
78
+ test.each(['14', '15', '16', '17', '18', '22'])(
79
+ 'adjacence %s : plancher sans pont thermique',
80
+ (adjacence) => {
81
+ expect(service.plancherHasPontThermiqueAdjacence({ enum_type_adjacence_id: adjacence })).toBe(
82
+ false
83
+ );
84
+ }
85
+ );
86
+
87
+ test.each(['1', '3', '5', '6', '7', '20'])(
88
+ 'adjacence %s : plancher avec pont thermique',
89
+ (adj) => {
90
+ expect(service.plancherHasPontThermiqueAdjacence({ enum_type_adjacence_id: adj })).toBe(true);
91
+ }
92
+ );
93
+
94
+ test('cas 2557E3382433Y : plancher bas / mur, plancher sur local habitation chauffé (22) : k = 0', () => {
95
+ const di = service.execute(ctx, enveloppe('22'), pontThermique('1'));
96
+ expect(di.k).toBe(0);
97
+ expect(tvStore.getKForPlancher).not.toHaveBeenCalled();
98
+ });
99
+
100
+ test('cas 2534E3387900I : plancher haut lourd / mur, plafond sous local habitation chauffé (22) : k = 0', () => {
101
+ const di = service.execute(ctx, enveloppe('22'), pontThermique('3'));
102
+ expect(di.k).toBe(0);
103
+ expect(tvStore.getKForPlancher).not.toHaveBeenCalled();
104
+ });
105
+
106
+ test('plancher bas sur hall d’entrée (17) : k = 0', () => {
107
+ expect(service.execute(ctx, enveloppe('17'), pontThermique('1')).k).toBe(0);
108
+ });
109
+
110
+ test('contre-test : plancher bas sur sous-sol non chauffé (6) : valeur tabulée (pas de b sur les PT)', () => {
111
+ const di = service.execute(ctx, enveloppe('6'), pontThermique('1'));
112
+ expect(tvStore.getKForPlancher).toHaveBeenCalledWith(1, expect.anything(), expect.anything());
113
+ expect(di.k).toBe(0.71);
114
+ });
115
+
116
+ test('contre-test : plancher haut lourd sur extérieur : valeur tabulée', () => {
117
+ const di = service.execute(ctx, enveloppe('1'), pontThermique('3'));
118
+ expect(tvStore.getKForPlancher).toHaveBeenCalledWith(3, expect.anything(), expect.anything());
119
+ expect(di.k).toBe(0.71);
120
+ });
121
+
122
+ test('mur sur circulation (14) : k = 0 quelle que soit l’adjacence du plancher', () => {
123
+ expect(service.execute(ctx, enveloppe('1', '14'), pontThermique('1')).k).toBe(0);
124
+ });
125
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.11",
3
+ "version": "1.8.13",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -17,7 +17,7 @@
17
17
  "test:unit": "vitest run src",
18
18
  "test:unit:ci": "vitest run src --coverage",
19
19
  "test:corpus": "node test/corpus/run_all_corpus_checks.js",
20
- "test:corpus:all": "node test/corpus/run_all_corpus_checks.js && npm run reports:readme",
20
+ "test:corpus:all": "npm run reports:clean && node test/corpus/run_all_corpus_checks.js && npm run reports:readme",
21
21
  "test:corpus:all+database": "npm run test:corpus:all && node test/corpus/corpus_database.js",
22
22
  "qa:lint": "eslint .",
23
23
  "qa:lint:fix": "npm run qa:lint -- --fix",
@@ -29,6 +29,7 @@
29
29
  "assets:sync": "node scripts/synchronize_assets.js",
30
30
  "version:update": "node scripts/update_version.js",
31
31
  "reports:preview": "npm run http-server dist/reports/corpus -o",
32
+ "reports:clean": "node scripts/clean_corpus_reports.js",
32
33
  "reports:readme": "node scripts/generate_corpus_readme.js",
33
34
  "reports:stamp": "node scripts/stamp_corpus_release.js",
34
35
  "reports:version": "node scripts/corpus_version.js"