Skip to content
Open
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
55 changes: 54 additions & 1 deletion src/3.4_pont_thermique.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,53 @@ function defaultValue(type_liaison, pt_di, de) {
return pourcentageValeurPontThermique(type_liaison, de, k, pt_di);
}

/**
* Mode bug_for_bug_compat uniquement - pont thermique « menuiserie / mur »
*
* Certains logiciels (DPEWIN 3.1.x / 5.1.6, moteur 3cl_tribu_1.4.25.1) exportent une largeur de dormant
* (ex : 5 cm) sur la menuiserie alors que le tv_pont_thermique_id et le k exportés pour le pont thermique
* correspondent à la ligne d'une autre largeur de dormant (ex : 10 cm).
* Normativement la ligne recalculée est la bonne (largeur du dormant « arrondie à la valeur la plus proche »),
* mais pour rester fidèle au calcul certifié, on reprend la ligne exportée par le DPE si :
* - elle ne diffère de la ligne recalculée que par la largeur du dormant
* (même type de liaison, isolation du mur, type de pose et présence de retour d'isolation),
* - son k est égal au k exporté dans les données intermédiaires du pont thermique.
*
* @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4.5 Menuiserie / mur
* @see https://github.com/Open3CL/engine/issues/296
*
* @param row {object|undefined} ligne de la table pont_thermique recalculée
* @param tv_id_dpe {string|number|undefined} tv_pont_thermique_id exporté par le DPE
* @param pt_di {Donnee_intermediaire} données intermédiaires exportées du pont thermique
* @returns {object|undefined} ligne à utiliser pour le pont thermique
*/
export function ligneDpeSiSeulDormantDiffere(row, tv_id_dpe, pt_di) {
if (!row || isNil(tv_id_dpe) || isNil(pt_di?.k)) {
return row;
}

const rowDpe = tv('pont_thermique', { tv_pont_thermique_id: tv_id_dpe });

if (
rowDpe &&
String(rowDpe.tv_pont_thermique_id) === String(tv_id_dpe) &&
rowDpe.enum_type_liaison_id === row.enum_type_liaison_id &&
rowDpe.largeur_dormant &&
rowDpe.largeur_dormant !== row.largeur_dormant &&
rowDpe.isolation_mur === row.isolation_mur &&
rowDpe.type_pose === row.type_pose &&
rowDpe.presence_retour_isolation === row.presence_retour_isolation &&
Number(rowDpe.k) === Number(pt_di.k)
) {
console.warn(
`Pont thermique menuiserie / mur : reprise de la ligne tv_pont_thermique_id = ${tv_id_dpe} du DPE (largeur de dormant ${rowDpe.largeur_dormant} au lieu de ${row.largeur_dormant})`
);
return rowDpe;
}

return row;
}

function tv_k(pt_di, di, de, du, pc_id, logement) {
const enveloppe = logement.enveloppe;

Expand All @@ -41,6 +88,7 @@ function tv_k(pt_di, di, de, du, pc_id, logement) {
const porte_list = enveloppe.porte_collection.porte || [];

const type_liaison = requestInput(de, du, 'type_liaison');
const tv_id_dpe = de.tv_pont_thermique_id;

if (!de.reference_1) {
console.warn(`BUG: pas de reference pour le pont thermique ${de.description}...`);
Expand Down Expand Up @@ -418,7 +466,12 @@ function tv_k(pt_di, di, de, du, pc_id, logement) {
}
}

const row = tv('pont_thermique', matcher);
let row = tv('pont_thermique', matcher);

if (bug_for_bug_compat && type_liaison === 'menuiserie / mur') {
row = ligneDpeSiSeulDormantDiffere(row, tv_id_dpe, pt_di);
}

if (row) {
di.k = Number(row.k);
de.tv_pont_thermique_id = Number(row.tv_pont_thermique_id);
Expand Down
180 changes: 177 additions & 3 deletions src/3.4_pont_thermique.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ vi.mock('./utils.js', () => ({
}
}));

const { default: calc_pont_thermique } = await import('./3.4_pont_thermique.js');
const { default: calc_pont_thermique, ligneDpeSiSeulDormantDiffere } =
await import('./3.4_pont_thermique.js');
const { tv } = await import('./utils.js');

/** Enveloppe minimale avec collections contrôlées. */
Expand Down Expand Up @@ -752,7 +753,9 @@ describe('calc_pont_thermique - liaison menuiserie / mur', () => {

calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));

expect(tv).toHaveBeenLastCalledWith(
// 3e appel tv : vérification de la ligne exportée (issue #296), sans effet ici
expect(tv).toHaveBeenNthCalledWith(
2,
'pont_thermique',
expect.objectContaining({ type_pose: 'nu extérieur' })
);
Expand All @@ -769,7 +772,8 @@ describe('calc_pont_thermique - liaison menuiserie / mur', () => {

calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));

expect(tv).toHaveBeenLastCalledWith(
expect(tv).toHaveBeenNthCalledWith(
2,
'pont_thermique',
expect.objectContaining({ type_pose: 'tunnel' })
);
Expand Down Expand Up @@ -1017,3 +1021,173 @@ describe('calc_pont_thermique - plancher sur paroi non déperditive (issue #212)
expect(pt.donnee_intermediaire.k).toBe(0.71);
});
});

/**
* Reprise de la ligne « menuiserie / mur » exportée par le DPE quand seule la largeur du dormant
* diffère de la ligne recalculée (compatibilité bug uniquement).
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4.5
* @see https://github.com/Open3CL/engine/issues/296
*/
describe('calc_pont_thermique - ligne DPE reprise si seul le dormant diffère (issue #296)', () => {
/** Ligne recalculée : dormant 5 cm. */
const ligneRecalculee = {
tv_pont_thermique_id: '71',
enum_type_liaison_id: '5',
isolation_mur: 'Non isolé',
type_pose: 'Nu Extérieur',
largeur_dormant: '5',
k: '0.43'
};
/** Ligne exportée par le DPE : dormant 10 cm, mêmes autres critères. */
const ligneDpe = {
...ligneRecalculee,
tv_pont_thermique_id: '72',
largeur_dormant: '10',
k: '0.29'
};

function cas(k = 0.29) {
const mur = [
{
donnee_entree: {
reference: 'M1',
enum_type_adjacence_id: '1',
enum_materiaux_structure_mur_id: '1',
type_isolation: 'non isolé'
},
donnee_utilisateur: {}
}
];
const baie = {
donnee_entree: {
reference: 'B1',
type_pose: 'nu extérieur',
presence_retour_isolation: 0,
largeur_dormant: 5
},
donnee_utilisateur: {}
};
const pt = pontThermique(
{
type_liaison: 'menuiserie / mur',
enum_type_liaison_id: '5',
reference_1: 'B1',
reference_2: 'M1',
description: 'menuiserie / mur',
tv_pont_thermique_id: '72'
},
k
);
return { pt, log: logement({ mur, bv: [baie] }) };
}

/** Mock de la table : recherche par identifiant → ligne DPE, sinon ligne recalculée. */
function mockTable(dpe = ligneDpe) {
tv.mockImplementation((table, matcher) =>
matcher.tv_pont_thermique_id ? dpe : ligneRecalculee
);
}

test('cas 2375E2293948T : dormant exporté 5 mais ligne DPE « dormant 10 » avec k exporté : ligne DPE reprise', () => {
state.bug = true;
mockTable();
const { pt, log } = cas();

calc_pont_thermique(pt, '1', log);

expect(tv).toHaveBeenCalledWith(
'pont_thermique',
expect.objectContaining({ largeur_dormant: 5 })
);
expect(tv).toHaveBeenLastCalledWith('pont_thermique', { tv_pont_thermique_id: '72' });
expect(pt.donnee_intermediaire.k).toBe(0.29);
expect(pt.donnee_entree.tv_pont_thermique_id).toBe(72);
expect(warnSpy).toHaveBeenCalled();
});

test('sans compatibilité bug : ligne recalculée conservée (règle normative), table non relue', () => {
mockTable();
const { pt, log } = cas();

calc_pont_thermique(pt, '1', log);

expect(tv).toHaveBeenCalledTimes(1);
expect(pt.donnee_intermediaire.k).toBe(0.43);
expect(pt.donnee_entree.tv_pont_thermique_id).toBe(71);
});

test('compatibilité bug mais k exporté différent du k de la ligne DPE : ligne recalculée conservée', () => {
state.bug = true;
mockTable();
const { pt, log } = cas(0.43);

calc_pont_thermique(pt, '1', log);

expect(pt.donnee_intermediaire.k).toBe(0.43);
expect(pt.donnee_entree.tv_pont_thermique_id).toBe(71);
});

test('compatibilité bug, liaison autre que menuiserie / mur : aucune relecture de la ligne DPE', () => {
state.bug = true;
tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '10' });
const pt = pontThermique(
{
type_liaison: 'refend / mur',
reference_1: 'M1',
description: 'refend',
tv_pont_thermique_id: '72'
},
0.29
);

calc_pont_thermique(pt, '1', logement());

expect(tv).toHaveBeenCalledTimes(1);
expect(pt.donnee_intermediaire.k).toBe(0.5);
});

describe('ligneDpeSiSeulDormantDiffere', () => {
test('ligne DPE ne différant que par le dormant et k identique : ligne DPE retournée', () => {
tv.mockReturnValue(ligneDpe);

expect(ligneDpeSiSeulDormantDiffere(ligneRecalculee, 72, { k: 0.29 })).toBe(ligneDpe);
expect(tv).toHaveBeenCalledWith('pont_thermique', { tv_pont_thermique_id: 72 });
});

test('aucune ligne recalculée : rien n’est repris', () => {
expect(ligneDpeSiSeulDormantDiffere(undefined, 72, { k: 0.29 })).toBeUndefined();
expect(tv).not.toHaveBeenCalled();
});

test.each([
['tv_pont_thermique_id absent', undefined, { k: 0.29 }],
['tv_pont_thermique_id nul', null, { k: 0.29 }],
['k exporté absent', 72, {}],
['données intermédiaires absentes', 72, undefined]
])('%s : ligne recalculée conservée sans relire la table', (_, id, di) => {
expect(ligneDpeSiSeulDormantDiffere(ligneRecalculee, id, di)).toBe(ligneRecalculee);
expect(tv).not.toHaveBeenCalled();
});

test.each([
['ligne DPE introuvable', undefined],
[
'identifiant retourné différent (correspondance partielle)',
{ ...ligneDpe, tv_pont_thermique_id: '720' }
],
['type de liaison différent', { ...ligneDpe, enum_type_liaison_id: '1' }],
['largeur de dormant absente', { ...ligneDpe, largeur_dormant: undefined }],
['même largeur de dormant', { ...ligneDpe, largeur_dormant: '5' }],
['isolation du mur différente', { ...ligneDpe, isolation_mur: 'ITI' }],
['type de pose différent', { ...ligneDpe, type_pose: 'Tunnel' }],
['retour d’isolation différent', { ...ligneDpe, presence_retour_isolation: '1' }],
['k de la ligne DPE différent du k exporté', { ...ligneDpe, k: '0.25' }]
])('%s : ligne recalculée conservée', (_, dpe) => {
tv.mockReturnValue(dpe);

expect(ligneDpeSiSeulDormantDiffere(ligneRecalculee, '72', { k: '0.29' })).toBe(
ligneRecalculee
);
});
});
});
54 changes: 46 additions & 8 deletions src/features/dpe/infrastructure/enveloppe/pontThermiqueTv.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export class PontThermiqueTvStore extends TvStore {
return parseFloat(k);
}

/**
* Ligne de la table pont_thermique via tv_pont_thermique_id
*
* @param tvPontThermiqueId {number|string}
* @return {object|undefined}
*/
getById(tvPontThermiqueId) {
return tv['pont_thermique'].find(
(v) => parseInt(v.tv_pont_thermique_id) === parseInt(tvPontThermiqueId)
);
}

/**
* Valeur du pont thermique (W/(m.K))
*
Expand Down Expand Up @@ -90,14 +102,12 @@ export class PontThermiqueTvStore extends TvStore {
presenceRetourIsolation,
largeurDormant
) {
const k = tv['pont_thermique'].find(
(v) =>
parseInt(v.enum_type_liaison_id) === enumTypeLiaisonId &&
v.isolation_mur.toLowerCase() === isolationMur.toLowerCase() &&
(!v.enum_type_pose_id || parseInt(v.enum_type_pose_id) === typePose) &&
(!v.presence_retour_isolation ||
parseInt(v.presence_retour_isolation) === presenceRetourIsolation) &&
(!v.largeur_dormant || !largeurDormant || parseInt(v.largeur_dormant) === largeurDormant)
const k = this.getForMenuiserie(
enumTypeLiaisonId,
isolationMur,
typePose,
presenceRetourIsolation,
largeurDormant
)?.k;

if (!k) {
Expand All @@ -109,4 +119,32 @@ export class PontThermiqueTvStore extends TvStore {

return parseFloat(k);
}

/**
* Ligne de la table pont_thermique pour une liaison menuiserie / mur
*
* @param enumTypeLiaisonId {number}
* @param isolationMur {string}
* @param typePose {number}
* @param presenceRetourIsolation {number}
* @param largeurDormant {number}
* @return {object|undefined}
*/
getForMenuiserie(
enumTypeLiaisonId,
isolationMur,
typePose,
presenceRetourIsolation,
largeurDormant
) {
return tv['pont_thermique'].find(
(v) =>
parseInt(v.enum_type_liaison_id) === enumTypeLiaisonId &&
v.isolation_mur.toLowerCase() === isolationMur.toLowerCase() &&
(!v.enum_type_pose_id || parseInt(v.enum_type_pose_id) === typePose) &&
(!v.presence_retour_isolation ||
parseInt(v.presence_retour_isolation) === presenceRetourIsolation) &&
(!v.largeur_dormant || !largeurDormant || parseInt(v.largeur_dormant) === largeurDormant)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,30 @@ describe('Lecture des tables de valeurs', () => {
).toBe(expected);
}
);

describe('lecture des lignes de la table (issue #296)', () => {
test.each([
{ label: 'identifiant numérique', id: 72, expected: '72' },
{ label: 'identifiant chaîne', id: '72', expected: '72' }
])('ligne via tv_pont_thermique_id ($label)', ({ id, expected }) => {
const ligne = tvStore.getById(id);
expect(ligne.tv_pont_thermique_id).toBe(expected);
expect(ligne.largeur_dormant).toBe('10');
});

test('tv_pont_thermique_id inexistant : aucune ligne', () => {
expect(tvStore.getById(0)).toBeUndefined();
});

test('ligne menuiserie / mur : même critère que getKForMenuiserie', () => {
const ligne = tvStore.getForMenuiserie(5, 'Non isolé', 1, undefined, 5);
expect(ligne.tv_pont_thermique_id).toBe('71');
expect(ligne.largeur_dormant).toBe('5');
expect(parseFloat(ligne.k)).toBe(tvStore.getKForMenuiserie(5, 'Non isolé', 1, undefined, 5));
});

test('ligne menuiserie / mur introuvable : undefined', () => {
expect(tvStore.getForMenuiserie(5, 'undefined', 1, 1, 5)).toBeUndefined();
});
});
});
Loading