diff --git a/impact/impact/settings.py b/impact/impact/settings.py
index 8903ff285..69ab86aba 100644
--- a/impact/impact/settings.py
+++ b/impact/impact/settings.py
@@ -271,6 +271,8 @@
"data:", # some images are defined this way
# matomo :
"stats.beta.gouv.fr",
+ # S3 scaleway :
+ "sites-faciles.s3.fr-par.scw.cloud",
],
"style-src": [
SELF,
diff --git a/impact/reglementations/enums.py b/impact/reglementations/enums.py
index 35af3ca40..4e9bfd40e 100644
--- a/impact/reglementations/enums.py
+++ b/impact/reglementations/enums.py
@@ -390,6 +390,7 @@ class EtapeCSRD:
"selection-enjeux",
"analyse-materialite",
"collection-donnees-entreprise",
+ "redaction-rapport-durabilite",
]
@classmethod
@@ -433,4 +434,8 @@ def get(cls, id_etape):
id="collection-donnees-entreprise",
nom="Collecter les données de son entreprise",
),
+ EtapeCSRD(
+ id="redaction-rapport-durabilite",
+ nom="Rédiger son rapport de durabilité",
+ ),
]
diff --git a/impact/reglementations/forms/csrd.py b/impact/reglementations/forms/csrd.py
index 6a390ba6a..2d2fbed89 100644
--- a/impact/reglementations/forms/csrd.py
+++ b/impact/reglementations/forms/csrd.py
@@ -25,13 +25,17 @@ def __init__(self, *args, esrs: str = None, **kwargs):
if self.instance:
qs = self.instance.enjeux_par_esrs(self.esrs)
self.fields["enjeux"].queryset = qs
- self.initial = {"enjeux": qs.filter(selection=True)}
+ self.initial = {"enjeux": qs.selectionnes()}
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
for enjeu in self.instance.enjeux.filter(esrs=self.esrs):
enjeu.selection = enjeu in self.cleaned_data["enjeux"]
+ if not enjeu.selection:
+ # si un enjeu n'est pas sélectionné, il ne peut pas être analysé
+ # (raz éventuelle de l'analyse si on déselectionne l'enjeu)
+ enjeu.materiel = None
enjeu.save()
def sections(self):
diff --git a/impact/reglementations/models/csrd.py b/impact/reglementations/models/csrd.py
index 76cab6532..8db01772f 100644
--- a/impact/reglementations/models/csrd.py
+++ b/impact/reglementations/models/csrd.py
@@ -189,13 +189,16 @@ def modifiables(self):
return self.filter(modifiable=True)
def materiels(self):
- return self.filter(materiel=True)
+ return self.selectionnes().filter(materiel=True)
def non_materiels(self):
- return self.filter(materiel=False)
+ return self.selectionnes().filter(materiel=False)
+
+ def analyses(self):
+ return self.selectionnes().filter(materiel__isnull=False)
def non_analyses(self):
- return self.filter(materiel__isnull=True)
+ return self.selectionnes().filter(materiel__isnull=True)
def environnement(self):
return self.filter(esrs__startswith="ESRS_E")
diff --git a/impact/reglementations/templates/reglementations/csrd/etape-collection-donnees-entreprise.html b/impact/reglementations/templates/reglementations/csrd/etape-collection-donnees-entreprise.html
index e019c6d31..6f56cc3a0 100644
--- a/impact/reglementations/templates/reglementations/csrd/etape-collection-donnees-entreprise.html
+++ b/impact/reglementations/templates/reglementations/csrd/etape-collection-donnees-entreprise.html
@@ -12,9 +12,49 @@
{{ etape.nom }}
-
- Étape en préparation : vous pourrez bientôt télécharger vos données matérielles et non-matérielles.
-
+
Analyse de la matérialité des informations élémentaires (points de données ou « data points »)
+
+
La phase 1 "analyse de double matérialité" vous a permis de déterminer vos enjeux de durabilité matériels. Cette phase 2 vous permet de déterminer les indicateurs pertinents sur lesquels vous allez reporter, puis collecter les données exigées.
+
+
La matérialité des informations s’appréhende en fonction des critères suivants : (i) l’importance de l’information élémentaire pour décrire l’enjeu ou (ii) son utilité pour répondre aux besoins des utilisateurs.
+
+
+
+
+
+
Que faire ?
+
+ Téléchargez les exigences de publications des ESRS de vos enjeux ESG prioritaires, puis sélectionnez les points de données à reporter.
+
+
+ Complétez, si pertinent, votre analyse avec les exigences de publication des autres ESRS.
+
+
+
+
+
+
+
+
+
Pour vous aidez, vous pouvez consulter le logigramme permettant de déterminer les informations à inclure au titre des ESRS.
+
+
+
+
+ Consulter le logigramme
+
+
+
+
+
+
+
En savoir plus sur les démarches de collecte des données
@@ -29,4 +69,7 @@
{{ etape.nom }}
+
+ {% include "snippets/csrd_submit.html" with prochaine_etape="redaction-rapport-durabilite" %}
+
{% endblock %}
diff --git a/impact/reglementations/templates/reglementations/csrd/etape-redaction-rapport-durabilite.html b/impact/reglementations/templates/reglementations/csrd/etape-redaction-rapport-durabilite.html
new file mode 100644
index 000000000..aaf6e6318
--- /dev/null
+++ b/impact/reglementations/templates/reglementations/csrd/etape-redaction-rapport-durabilite.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block title %}Collection des données de l'entreprise{% endblock %}
+
+{% block content %}
+
+ {% include "snippets/csrd_header.html" %}
+
+
+
+ {% include "snippets/gestion_csrd_menu.html" %}
+
+
+
{{ etape.nom }}
+
+ Étape en préparation : vous pourrez bientôt enregistrer le lien de téléchargement de votre Rapport de Durabilité.
+
+
+
+
+
+{% endblock %}
diff --git a/impact/reglementations/tests/test_csrd_views.py b/impact/reglementations/tests/test_csrd_views.py
index 7808242a3..7fef9af27 100644
--- a/impact/reglementations/tests/test_csrd_views.py
+++ b/impact/reglementations/tests/test_csrd_views.py
@@ -1,9 +1,11 @@
from datetime import date
from datetime import datetime
+from io import BytesIO
import pytest
from django.contrib.messages import WARNING
from django.urls import reverse
+from openpyxl import load_workbook
from pytest_django.asserts import assertTemplateUsed
from habilitations.models import attach_user_to_entreprise
@@ -128,6 +130,8 @@ def test_guide_de_la_csrd_par_etape(etape, client, alice, entreprise_factory):
"/csrd/{siren}/etape-introduction",
"/csrd/{siren}/etape-selection-enjeux",
"/csrd/{siren}/etape-analyse-materialite",
+ "/csrd/{siren}/etape-collection-donnees-entreprise",
+ "/csrd/{siren}/etape-redaction-rapport-durabilite",
],
)
def test_gestion_de_la_csrd(etape, client, alice, entreprise_factory):
@@ -155,22 +159,38 @@ def test_gestion_de_la_csrd(etape, client, alice, entreprise_factory):
assertTemplateUsed(
response, "reglementations/csrd/etape-analyse-materialite.html"
)
+ elif etape.endswith("collection-donnees-entreprise"):
+ assertTemplateUsed(
+ response, "reglementations/csrd/etape-collection-donnees-entreprise.html"
+ )
+ elif etape.endswith("redaction-rapport-durabilite"):
+ assertTemplateUsed(
+ response, "reglementations/csrd/etape-redaction-rapport-durabilite.html"
+ )
+ rapport_csrd = RapportCSRD.objects.get(proprietaire=alice, entreprise=entreprise)
+ NOMBRE_ENJEUX = 103
+ assert len(rapport_csrd.enjeux.all()) == NOMBRE_ENJEUX
+
+
+def test_étape_inexistante_de_la_csrd(client, alice, entreprise_factory):
+ entreprise = entreprise_factory()
+ attach_user_to_entreprise(alice, entreprise, "Présidente")
+ client.force_login(alice)
etape_inexistante = f"/csrd/{entreprise.siren}/etape-4"
+
response = client.get(etape_inexistante)
assert response.status_code == 404
- rapport_csrd = RapportCSRD.objects.get(proprietaire=alice, entreprise=entreprise)
- NOMBRE_ENJEUX = 103
- assert len(rapport_csrd.enjeux.all()) == NOMBRE_ENJEUX
-
@pytest.mark.parametrize(
"etape",
[
"introduction",
"selection-enjeux",
+ "analyse-materialite",
+ "collection-donnees-entreprise",
],
)
def test_enregistrement_de_l_étape_de_la_csrd(etape, client, alice, entreprise_factory):
@@ -188,9 +208,6 @@ def test_enregistrement_de_l_étape_de_la_csrd(etape, client, alice, entreprise_
response = client.post(url, follow=True)
- content = response.content.decode("utf-8")
- assert "" in content
-
rapport_csrd = RapportCSRD.objects.get(proprietaire=alice, entreprise=entreprise)
assert rapport_csrd.etape_validee == etape
@@ -200,6 +217,8 @@ def test_enregistrement_de_l_étape_de_la_csrd(etape, client, alice, entreprise_
[
"introduction",
"selection-enjeux",
+ "analyse-materialite",
+ "collection-donnees-entreprise",
],
)
def test_enregistrement_de_l_étape_de_la_csrd_retourne_une_404_si_aucune_CSRD(
@@ -377,3 +396,119 @@ def test_liste_des_enjeux_csrd(client, alice, entreprise_non_qualifiee):
assert "" in response.content.decode(
"utf-8"
)
+
+
+def test_datapoints_pour_enjeux_materiels_au_format_xlsx(
+ client, alice, entreprise_non_qualifiee
+):
+ attach_user_to_entreprise(alice, entreprise_non_qualifiee, "Présidente")
+ csrd = RapportCSRD.objects.create(
+ proprietaire=alice,
+ entreprise=entreprise_non_qualifiee,
+ annee=f"{datetime.now():%Y}",
+ )
+ enjeux = csrd.enjeux.all()
+ enjeu_attenuation = enjeux[1]
+ enjeu_attenuation.selection = True
+ enjeu_attenuation.materiel = True
+ enjeu_attenuation.save()
+ esrs_materielle = enjeu_attenuation.esrs
+ client.force_login(alice)
+
+ response = client.get(
+ f"/csrd/{entreprise_non_qualifiee.siren}/datapoints.xlsx",
+ )
+
+ assert (
+ response["content-type"]
+ == "application/vnd.openxmlformatsofficedocument.spreadsheetml.sheet"
+ )
+ workbook = load_workbook(filename=BytesIO(response.content))
+ noms_onglet = workbook.get_sheet_names()
+ assert esrs_materielle.replace("_", " ") in noms_onglet
+ assert "Index" in noms_onglet
+ assert "ESRS 2" in noms_onglet
+ assert "ESRS2 MDR" in noms_onglet
+ assert "ESRS G1" not in noms_onglet
+
+
+def test_datapoints_pour_enjeux_non_materiels_au_format_xlsx(
+ client, alice, entreprise_non_qualifiee
+):
+ attach_user_to_entreprise(alice, entreprise_non_qualifiee, "Présidente")
+ csrd = RapportCSRD.objects.create(
+ proprietaire=alice,
+ entreprise=entreprise_non_qualifiee,
+ annee=f"{datetime.now():%Y}",
+ )
+ enjeux = csrd.enjeux.all()
+
+ # enjeu de l'ESRS_E1:
+ enjeu_attenuation = enjeux[1]
+ enjeu_attenuation.selection = True
+ enjeu_attenuation.materiel = True
+ enjeu_attenuation.save()
+ esrs_materielle = enjeu_attenuation.esrs
+ client.force_login(alice)
+
+ # note : les enjeux affichés dans le fichier "non-matériels"
+ # doivent au préalable avoir été sélectionnés
+
+ # enjeu de l'ESRS_G1:
+ enjeux_G1 = enjeux.filter(esrs="ESRS_G1").first()
+ enjeux_G1.selection = True
+ enjeux_G1.materiel = False # et pas None
+ enjeux_G1.save()
+
+ response = client.get(
+ f"/csrd/{entreprise_non_qualifiee.siren}/datapoints.xlsx?materiel=false",
+ )
+
+ assert (
+ response["content-type"]
+ == "application/vnd.openxmlformatsofficedocument.spreadsheetml.sheet"
+ )
+ workbook = load_workbook(filename=BytesIO(response.content))
+ noms_onglet = workbook.sheetnames
+ assert esrs_materielle.replace("_", " ") not in noms_onglet
+ assert "Index" in noms_onglet
+ assert "ESRS 2" in noms_onglet
+ assert "ESRS2 MDR" in noms_onglet
+ assert "ESRS G1" in noms_onglet
+
+
+def test_datapoints_csrd__au_format_xlsx_retourne_une_404_si_entreprise_inexistante(
+ client, alice
+):
+ client.force_login(alice)
+
+ response = client.get(
+ f"/csrd/000000001/datapoints.xlsx",
+ )
+
+ assert response.status_code == 404
+
+
+def test_datapoints_csrd_au_format_xlsx_retourne_une_404_si_habilitation_inexistante(
+ client, alice, entreprise_non_qualifiee
+):
+ client.force_login(alice)
+
+ response = client.get(
+ f"/csrd/{entreprise_non_qualifiee.siren}/datapoints.xlsx",
+ )
+
+ assert response.status_code == 404
+
+
+def test_datapoints_csrd_au_format_xlsx_retourne_une_404_si_csrd_inexistante(
+ client, alice, entreprise_non_qualifiee
+):
+ attach_user_to_entreprise(alice, entreprise_non_qualifiee, "Présidente")
+ client.force_login(alice)
+
+ response = client.get(
+ f"/csrd/{entreprise_non_qualifiee.siren}/datapoints.xlsx",
+ )
+
+ assert response.status_code == 404
diff --git a/impact/reglementations/urls.py b/impact/reglementations/urls.py
index a948b48f1..6cc0d29cc 100644
--- a/impact/reglementations/urls.py
+++ b/impact/reglementations/urls.py
@@ -114,6 +114,11 @@
views.csrd.enjeux_materiels_xlsx,
name="enjeux_materiels_xlsx",
),
+ path(
+ "csrd/
/datapoints.xlsx",
+ views.csrd.datapoints_xlsx,
+ name="datapoints_xlsx",
+ ),
]
# Fragments HTMX
diff --git a/impact/reglementations/views/csrd.py b/impact/reglementations/views/csrd.py
index c627c75d6..37b2d33cc 100644
--- a/impact/reglementations/views/csrd.py
+++ b/impact/reglementations/views/csrd.py
@@ -16,6 +16,7 @@
from django.template.loader import get_template
from django.template.loader import TemplateDoesNotExist
from django.urls import reverse_lazy
+from openpyxl import load_workbook
from openpyxl import Workbook
from entreprises.models import CaracteristiquesAnnuelles
@@ -581,7 +582,7 @@ def gestion_csrd(request, siren=None, id_etape="introduction"):
match EtapeCSRD.get(id_etape).id:
## légèrement plus lisible qu'un `if`
case "collection-donnees-entreprise":
- nb_enjeux_non_analyses = csrd.enjeux.selectionnes().non_analyses().count()
+ nb_enjeux_non_analyses = csrd.enjeux.non_analyses().count()
context |= {
"can_download": nb_enjeux_non_analyses
!= csrd.enjeux.selectionnes().count(),
@@ -659,6 +660,11 @@ def _build_xlsx(enjeux, csrd=None, materiels=False):
numero_ligne += 1
+ filename = "enjeux_csrd.xlsx" if not materiels else "enjeux_csrd_materiels.xlsx"
+ return _xlsx_response(workbook, filename)
+
+
+def _xlsx_response(workbook, filename):
with NamedTemporaryFile() as tmp:
workbook.save(tmp.name)
tmp.seek(0)
@@ -668,7 +674,42 @@ def _build_xlsx(enjeux, csrd=None, materiels=False):
xlsx_stream,
content_type="application/vnd.openxmlformatsofficedocument.spreadsheetml.sheet",
)
- filename = "enjeux_csrd.xlsx" if not materiels else "enjeux_csrd_materiels.xlsx"
- response["Content-Disposition"] = f"filename='{filename}'"
+ response["Content-Disposition"] = f"filename={filename}"
return response
+
+
+@login_required
+@csrd_required
+def datapoints_xlsx(request, _, csrd=None):
+ materiel = request.GET.get("materiel", True) != "false"
+ esrs_a_supprimer = _esrs_materiel_a_supprimer(csrd, materiel)
+ workbook = load_workbook("impact/static/CSRD/ESRS_Data_Points_EFRAG.xlsx")
+ for esrs in esrs_a_supprimer:
+ titre_onglet = esrs.replace("_", " ")
+ workbook.remove(workbook[titre_onglet])
+
+ filename = (
+ "datapoints_csrd_materiels.xlsx"
+ if materiel
+ else "datapoints_csrd_non_materiels.xlsx"
+ )
+ return _xlsx_response(workbook, filename)
+
+
+def _esrs_materiel_a_supprimer(csrd: RapportCSRD, materiel: bool):
+ tous_les_esrs = set(ESRS.values)
+
+ if materiel:
+ enjeux_materiels = csrd.enjeux.materiels()
+ esrs_materiels = set((enjeu.esrs for enjeu in enjeux_materiels))
+ esrs_a_supprimer = tous_les_esrs - esrs_materiels
+ else:
+ enjeux_non_materiels = csrd.enjeux.non_materiels()
+ esrs_non_materiels = set((enjeu.esrs for enjeu in enjeux_non_materiels))
+ esrs_a_supprimer = tous_les_esrs - esrs_non_materiels
+
+ # ne pas supprimer ESRS_1 et ESRS_2 car ils n'existent pas dans le fichier .xlsx
+ esrs_a_supprimer -= set(("ESRS_1", "ESRS_2"))
+
+ return esrs_a_supprimer
diff --git a/impact/reglementations/views/fragments/enjeux_materiels.py b/impact/reglementations/views/fragments/enjeux_materiels.py
index 6556aab17..0b14de148 100644
--- a/impact/reglementations/views/fragments/enjeux_materiels.py
+++ b/impact/reglementations/views/fragments/enjeux_materiels.py
@@ -30,10 +30,10 @@ def _grouper_enjeux_par_esrs(enjeux):
{
"titre": TitreESRS[esrs].value,
"esrs": esrs,
- "analyses": len(enjeux.filter(esrs=esrs, materiel__isnull=False)),
+ "analyses": len(enjeux.analyses().filter(esrs=esrs)),
"a_analyser": len(enjeux_),
"enjeux": enjeux_,
- "nb_materiels": len(enjeux.filter(esrs=esrs, materiel=True)),
+ "nb_materiels": len(enjeux.materiels().filter(esrs=esrs)),
}
)
@@ -46,7 +46,7 @@ def _grouper_enjeux_par_esrs(enjeux):
def rafraichissement_enjeux_materiels(request, csrd_id):
csrd = get_object_or_404(RapportCSRD, id=csrd_id)
enjeux_selectionnes = csrd.enjeux.selectionnes()
- enjeux_non_analyses = enjeux_selectionnes.non_analyses()
+ enjeux_non_analyses = csrd.enjeux.non_analyses()
context = {
"csrd": csrd,
diff --git a/impact/static/CSRD/ESRS_Data_Points_EFRAG.xlsx b/impact/static/CSRD/ESRS_Data_Points_EFRAG.xlsx
new file mode 100644
index 000000000..a9363df3f
Binary files /dev/null and b/impact/static/CSRD/ESRS_Data_Points_EFRAG.xlsx differ