Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/4_ventilation.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ function tv_debits_ventilation(di, de, du) {
}
}

/**
* Vrai si le DPE comporte au moins une menuiserie (baie ou porte) et qu'aucune n'exporte
* le champ `presence_joint` (issue #294).
*/
export function presenceJointNonRenseignee(porte_list, bv_list) {
const menuiseries = (porte_list || []).concat(bv_list || []);
return (
menuiseries.length > 0 &&
menuiseries.every((m) => [undefined, null, ''].includes(m.donnee_entree?.presence_joint))
);
}

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) => {
Expand Down Expand Up @@ -82,14 +94,23 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
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 du DPE isolation_surfaces = ${rowQ4paConv.isolation_surfaces} est utilisée.`
alors que ce devrait être isolation_surfaces = ${isolation_surfaces}. La valeur recalculée isolation_surfaces = ${isolation_surfaces} est utilisée.`
);
}

if (rowQ4paConv.presence_joints_menuiserie !== pjt) {
/**
* Issue #294 : aucune menuiserie n'exporte `presence_joint`. Aucune donnée ne contredit alors
* la ligne q4pa_conv retenue par le logiciel (ex. ligne 12 « avant 1948 avec joints », 2,5) :
* on reprend sa valeur `presence_joints_menuiserie` au lieu de considérer les menuiseries
* sans joint.
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
*/
if (presenceJointNonRenseignee(porte_list, bv_list)) {
pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0';
} else if (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 du DPE presence_joints_menuiserie = ${rowQ4paConv.presence_joints_menuiserie} est utilisée.`
alors que ce devrait être presence_joints_menuiserie = ${pjt}. La valeur recalculée presence_joints_menuiserie = ${pjt} est utilisée.`
);
}
}
Expand Down
103 changes: 102 additions & 1 deletion src/4_ventilation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ vi.mock('./5_conso_ventilation.js', () => ({
default: vi.fn()
}));

const { default: calc_ventilation, calc_hperm } = await import('./4_ventilation.js');
const {
default: calc_ventilation,
calc_hperm,
presenceJointNonRenseignee
} = await import('./4_ventilation.js');
const { tv } = await import('./utils.js');
const { default: calc_pvent } = await import('./5_conso_ventilation.js');

Expand Down Expand Up @@ -404,4 +408,101 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
expect(errorSpy).not.toHaveBeenCalled();
errorSpy.mockRestore();
});

/**
* Issue #294 : presence_joint absent sur toutes les menuiseries => la valeur
* presence_joints_menuiserie de la ligne q4pa_conv du DPE est reprise.
*/
describe('presence_joint non exporté (issue #294)', () => {
/** Avant 1948, surfaces non isolées : seul le critère des joints départage les lignes 1 et 12. */
const murNonIsole = [
{
donnee_intermediaire: { b: 1 },
donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 100 }
}
];
const vtLigne12 = () => ({
donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: 12, description: 'v' }
});
const ROW_12 = {
q4pa_conv: '2.5',
tv_q4pa_conv_id: '12',
isolation_surfaces: '0',
presence_joints_menuiserie: '1'
};

test('aucune menuiserie ne renseigne le champ : joints du DPE repris, sans message', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { calc_ventilation_bug, tvBug } = await chargerAvecBug(ROW_12);
const baies = [{ donnee_entree: { surface_totale_baie: 10 } }];
const portes = [{ donnee_entree: { surface_porte: 2, presence_joint: null } }];

calc_ventilation_bug(vtLigne12(), CG, 'th', 80, 100, murNonIsole, [], portes, baies);

expect(tvBug).toHaveBeenLastCalledWith(
'q4pa_conv',
expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '1' })
);
expect(errorSpy).not.toHaveBeenCalled();
errorSpy.mockRestore();
});

test('ligne du DPE sans joints : la valeur "0" est conservée', async () => {
const { calc_ventilation_bug, tvBug } = await chargerAvecBug({
...ROW_12,
tv_q4pa_conv_id: '1',
presence_joints_menuiserie: undefined
});
const baies = [{ donnee_entree: { surface_totale_baie: 10 } }];

calc_ventilation_bug(vtLigne12(), CG, 'th', 80, 100, murNonIsole, [], [], baies);

expect(tvBug).toHaveBeenLastCalledWith(
'q4pa_conv',
expect.objectContaining({ presence_joints_menuiserie: '0' })
);
});

test('presence_joint renseigné à 0 : la valeur recalculée prime et l’écart est signalé', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { calc_ventilation_bug, tvBug } = await chargerAvecBug(ROW_12);
const baies = [{ donnee_entree: { surface_totale_baie: 10, presence_joint: 0 } }];

calc_ventilation_bug(vtLigne12(), CG, 'th', 80, 100, murNonIsole, [], [], baies);

expect(tvBug).toHaveBeenLastCalledWith(
'q4pa_conv',
expect.objectContaining({ presence_joints_menuiserie: '0' })
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('La valeur recalculée presence_joints_menuiserie = 0 est utilisée')
);
errorSpy.mockRestore();
});
});
});

describe('presenceJointNonRenseignee', () => {
test('aucune menuiserie : faux', () => {
expect(presenceJointNonRenseignee([], [])).toBe(false);
expect(presenceJointNonRenseignee(undefined, undefined)).toBe(false);
});

test('champ absent, null ou vide sur toutes les menuiseries : vrai', () => {
expect(
presenceJointNonRenseignee(
[{ donnee_entree: { presence_joint: '' } }],
[{ donnee_entree: {} }, { donnee_entree: { presence_joint: null } }]
)
).toBe(true);
});

test('au moins une menuiserie renseignée (même à 0) : faux', () => {
expect(
presenceJointNonRenseignee(
[{ donnee_entree: { presence_joint: 0 } }],
[{ donnee_entree: {} }]
)
).toBe(false);
});
});
10 changes: 10 additions & 0 deletions src/features/dpe/infrastructure/tv.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,16 @@ export class TvStore {
return debitsVentilation;
}

/**
* Ligne de la table q4pa_conv par son identifiant
* @param tvQ4paConvId {number|string|undefined}
* @return {object|undefined}
*/
getQ4paConvById(tvQ4paConvId) {
if (tvQ4paConvId === undefined || tvQ4paConvId === null) return;
return tv['q4pa_conv'].find((v) => v.tv_q4pa_conv_id === String(tvQ4paConvId));
}

/**
* Valeur conventionnelle de la perméabilité sous 4Pa
* @see Chapitre 4 - Calcul des déperditions par renouvellement d’air
Expand Down
11 changes: 11 additions & 0 deletions src/features/dpe/infrastructure/tv.store.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,17 @@ describe('Lecture des tables de valeurs', () => {
const ug = tvStore.getQ4paConv('2', TypeHabitation.MAISON, '0', '1');
expect(ug).toBeUndefined();
});

test('lecture par identifiant (issue #294)', () => {
expect(tvStore.getQ4paConvById(12)).toMatchObject({
q4pa_conv: '2.5',
presence_joints_menuiserie: '1'
});
expect(tvStore.getQ4paConvById('1')).toMatchObject({ q4pa_conv: '4.6' });
expect(tvStore.getQ4paConvById(undefined)).toBeUndefined();
expect(tvStore.getQ4paConvById(null)).toBeUndefined();
expect(tvStore.getQ4paConvById(999)).toBeUndefined();
});
});

test('lecture des valeurs de tempBase', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,31 @@ export class DeperditionEnveloppeService {
this.#surfaceIsolee,
this.#surfaceNonIsolee,
this.#surfaceMenuiserieAvecJoint,
this.#surfaceMenuiserieSansJoint
this.#surfaceMenuiserieSansJoint,
this.#presenceJointNonRenseignee(logement.enveloppe)
);
deperditions.hvent += ventilation.donnee_intermediaire.hvent;
deperditions.hperm += ventilation.donnee_intermediaire.hperm;
});

return deperditions;
}

/**
* Vrai si le logement comporte au moins une menuiserie (baie ou porte) et qu'aucune n'exporte
* le champ presence_joint (issue #294).
*
* @param enveloppe {Enveloppe}
* @return {boolean}
*/
#presenceJointNonRenseignee(enveloppe) {
const menuiseries = [
...(enveloppe?.porte_collection?.porte || []),
...(enveloppe?.baie_vitree_collection?.baie_vitree || [])
];
return (
menuiseries.length > 0 &&
menuiseries.every((m) => [undefined, null, ''].includes(m.donnee_entree?.presence_joint))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,17 @@ describe('DeperditionEnveloppeService (§3 déperditions de l’enveloppe)', ()
// déperditive = mur 10 + porte 2 + plancher haut 30 + baie 4 = 46
// isolée = mur 5 + plancher haut 7 = 12 ; non isolée = mur 10 + plancher haut 30 = 40
// menuiseries avec joint = porte 2 + baie 4 = 6 ; sans joint = porte 3 + baie 6 = 9
expect(ventilationService.execute).toHaveBeenCalledWith(ctx, { id: 'v' }, 46, 12, 40, 6, 9);
// presence_joint renseigné sur au moins une menuiserie => indicateur #294 à false
expect(ventilationService.execute).toHaveBeenCalledWith(
ctx,
{ id: 'v' },
46,
12,
40,
6,
9,
false
);
});

test('les surfaces sont réinitialisées à chaque calcul', () => {
Expand All @@ -186,7 +196,18 @@ describe('DeperditionEnveloppeService (§3 déperditions de l’enveloppe)', ()
service.deperditions(ctx, logement);
service.deperditions(ctx, logement);

expect(ventilationService.execute).toHaveBeenLastCalledWith(ctx, {}, 2, 0, 0, 0, 2);
// aucune menuiserie ne renseigne presence_joint => indicateur #294 à true
expect(ventilationService.execute).toHaveBeenLastCalledWith(ctx, {}, 2, 0, 0, 0, 2, true);
});

test('logement sans menuiserie : indicateur presence_joint non renseigné à false (issue #294)', () => {
const logement = logementVide();
logement.ventilation_collection.ventilation = [{ donnee_entree: {} }];
ventilationService.execute.mockReturnValue({ hvent: 0, hperm: 0 });

service.deperditions(ctx, logement);

expect(ventilationService.execute).toHaveBeenLastCalledWith(ctx, {}, 0, 0, 0, 0, 0, false);
});

describe('espaces tampons solarisés', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DeperditionService } from '../deperdition.service.js';

import { logger } from '../../../../../core/util/logger/log-service.js';
import { bug_for_bug_compat } from '../../../../../utils.js';
import { TypeHabitation } from '../../../../dpe/domain/models/type-habitation.model.js';
import { TypeVentilation } from '../../../../dpe/domain/models/type-ventilation.model.js';

Expand All @@ -22,6 +23,7 @@ export class DeperditionVentilationService extends DeperditionService {
* @param surfaceNonIsolee {number}
* @param surfaceMenuiserieAvecJoint {number}
* @param surfaceMenuiserieSansJoint {number}
* @param presenceJointNonRenseignee {boolean} aucune menuiserie n'exporte presence_joint (issue #294)
* @return {VentilationDI}
*/
execute(
Expand All @@ -31,15 +33,17 @@ export class DeperditionVentilationService extends DeperditionService {
surfaceIsolee,
surfaceNonIsolee,
surfaceMenuiserieAvecJoint,
surfaceMenuiserieSansJoint
surfaceMenuiserieSansJoint,
presenceJointNonRenseignee = false
) {
const q4paConv = this.q4paConv(
ventilationDE,
ctx,
surfaceIsolee,
surfaceNonIsolee,
surfaceMenuiserieAvecJoint,
surfaceMenuiserieSansJoint
surfaceMenuiserieSansJoint,
presenceJointNonRenseignee
);
const debitsVentilation = this.#debitsVentilation(ventilationDE.enum_type_ventilation_id);

Expand All @@ -62,6 +66,7 @@ export class DeperditionVentilationService extends DeperditionService {
* @param surfaceNonIsolee {number}
* @param surfaceMenuiserieAvecJoint {number}
* @param surfaceMenuiserieSansJoint {number}
* @param presenceJointNonRenseignee {boolean}
* @return {number}
*/
q4paConv(
Expand All @@ -70,7 +75,8 @@ export class DeperditionVentilationService extends DeperditionService {
surfaceIsolee,
surfaceNonIsolee,
surfaceMenuiserieAvecJoint,
surfaceMenuiserieSansJoint
surfaceMenuiserieSansJoint,
presenceJointNonRenseignee = false
) {
// Pour les bâtiments qui ont fait l’objet d’une mesure d’étanchéité à l’air moins de deux ans avant le diagnostic, la valeur
// mesurée de Q4Paconv/m² peut être saisie.
Expand All @@ -92,6 +98,15 @@ export class DeperditionVentilationService extends DeperditionService {
// (si les menuiseries représentant plus de 50% de la surface totale possèdent des joints) => valeur forfaitaire.
if (surfaceMenuiserieAvecJoint > surfaceMenuiserieSansJoint) {
presenceJointsMenuiserie = '1';
} else if (
bug_for_bug_compat &&
presenceJointNonRenseignee &&
this.tvStore.getQ4paConvById(ventilationDE.tv_q4pa_conv_id)
?.presence_joints_menuiserie === '1'
) {
// Issue #294 : aucune menuiserie n'exporte presence_joint, rien ne contredit la ligne
// q4pa_conv « avant 1948 avec joints » retenue par le DPE => on la reprend.
presenceJointsMenuiserie = '1';
}
}
} else if (enumPeriodeConstructionId === '2') {
Expand Down
Loading