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
108 changes: 44 additions & 64 deletions src/4_ventilation.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,6 @@ 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))
);
}

/**
* Vrai si la ligne de la table `q4pa_conv` couvre la période de construction du DPE.
*/
Expand All @@ -45,13 +33,22 @@ export function isLigneQ4paMemePeriode(rowQ4paConv, cg) {
}

/**
* Vrai si la ligne de la table `q4pa_conv` considère les surfaces isolées (`isolation_surfaces` = 1)
* alors que le recalcul à partir des parois les donne non isolées (`isolation_surfaces` = 0).
* Vrai si la ligne `q4pa_conv` du DPE ne s'écarte des valeurs recalculées que dans le sens
* « surfaces isolées » et/ou « avec joints » : chaque critère porté par la ligne est supérieur ou
* égal à la valeur recalculée, et au moins un est strictement supérieur. Un critère absent de la
* ligne (ex. joints sur les lignes « surfaces isolées ») n'est pas comparé.
*/
export function isLigneQ4paSurfacesIsolees(rowQ4paConv, isolationSurfacesRecalculee) {
return (
String(rowQ4paConv.isolation_surfaces) === '1' && String(isolationSurfacesRecalculee) === '0'
);
export function isLigneQ4paPlusFavorable(rowQ4paConv, isolationSurfaces, presenceJoints) {
let plusFavorable = false;
for (const [valeurLigne, valeurRecalculee] of [
[rowQ4paConv.isolation_surfaces, isolationSurfaces],
[rowQ4paConv.presence_joints_menuiserie, presenceJoints]
]) {
if ([undefined, null, ''].includes(valeurLigne)) continue;
if (String(valeurLigne) < String(valeurRecalculee)) return false;
if (String(valeurLigne) > String(valeurRecalculee)) plusFavorable = true;
}
return plusFavorable;
}

function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
Expand Down Expand Up @@ -116,63 +113,46 @@ 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.
* la ligne de la table. Certains logiciels retiennent une ligne qui contredit les parois ou
* les menuiseries déclarées (ex. ligne 10 « avant 1948, surfaces isolées » alors que les
* parois sont non isolées (#312), ligne 12 « avant 1948, avec joints » alors que les
* menuiseries sont sans joint (#332) ou n'exportent pas `presence_joint` (#294)).
*
* Cette reprise n'est faite qu'avec l'option `compat_for_corpus_analysis` (analyse du corpus
* ADEME). Par défaut, la ligne contradictoire est seulement signalée et q4pa_conv est
* recalculé selon la méthode : un contrôle de cohérence du DPE doit détecter l'erreur et un
* scénario de travaux ne doit pas la propager.
* Avec l'option `compat_for_corpus_analysis` (analyse du corpus ADEME) uniquement, une seule
* règle : si la ligne du DPE couvre la période de construction et qu'elle ne s'écarte du
* recalcul que dans le sens « surfaces isolées » / « avec joints », elle est reprise en
* entier, pour reproduire le calcul du logiciel.
*
* Sens unique : la reprise ne s'applique que si la ligne indique des surfaces isolées alors que
* le recalcul les donne non isolées. Dans le sens inverse (ligne « non isolées », recalcul
* « isolées »), le DPE a pu être modifié par l'appelant (scénario d'isolation des parois) :
* la valeur recalculée est utilisée.
* @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
*/
if (rowQ4paConv.isolation_surfaces !== isolation_surfaces) {
if (
compat_for_corpus_analysis &&
isLigneQ4paSurfacesIsolees(rowQ4paConv, isolation_surfaces) &&
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.`
);
}
}

/**
* 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. Uniquement avec l'option `compat_for_corpus_analysis`.
* Sinon (mode par défaut, ligne d'une autre période ou écart dans le sens inverse, cas d'un
* scénario de travaux qui améliore les parois ou les menuiseries) : q4pa_conv est recalculé
* selon la méthode et les écarts sont seulement signalés.
* @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
*/
if (
compat_for_corpus_analysis &&
!ligneDpeReprise &&
presenceJointNonRenseignee(porte_list, bv_list)
isLigneQ4paMemePeriode(rowQ4paConv, cg) &&
isLigneQ4paPlusFavorable(rowQ4paConv, isolation_surfaces, pjt)
) {
pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0';
} 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.`
`Calcul de hperm pour la ventilation ${de.description}. La ligne q4pa_conv ${de.tv_q4pa_conv_id} du DPE (isolation_surfaces = ${rowQ4paConv.isolation_surfaces}, presence_joints_menuiserie = ${rowQ4paConv.presence_joints_menuiserie})
contredit les valeurs recalculées (isolation_surfaces = ${isolation_surfaces}, presence_joints_menuiserie = ${pjt}). La ligne du DPE est conservée.`
);
isolation_surfaces = rowQ4paConv.isolation_surfaces === '1' ? '1' : '0';
pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0';
} else {
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} \n 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) {
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.`
);
}
}
}
}
Expand Down
145 changes: 104 additions & 41 deletions src/4_ventilation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ vi.mock('./5_conso_ventilation.js', () => ({
const {
default: calc_ventilation,
calc_hperm,
presenceJointNonRenseignee,
isLigneQ4paMemePeriode,
isLigneQ4paSurfacesIsolees
isLigneQ4paPlusFavorable
} = 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 @@ -446,11 +445,12 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
const ROW_12 = {
q4pa_conv: '2.5',
tv_q4pa_conv_id: '12',
enum_periode_construction_id: '1',
isolation_surfaces: '0',
presence_joints_menuiserie: '1'
};

test('aucune menuiserie ne renseigne le champ : joints du DPE repris, sans message', async () => {
test('aucune menuiserie ne renseigne le champ : ligne du DPE reprise', 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 } }];
Expand All @@ -462,7 +462,9 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
'q4pa_conv',
expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '1' })
);
expect(errorSpy).not.toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('La ligne du DPE est conservée')
);
errorSpy.mockRestore();
});

Expand All @@ -482,13 +484,37 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
);
});

test('presence_joint renseigné à 0 : la valeur recalculée prime et l’écart est signalé', async () => {
/**
* Issue #332 : menuiseries toutes exportées sans joint alors que la ligne du DPE est « avec
* joints » (2606E1314062H). Même règle : la ligne de la même période est reprise.
*/
test('presence_joint renseigné à 0 (#332) : ligne « avec joints » du DPE reprise', 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({ isolation_surfaces: '0', presence_joints_menuiserie: '1' })
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('La ligne q4pa_conv 12 du DPE')
);
errorSpy.mockRestore();
});

test('ligne « avec joints » d’une autre période : joints recalculés et écart signalé', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { calc_ventilation_bug, tvBug } = await chargerAvecBug({
...ROW_12,
enum_periode_construction_id: '2'
});
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' })
Expand All @@ -498,6 +524,27 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
);
errorSpy.mockRestore();
});

test('ligne « sans joints » alors que les menuiseries ont des joints : joints recalculés', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { calc_ventilation_bug, tvBug } = await chargerAvecBug({
...ROW_12,
tv_q4pa_conv_id: '1',
presence_joints_menuiserie: '0'
});
const baies = [{ donnee_entree: { surface_totale_baie: 10, presence_joint: 1 } }];

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

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

/**
Expand Down Expand Up @@ -533,9 +580,9 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
expect.objectContaining({ isolation_surfaces: '1', presence_joints_menuiserie: '0' })
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('La ligne q4pa_conv 10 du DPE est conservée')
expect.stringContaining('La ligne q4pa_conv 10 du DPE')
);
// les joints de la ligne reprise concordent : pas de second message
// un seul message pour la ligne reprise
expect(errorSpy).toHaveBeenCalledTimes(1);
errorSpy.mockRestore();
});
Expand Down Expand Up @@ -607,7 +654,7 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
errorSpy.mockRestore();
});

test('presence_joint non exporté : seuls les joints de la ligne du DPE sont repris', async () => {
test('presence_joint non exporté : la ligne n’est pas reprise, même pour les joints', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {});
const { calc_ventilation_bug, tvBug } = await chargerAvecBug(ROW_12);
const { murs } = surfacesIsolantes();
Expand All @@ -617,7 +664,7 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>

expect(tvBug).toHaveBeenLastCalledWith(
'q4pa_conv',
expect.objectContaining({ isolation_surfaces: '1', presence_joints_menuiserie: '1' })
expect.objectContaining({ isolation_surfaces: '1', presence_joints_menuiserie: '0' })
);
});
});
Expand Down Expand Up @@ -716,31 +763,6 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
});
});

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);
});
});

describe('isLigneQ4paMemePeriode', () => {
test('période contenue dans une liste « a|b|c » : vrai', () => {
expect(
Expand All @@ -766,18 +788,59 @@ describe('isLigneQ4paMemePeriode', () => {
});
});

describe('isLigneQ4paSurfacesIsolees', () => {
describe('isLigneQ4paPlusFavorable', () => {
test('ligne « surfaces isolées » et recalcul « non isolées » : vrai', () => {
expect(isLigneQ4paSurfacesIsolees({ isolation_surfaces: '1' }, '0')).toBe(true);
expect(isLigneQ4paSurfacesIsolees({ isolation_surfaces: 1 }, 0)).toBe(true);
expect(isLigneQ4paPlusFavorable({ isolation_surfaces: '1' }, '0', '0')).toBe(true);
expect(isLigneQ4paPlusFavorable({ isolation_surfaces: 1 }, '0', '1')).toBe(true);
});

test('ligne « avec joints » et recalcul « sans joints », isolation concordante : vrai', () => {
expect(
isLigneQ4paPlusFavorable(
{ isolation_surfaces: '0', presence_joints_menuiserie: '1' },
'0',
'0'
)
).toBe(true);
});

test('ligne « non isolées » et recalcul « isolées » : faux, même si la ligne a des joints', () => {
expect(isLigneQ4paPlusFavorable({ isolation_surfaces: '0' }, '1', '0')).toBe(false);
expect(
isLigneQ4paPlusFavorable(
{ isolation_surfaces: '0', presence_joints_menuiserie: '1' },
'1',
'0'
)
).toBe(false);
});

test('ligne « non isolées » et recalcul « isolées » : faux (sens inverse)', () => {
expect(isLigneQ4paSurfacesIsolees({ isolation_surfaces: '0' }, '1')).toBe(false);
test('ligne « sans joints » et recalcul « avec joints » : faux', () => {
expect(
isLigneQ4paPlusFavorable(
{ isolation_surfaces: '0', presence_joints_menuiserie: '0' },
'0',
'1'
)
).toBe(false);
});

test('valeurs concordantes ou ligne sans critère : faux', () => {
expect(isLigneQ4paSurfacesIsolees({ isolation_surfaces: '1' }, '1')).toBe(false);
expect(isLigneQ4paSurfacesIsolees({}, '0')).toBe(false);
expect(isLigneQ4paPlusFavorable({ isolation_surfaces: '1' }, '1', '0')).toBe(false);
expect(
isLigneQ4paPlusFavorable(
{ isolation_surfaces: '0', presence_joints_menuiserie: '1' },
'0',
'1'
)
).toBe(false);
expect(isLigneQ4paPlusFavorable({}, '0', '0')).toBe(false);
expect(
isLigneQ4paPlusFavorable(
{ isolation_surfaces: null, presence_joints_menuiserie: '' },
'0',
'0'
)
).toBe(false);
});
});
38 changes: 38 additions & 0 deletions test/fixtures/2606E1314062H.xml

Large diffs are not rendered by default.

Loading