From ba0244f802a8d96c7a934a985a9c2dd5393b42de Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Fri, 2 Oct 2026 22:15:40 +0000 Subject: [PATCH] =?UTF-8?q?fix(ecs):=20r=C3=A9partir=20le=20besoin=20ecs?= =?UTF-8?q?=20d'un=20immeuble=20au=20prorata=20des=20surfaces=20des=20inst?= =?UTF-8?q?allations=20(#232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pour un DPE immeuble avec au moins 2 installations ECS dont une collective, le besoin ECS (conventionnel et dépensier) de chaque installation vaut Sh_installation / Sh_immeuble / (rdim || 1), au lieu du besoin divisé par 2. Prorata brut, sans normalisation ni plafonnement (code Tribu Calcul_batiment.Calcul_Cecs). La consommation reste remultipliée par rdim dans conso.js (cle_repartition_ecs), comme Tribu (Calcul_Cecs_installation_2). Repli sur la division par 2 (§11.4) avec avertissement si une surface d'installation ou la surface de l'immeuble est absente ou nulle. Maison, appartement, cas virtualisés et immeuble tout individuel inchangés. Closes #232 --- src/11_ecs.js | 16 +++++++- src/11_ecs.spec.js | 98 ++++++++++++++++++++++++++++++++++++++++++++++ src/engine.js | 25 +++++++++++- src/engine.spec.js | 77 +++++++++++++++++++++++++++++++++++- 4 files changed, 211 insertions(+), 5 deletions(-) diff --git a/src/11_ecs.js b/src/11_ecs.js index 4cca0b97..2ca9e9ec 100644 --- a/src/11_ecs.js +++ b/src/11_ecs.js @@ -149,7 +149,8 @@ export default function calc_ecs( surfaceImmeuble, nombreAppartements, isImmeubleSystemEcsIndividuels, - becsParMois + becsParMois, + isImmeubleMultiEcs = false ) { const de = ecs.donnee_entree; const di = {}; @@ -157,7 +158,18 @@ export default function calc_ecs( // La conso de chaque générateur ECS doit être ramenée au prorata de la surface du logement di.ratio_besoin_ecs = 1; - if (virtualisationECS) { + if (isImmeubleMultiEcs) { + /** + * Immeuble avec plusieurs installations ECS (collectives ou mixtes) : ratio de besoin = + * surface habitable desservie par l'installation / surface habitable de l'immeuble, + * sans normalisation ni plafonnement. Le besoin est exprimé par unité dimensionnée (/ rdim), + * la consommation étant remultipliée par rdim lors de l'agrégation (conso.js, cle_repartition_ecs). + * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §11.4 + * @see Moteur_DPE.dll Calcul_batiment.Calcul_Cecs() et Calcul_Cecs_installation_2() + */ + di.ratio_besoin_ecs = + Number(de.surface_habitable) / Number(surfaceImmeuble) / (Number(de.rdim) || 1); + } else if (virtualisationECS) { di.ratio_besoin_ecs = de.cle_repartition_ecs || 1; } else if (isImmeubleSystemEcsIndividuels) { if (nombreAppartements) { diff --git a/src/11_ecs.spec.js b/src/11_ecs.spec.js index e496fbc6..dbb14a85 100644 --- a/src/11_ecs.spec.js +++ b/src/11_ecs.spec.js @@ -148,6 +148,104 @@ describe('calc_ecs - ratio de besoin ECS', () => { }); }); +/** + * Immeuble à plusieurs installations ECS collectives ou mixtes (#232) : + * ratio = Sh_ecs / Sh immeuble / (rdim || 1), prorata brut sans normalisation ni plafond. + * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §11.4 + * @see Moteur_DPE.dll Calcul_batiment.Calcul_Cecs() (l.579-593) + */ +describe('calc_ecs - immeuble à plusieurs installations ECS (prorata des surfaces)', () => { + /** Calcule le ratio d'une installation dans le cas immeuble multi-ECS. */ + function calcImmeuble(de, surfaceImmeuble, options = {}) { + const ecs = makeEcs({ tv_rendement_distribution_ecs_id: '5', ...de }); + calc_ecs( + {}, + ecs, + 1000, + 1500, + 1, + 'ca1', + 'h1a', + 'immeuble', + options.virtualisation ?? false, + surfaceImmeuble, + options.nombreAppartements ?? null, + options.individuels ?? false, + {}, + options.multiEcs ?? true + ); + return ecs.donnee_intermediaire; + } + + test('autotest IC1-0-2 : 2 installations collectives 20 et 155 m² sur 175 m²', () => { + const di1 = calcImmeuble({ enum_type_installation_id: '2', surface_habitable: 20 }, 175); + const di2 = calcImmeuble({ enum_type_installation_id: '2', surface_habitable: 155 }, 175); + + expect(di1.ratio_besoin_ecs).toBeCloseTo(20 / 175, 9); // 0,1143 + expect(di2.ratio_besoin_ecs).toBeCloseTo(155 / 175, 9); // 0,8857 + expect(di1.ratio_besoin_ecs + di2.ratio_besoin_ecs).toBeCloseTo(1, 9); + expect(di1.besoin_ecs).toBeCloseTo((1000 * 20) / 175, 9); + }); + + test('somme des surfaces < Sh immeuble (461 sur 922) : prorata brut 0,5 sans normalisation', () => { + const di = calcImmeuble({ enum_type_installation_id: '2', surface_habitable: 461 }, 922); + + expect(di.ratio_besoin_ecs).toBe(0.5); + expect(di.besoin_ecs).toBe(500); + }); + + test('somme des surfaces > Sh immeuble : prorata brut sans plafonnement', () => { + const di = calcImmeuble({ enum_type_installation_id: '2', surface_habitable: 7706 }, 8726); + + expect(di.ratio_besoin_ecs).toBeCloseTo(7706 / 8726, 9); + }); + + test('immeuble mixte : installation individuelle 800 m², rdim 13, sur 2400 m² => besoin par unité', () => { + const di = calcImmeuble( + { enum_type_installation_id: '1', surface_habitable: 800, rdim: 13 }, + 2400 + ); + + expect(di.ratio_besoin_ecs).toBeCloseTo(800 / 2400 / 13, 9); // 0,02564 + expect(di.ratio_besoin_ecs).toBeCloseTo(0.02564, 5); + }); + + test('rdim nul ou absent : diviseur 1', () => { + expect( + calcImmeuble({ surface_habitable: 50, rdim: 0, enum_type_installation_id: '2' }, 200) + .ratio_besoin_ecs + ).toBe(0.25); + expect( + calcImmeuble({ surface_habitable: '50', enum_type_installation_id: '2' }, '200') + .ratio_besoin_ecs + ).toBe(0.25); + }); + + test('besoin_ecs_depensier suit le même ratio que besoin_ecs', () => { + const di = calcImmeuble({ enum_type_installation_id: '2', surface_habitable: 20 }, 175); + + expect(di.besoin_ecs_depensier).toBeCloseTo((1500 * 20) / 175, 9); + expect(di.besoin_ecs_depensier / di.besoin_ecs).toBeCloseTo(1.5, 9); + }); + + test('prioritaire sur la clé de répartition et la branche « tout individuel »', () => { + const di = calcImmeuble( + { enum_type_installation_id: '2', surface_habitable: 100, cle_repartition_ecs: 0.9 }, + 200, + { virtualisation: true, individuels: true, nombreAppartements: 4 } + ); + + expect(di.ratio_besoin_ecs).toBe(0.5); + }); + + test('sans indicateur multi-ECS (valeur par défaut) : comportement historique conservé (1 / rdim)', () => { + const ecs = makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 100, rdim: 4 }); + calc_ecs({}, ecs, 1000, 1500, 1, 'ca1', 'h1a', 'immeuble', false, 200, null, false, {}); + + expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(0.25); + }); +}); + describe('calc_ecs - rendement de distribution', () => { test('la ligne forfaitaire alimente rendement_distribution et l’identifiant de table', () => { const de = { tv_rendement_distribution_ecs_id: '5' }; diff --git a/src/engine.js b/src/engine.js index 7796e4c9..6fe5e7f1 100644 --- a/src/engine.js +++ b/src/engine.js @@ -323,17 +323,37 @@ export function calcul_3cl(inputDpe, options) { let becs = apport_et_besoin.besoin_ecs; let becs_dep = apport_et_besoin.besoin_ecs_depensier; let isImmeubleSystemEcsIndividuels = false; + let isImmeubleMultiEcs = false; /** * 11.4 Plusieurs systèmes d’ECS (limité à 2 systèmes différents par logement) * Les besoins en ECS pour chaque générateur sont / 2 + * + * Immeuble avec plusieurs installations ECS (collectives ou mixtes) : le besoin est réparti + * au prorata de la surface habitable desservie par chaque installation (Sh_ecs / Sh immeuble), + * sans normalisation ni plafonnement (cf. calc_ecs). + * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §11.4 + * @see Guide Tribu TE-211208 p. 24 ; Moteur_DPE.dll Calcul_batiment.Calcul_Cecs() */ if (ecs.length > 1) { // Immeuble avec différents systèmes individuels isImmeubleSystemEcsIndividuels = th === 'immeuble' && ecs.every((e) => e.donnee_entree.enum_type_installation_id === '1'); - if (!isImmeubleSystemEcsIndividuels) { + if (th === 'immeuble' && !isImmeubleSystemEcsIndividuels) { + isImmeubleMultiEcs = + Number(cg.surface_habitable_immeuble) > 0 && + ecs.every((e) => Number(e.donnee_entree.surface_habitable) > 0); + + if (!isImmeubleMultiEcs) { + console.warn( + `Immeuble avec plusieurs installations ECS : surface habitable de l'immeuble ou d'une installation ECS absente ou nulle. + Repli sur la division du besoin ECS par 2 (§11.4).` + ); + } + } + + if (!isImmeubleSystemEcsIndividuels && !isImmeubleMultiEcs) { becs /= 2; becs_dep /= 2; } @@ -438,7 +458,8 @@ export function calcul_3cl(inputDpe, options) { dpe.logement.caracteristique_generale.surface_habitable_immeuble, dpe.logement.caracteristique_generale.nombre_appartement, isImmeubleSystemEcsIndividuels, - apport_et_besoin.besoin_ecs_par_mois || {} + apport_et_besoin.besoin_ecs_par_mois || {}, + isImmeubleMultiEcs ); if (!dpeGenereImmeuble) { diff --git a/src/engine.spec.js b/src/engine.spec.js index 810b2ace..eec9f892 100644 --- a/src/engine.spec.js +++ b/src/engine.spec.js @@ -565,7 +565,7 @@ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => { expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(200); }); - test('immeuble : systèmes ECS non tous individuels => besoin divisé par deux', () => { + test('immeuble : systèmes ECS non tous individuels sans surfaces => repli sur la division par deux', () => { enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' }; const dpe = makeDpe(); dpe.logement.installation_ecs_collection.installation_ecs = [ @@ -578,11 +578,86 @@ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => { generateur_ecs_collection: { generateur_ecs: [] } } ]; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); calcul_3cl(dpe, { sanitize: false }); expect(calc_ecs.mock.calls[0][11]).toBe(false); expect(calc_ecs.mock.calls[0][2]).toBe(50); // 100 / 2 + expect(calc_ecs.mock.calls[0][13]).toBe(false); // pas de prorata des surfaces + expect(warn).toHaveBeenCalledWith(expect.stringContaining('Repli sur la division')); + warn.mockRestore(); + }); + + /** + * Immeuble à plusieurs installations ECS collectives ou mixtes : répartition au prorata des + * surfaces (cf. calc_ecs), sans division par 2. + * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §11.4 + */ + describe('immeuble à plusieurs installations ECS collectives ou mixtes (#232)', () => { + function makeImmeubleMultiEcs(surfaces, surfaceImmeuble = 200) { + enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' }; + const dpe = makeDpe(); + dpe.logement.caracteristique_generale.surface_habitable_immeuble = surfaceImmeuble; + dpe.logement.installation_ecs_collection.installation_ecs = surfaces.map((s, i) => ({ + donnee_entree: { enum_type_installation_id: i === 0 ? '1' : '2', surface_habitable: s }, + generateur_ecs_collection: { generateur_ecs: [] } + })); + return dpe; + } + + test('surfaces renseignées : besoin non divisé et prorata des surfaces signalé à calc_ecs', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + calcul_3cl(makeImmeubleMultiEcs([20, 155]), { sanitize: false }); + + expect(calc_ecs).toHaveBeenCalledTimes(2); + calc_ecs.mock.calls.forEach((call) => { + expect(call[2]).toBe(100); // besoin_ecs non divisé + expect(call[3]).toBe(120); // besoin_ecs_depensier non divisé + expect(call[11]).toBe(false); + expect(call[13]).toBe(true); + }); + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + test.each([ + ['une surface d’installation nulle', [0, 155], 200], + ['une surface d’installation absente', [undefined, 155], 200], + ['la surface de l’immeuble nulle', [20, 155], 0] + ])('%s : repli sur la division par deux avec avertissement', (_, surfaces, sh) => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + calcul_3cl(makeImmeubleMultiEcs(surfaces, sh), { sanitize: false }); + + expect(calc_ecs.mock.calls[0][2]).toBe(50); + expect(calc_ecs.mock.calls[0][3]).toBe(60); + expect(calc_ecs.mock.calls[0][13]).toBe(false); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('Repli sur la division')); + warn.mockRestore(); + }); + + test('appartement avec deux installations renseignées : division par deux inchangée', () => { + const dpe = makeImmeubleMultiEcs([40, 40]); + enums.methode_application_dpe_log = { 1: 'dpe appartement individuel' }; + + calcul_3cl(dpe, { sanitize: false }); + + expect(calc_ecs.mock.calls[0][2]).toBe(50); + expect(calc_ecs.mock.calls[0][13]).toBe(false); + }); + + test('immeuble dont toutes les installations sont individuelles : branche existante inchangée', () => { + const dpe = makeImmeubleMultiEcs([100, 100]); + dpe.logement.installation_ecs_collection.installation_ecs.forEach( + (e) => (e.donnee_entree.enum_type_installation_id = '1') + ); + + calcul_3cl(dpe, { sanitize: false }); + + expect(calc_ecs.mock.calls[0][2]).toBe(100); + expect(calc_ecs.mock.calls[0][11]).toBe(true); + expect(calc_ecs.mock.calls[0][13]).toBe(false); + }); }); test('maison avec deux systèmes ECS => besoin divisé par deux', () => {