diff --git a/src/4_ventilation.js b/src/4_ventilation.js index 3d0f63b2..7da216d9 100644 --- a/src/4_ventilation.js +++ b/src/4_ventilation.js @@ -29,6 +29,15 @@ export function presenceJointNonRenseignee(porte_list, bv_list) { ); } +/** + * Vrai si la ligne de la table `q4pa_conv` couvre la période de construction du DPE. + */ +export function isLigneQ4paMemePeriode(rowQ4paConv, cg) { + return String(rowQ4paConv?.enum_periode_construction_id ?? '') + .split('|') + .includes(String(cg.enum_periode_construction_id)); +} + function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) { const surfaces = mur_list.concat(ph_list); const surface_isolee = surfaces.reduce((acc, s) => { @@ -91,11 +100,29 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) { }); if (rowQ4paConv) { + let ligneDpeReprise = false; + /** + * Le moteur de référence reçoit Q4Pa_conv en entrée : c'est le logiciel de saisie qui choisit + * la ligne de la table. Certains logiciels (LICIEL notamment) retiennent une ligne dont le + * critère `isolation_surfaces` contredit l'isolation déclarée des parois (ex. ligne 10 « avant + * 1948, surfaces isolées », 2, alors que toutes les parois sont non isolées → ligne 1, 4,6). + * Si la ligne saisie correspond à la période de construction du DPE, on la reprend en entier + * (isolation et joints) pour reproduire le calcul du logiciel. + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4 + */ if (rowQ4paConv.isolation_surfaces !== isolation_surfaces) { - console.error( - `Calcul de hperm pour la ventilation ${de.description}. Le DPE considère isolation_surfaces = ${rowQ4paConv.isolation_surfaces} - alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La valeur recalculée isolation_surfaces = ${isolation_surfaces} est utilisée.` - ); + if (isLigneQ4paMemePeriode(rowQ4paConv, cg)) { + console.error( + `Calcul de hperm pour la ventilation ${de.description}. Le DPE considère isolation_surfaces = ${rowQ4paConv.isolation_surfaces} \n alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La ligne q4pa_conv ${de.tv_q4pa_conv_id} du DPE est conservée.` + ); + isolation_surfaces = rowQ4paConv.isolation_surfaces; + pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0'; + ligneDpeReprise = true; + } else { + console.error( + `Calcul de hperm pour la ventilation ${de.description}. Le DPE considère isolation_surfaces = ${rowQ4paConv.isolation_surfaces} \n alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La valeur recalculée isolation_surfaces = ${isolation_surfaces} est utilisée.` + ); + } } /** @@ -105,9 +132,9 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) { * sans joint. * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4 */ - if (presenceJointNonRenseignee(porte_list, bv_list)) { + if (!ligneDpeReprise && presenceJointNonRenseignee(porte_list, bv_list)) { pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0'; - } else if (rowQ4paConv.presence_joints_menuiserie !== pjt) { + } else if (!ligneDpeReprise && rowQ4paConv.presence_joints_menuiserie !== pjt) { console.error( `Calcul de hperm pour la ventilation ${de.description}. Le DPE considère presence_joints_menuiserie = ${rowQ4paConv.presence_joints_menuiserie} alors que ce devrait être presence_joints_menuiserie = ${pjt}. La valeur recalculée presence_joints_menuiserie = ${pjt} est utilisée.` diff --git a/src/4_ventilation.spec.js b/src/4_ventilation.spec.js index 30e80447..549e19d7 100644 --- a/src/4_ventilation.spec.js +++ b/src/4_ventilation.spec.js @@ -22,7 +22,8 @@ vi.mock('./5_conso_ventilation.js', () => ({ const { default: calc_ventilation, calc_hperm, - presenceJointNonRenseignee + presenceJointNonRenseignee, + isLigneQ4paMemePeriode } = await import('./4_ventilation.js'); const { tv } = await import('./utils.js'); const { default: calc_pvent } = await import('./5_conso_ventilation.js'); @@ -480,6 +481,86 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () => errorSpy.mockRestore(); }); }); + + /** + * Ligne q4pa_conv du DPE dont le critère d'isolation contredit les parois (2287E1201982M : + * ligne 10 « avant 1948, surfaces isolées » alors que toutes les parois sont non isolées). + */ + describe('isolation_surfaces incohérent avec la ligne du DPE', () => { + const murNonIsole = [ + { + donnee_intermediaire: { b: 1 }, + donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 100 } + } + ]; + const baieSansJoint = [{ donnee_entree: { surface_totale_baie: 10, presence_joint: 0 } }]; + const vtLigne = (id) => ({ + donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: id, description: 'v' } + }); + const ROW_10 = { + q4pa_conv: '2', + tv_q4pa_conv_id: '10', + enum_periode_construction_id: '1', + isolation_surfaces: '1' + }; + + test('ligne de la même période : isolation et joints de la ligne du DPE repris', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { calc_ventilation_bug, tvBug } = await chargerAvecBug(ROW_10); + + calc_ventilation_bug(vtLigne(10), CG, 'th', 80, 100, murNonIsole, [], [], baieSansJoint); + + expect(tvBug).toHaveBeenLastCalledWith( + 'q4pa_conv', + expect.objectContaining({ isolation_surfaces: '1', presence_joints_menuiserie: '0' }) + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('La ligne q4pa_conv 10 du DPE est conservée') + ); + // les joints de la ligne reprise concordent : pas de second message + expect(errorSpy).toHaveBeenCalledTimes(1); + errorSpy.mockRestore(); + }); + + test('ligne 12 (non isolé, avec joints) reprise en entier, malgré des baies sans joint', async () => { + const { calc_ventilation_bug, tvBug } = await chargerAvecBug({ + q4pa_conv: '2.5', + tv_q4pa_conv_id: '12', + enum_periode_construction_id: '1', + isolation_surfaces: '0', + presence_joints_menuiserie: '1' + }); + vi.spyOn(console, 'error').mockImplementation(() => {}); + const { murs } = surfacesIsolantes(); + + calc_ventilation_bug(vtLigne(12), CG, 'th', 80, 100, murs, [], [], baieSansJoint); + + expect(tvBug).toHaveBeenLastCalledWith( + 'q4pa_conv', + expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '1' }) + ); + }); + + test('ligne d’une autre période : la valeur recalculée est conservée', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { calc_ventilation_bug, tvBug } = await chargerAvecBug({ + ...ROW_10, + tv_q4pa_conv_id: '11', + enum_periode_construction_id: '2' + }); + + calc_ventilation_bug(vtLigne(11), CG, 'th', 80, 100, murNonIsole, [], [], baieSansJoint); + + expect(tvBug).toHaveBeenLastCalledWith( + 'q4pa_conv', + expect.objectContaining({ isolation_surfaces: '0' }) + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('La valeur recalculée isolation_surfaces = 0 est utilisée') + ); + errorSpy.mockRestore(); + }); + }); }); describe('presenceJointNonRenseignee', () => { @@ -506,3 +587,28 @@ describe('presenceJointNonRenseignee', () => { ).toBe(false); }); }); + +describe('isLigneQ4paMemePeriode', () => { + test('période contenue dans une liste « a|b|c » : vrai', () => { + expect( + isLigneQ4paMemePeriode( + { enum_periode_construction_id: '3|4|5' }, + { enum_periode_construction_id: 4 } + ) + ).toBe(true); + }); + + test('période différente : faux', () => { + expect( + isLigneQ4paMemePeriode( + { enum_periode_construction_id: '2' }, + { enum_periode_construction_id: '1' } + ) + ).toBe(false); + }); + + test('ligne sans période (ou absente) : faux', () => { + expect(isLigneQ4paMemePeriode({}, { enum_periode_construction_id: '1' })).toBe(false); + expect(isLigneQ4paMemePeriode(null, { enum_periode_construction_id: '1' })).toBe(false); + }); +}); diff --git a/test/open3cl-q4pa-ligne-dpe.spec.js b/test/open3cl-q4pa-ligne-dpe.spec.js new file mode 100644 index 00000000..9c055142 --- /dev/null +++ b/test/open3cl-q4pa-ligne-dpe.spec.js @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'vitest'; +import { getAdemeFileJson } from './test-helpers.js'; +import { calcul_3cl } from '../src/index.js'; +import { set_bug_for_bug_compat, set_tv_match_optimized_version } from '../src/utils.js'; + +/** + * Non-régression https://github.com/Open3CL/engine/issues/312 + * + * Le DPE 2287E1201982M (LICIEL) retient la ligne q4pa_conv 10 « avant 1948, surfaces isolées » + * (q4pa_conv = 2) alors que toutes ses parois sont non isolées (ligne 1 : 4,6). En mode + * bug_for_bug_compat, la ligne du DPE est reprise : q4pa_conv, hperm et conso_ch sont ceux du DPE. + */ +describe('q4pa_conv repris de la ligne du DPE - issue #312', () => { + test('2287E1201982M : q4pa_conv, hperm et conso_ch identiques au DPE', () => { + set_bug_for_bug_compat(); + set_tv_match_optimized_version(); + /** @type {FullDpe} */ + const input = getAdemeFileJson('2287E1201982M'); + const output = calcul_3cl(structuredClone(input)); + + const ventIn = input.logement.ventilation_collection.ventilation[0].donnee_intermediaire; + const ventOut = output.logement.ventilation_collection.ventilation[0].donnee_intermediaire; + expect(ventOut.q4pa_conv).toBe(2); + expect(ventOut.hperm).toBeCloseTo(ventIn.hperm, 1); + expect(output.logement.sortie.ef_conso.conso_ch).toBeCloseTo( + input.logement.sortie.ef_conso.conso_ch, + 0 + ); + }); +});