Architettura Software Carbon Accounting: Piattaforme ESG
Nel 2025, la rendicontazione delle emissioni di carbonio non e più una scelta volontaria per le grandi aziende europee: e un obbligo di legge. La Corporate Sustainability Reporting Directive (CSRD), entrata in vigore con il D.Lgs. 125/2024 in Italia, ha trasformato radicalmente il panorama del reporting ESG, portando la disciplina dal dominio della comunicazione corporate a quello della compliance regolamentare, con le stesse implicazioni operative e tecniche di un sistema contabile finanziario.
Il mercato globale del software di carbon accounting ha raggiunto 1.8 miliardi di dollari nel 2025 e si prevede superi i 6 miliardi entro il 2030, con un CAGR del 27.4%. Non si tratta semplicemente di comprare una piattaforma SaaS: progettare un sistema di carbon accounting robusto richiede competenze che spaziano dalla chimica dell'atmosfera all'ingegneria del software, dalla statistica al diritto comunitario. I team engineering che si trovano a costruire queste piattaforme devono comprendere il GHG Protocol, gli European Sustainability Reporting Standards (ESRS), i database di fattori di emissione e l'architettura di sistemi distribuiti capaci di raccogliere, calcolare e certificare dati da centinaia di fonti diverse.
Questo articolo e una guida tecnica completa: dalla modellazione del dominio con le entità del GHG Protocol, all'implementazione di un motore di calcolo in Python con FastAPI, fino all'automazione dei report CSRD/CDP/GRI e all'integrazione con la Climatiq API per i fattori di emissione real-time. Se stai costruendo una piattaforma ESG interna, valutando soluzioni enterprise o semplicemente vuoi capire come funzionano queste architetture sotto il cofano, sei nel posto giusto.
Cosa Imparerai in Questo Articolo
- Il GHG Protocol: Scope 1, 2 e 3 e le 15 categorie di emissioni indirette della supply chain
- Come modellare il dominio dati di un sistema carbon accounting (Organization, Facility, EmissionSource, Activity)
- I principali database di emission factors: DEFRA, EPA, ecoinvent, Climatiq API
- Architettura a microservizi: Data Collection, Calculation Engine, Reporting, Audit Trail
- Implementazione Python con FastAPI e pandas per il calcolo delle emissioni
- Automazione Scope 3: integrazione con ERP SAP, dati procurement, travel data
- Generazione automatica report CSRD/ESRS, CDP e GRI Standards
- Audit trail e data lineage: tracciabilita completa dei calcoli
- Confronto piattaforme: Persefoni, Watershed, Sphera, Plan A
- Case study: azienda manifatturiera italiana con reporting CSRD 2025
- Normativa aggiornata: CSRD Omnibus 2025, EU Taxonomy, D.Lgs. 125/2024
Posizione nella Serie EnergyTech
| # | Articolo | Stato |
|---|---|---|
| 1 | MQTT e InfluxDB: Time Series per Dati Energetici | Pubblicato |
| 2 | IEC 61850: Protocollo Standard per Reti Elettriche Intelligenti | Pubblicato |
| 3 | DERMS: Gestione Distribuita delle Risorse Energetiche | Pubblicato |
| 4 | Building Management System: Ottimizzazione Energetica AI | Pubblicato |
| 5 | Renewable Energy Forecasting: ML per Fotovoltaico ed Eolico | Pubblicato |
| 6 | EV Load Balancing: Smart Charging e Vehicle-to-Grid | Pubblicato |
| 7 | Blockchain P2P Energy Trading: Mercati Energetici Decentralizzati | Pubblicato |
| 8 | Sei qui - Architettura Software Carbon Accounting: Piattaforme ESG | Corrente |
| 9 | Digital Twin Energetico: Simulazione e Ottimizzazione | Prossimo |
| 10 | OCPP e Infrastruttura EV: Standard e Implementazione | In arrivo |
Il Contesto Normativo: CSRD, ESRS e D.Lgs. 125/2024
Comprendere la normativa non e un prerequisito burocratico: e la base su cui si progetta l'intera architettura del sistema. Ogni requisito tecnico, ogni campo del database, ogni endpoint dell'API deriva da un obbligo di disclosure specifico.
CSRD e le Onde di Applicazione
La Corporate Sustainability Reporting Directive ha definito tre ondate di applicazione per le aziende europee. La prima ondata (esercizio 2024, report pubblicati nel 2025) ha coinvolto le aziende già soggette alla precedente NFRD: società quotate, banche e assicurazioni con più di 500 dipendenti. La seconda e terza ondata, originariamente previste per il 2025 e 2026, sono state posticipate di due anni dalla Stop-the-Clock Directive pubblicata nella Gazzetta Ufficiale UE il 16 aprile 2025.
Il cambiamento più significativo e arrivato a dicembre 2025 con l'approvazione del pacchetto Omnibus I: la soglia di applicazione obbligatoria e stata alzata a 1.000 dipendenti e 450 milioni di euro di fatturato, riducendo di circa l'80% il numero di aziende soggette alla CSRD obbligatoria. Gli standard ESRS sono in fase di revisione con una riduzione del 61% dei datapoint obbligatori (da circa 1.100 a circa 430), con adozione prevista nella prima meta del 2026 e applicazione dal 2027.
Timeline CSRD Aggiornata (post-Omnibus 2025)
| Ondata | Soggetti | Primo Report | Stato |
|---|---|---|---|
| Wave 1 | Aziende già NFRD (>500 dip., quotate/banche/assicurazioni) | Report 2025 (FY 2024) | In corso |
| Wave 2 | Grandi aziende >1.000 dip. e >450M EUR fatturato | Report 2027 (FY 2026) - posticipato | Posticipato |
| Wave 3 | PMI quotate su mercati regolamentati EU | Report 2028 (FY 2027) - posticipato | Posticipato |
| ESRS rev. | Tutti i soggetti CSRD, standard semplificati | Dal FY 2027 | In consultazione |
D.Lgs. 125/2024: Il Recepimento Italiano
L'Italia ha recepito la CSRD con il Decreto Legislativo 125 del 6 settembre 2024, entrato in vigore il 25 settembre 2024. Il decreto ha abrogato il precedente D.Lgs. 254/2016 che trasponeva la NFRD. Le principali novità per le aziende italiane includono: l'obbligo di assurance limitata (limited assurance) sul report di sostenibilità da parte di un revisore abilitato, l'integrazione del report di sostenibilità nella relazione sulla gestione, e la pubblicazione nella sezione dedicata del Registro delle Imprese.
EU Taxonomy: Il Collegamento
Il carbon accounting si interseca direttamente con la EU Taxonomy Regulation (Reg. UE 852/2020), che classifica le attivita economiche come "sostenibili" in base a sei obiettivi ambientali. Le aziende CSRD devono rendicontare i KPI di allineamento alla Taxonomy (quote di fatturato, CapEx, OpEx "allineati" e "eleggibili"). Con le semplificazioni del 2025, le aziende con meno di 1.000 dipendenti sono esentate dalla Taxonomy reporting, e le restanti possono limitarsi alle attivita che rappresentano almeno il 10% di fatturato, CapEx o OpEx.
GHG Protocol: Il Framework di Riferimento
Il Greenhouse Gas Protocol e lo standard contabile per le emissioni di gas serra più diffuso al mondo, sviluppato dal World Resources Institute (WRI) e dal World Business Council for Sustainable Development (WBCSD). Praticamente tutti i framework di reporting (CSRD/ESRS, CDP, GRI, ISO 14064) fanno riferimento o si basano sul GHG Protocol.
I Tre Scope: Definizioni Precise
La distinzione in tre "ambiti" (scope) permette di attribuire le emissioni in modo chiaro, evitando duplicazioni nel conteggio tra diversi attori della filiera.
Scope 1: Emissioni Dirette
Emissioni provenienti da fonti di proprietà o sotto il controllo dell'organizzazione. Includono: combustione di combustibili fossili in caldaie, forni, veicoli aziendali di proprietà; emissioni di processo (es. CO2 da reazioni chimiche, CH4 da allevamenti); emissioni fuggitive (perdite di refrigeranti, fughe di gas da impianti). I gas GHG rilevanti sono i sette del Protocollo di Kyoto: CO2, CH4, N2O, HFC, PFC, SF6, NF3, tutti convertiti in CO2 equivalente (CO2e) usando i Global Warming Potential (GWP) dell'IPCC.
Scope 2: Emissioni Indirette da Energia Acquistata
Emissioni associate alla generazione di energia elettrica, calore, vapore o raffreddamento acquistati e consumati dall'organizzazione. Il GHG Protocol Scope 2 Guidance (2015) richiede la rendicontazione con due metodi distinti: il metodo location-based (usa il fattore di emissione della rete elettrica nella localita di consumo, es. il fattore medio della rete italiana) e il metodo market-based (usa i fattori degli strumenti di mercato come i Contratti per Differenza, i Certificati di Energia Rinnovabile/GO, i Power Purchase Agreement). Entrambi devono essere rendicontati.
Scope 3: Altre Emissioni Indirette
Le emissioni della catena del valore, divise in 15 categorie upstream e downstream. Sono tipicamente le più significative (in media 70-80% dell'impronta totale) e le più difficili da misurare. Le ESRS richiedono la rendicontazione delle categorie Scope 3 "materiali" identificate attraverso la Double Materiality Assessment.
Le 15 Categorie Scope 3
| # | Categoria | Tipo | Tipica per |
|---|---|---|---|
| 1 | Purchased goods and services | Upstream | Tutti i settori |
| 2 | Capital goods | Upstream | Manifatturiero, edilizia |
| 3 | Fuel and energy related activities | Upstream | Tutti i settori |
| 4 | Upstream transportation and distribution | Upstream | Retail, manifatturiero |
| 5 | Waste generated in operations | Upstream | Manifatturiero, alimentare |
| 6 | Business travel | Upstream | Servizi, tech |
| 7 | Employee commuting | Upstream | Tutti i settori |
| 8 | Upstream leased assets | Upstream | Real estate, retail |
| 9 | Downstream transportation | Downstream | Manifatturiero, FMCG |
| 10 | Processing of sold products | Downstream | Materie prime, chimico |
| 11 | Use of sold products | Downstream | Automotive, elettronica |
| 12 | End-of-life treatment | Downstream | Packaging, beni durevoli |
| 13 | Downstream leased assets | Downstream | Real estate |
| 14 | Franchises | Downstream | Food & beverage, retail |
| 15 | Investments | Downstream | Banche, fondi di investimento |
Data Model: Modellare il Dominio del Carbon Accounting
Il cuore di ogni piattaforma di carbon accounting e un data model robusto che rifletta fedelmente i concetti del GHG Protocol. Vediamo le entità principali con i loro attributi e relazioni.
Entità Principali
Un sistema completo prevede almeno sei entità core: Organization, Facility, EmissionSource, EmissionFactor, Activity e Calculation. Ecco il modello in Python con SQLAlchemy:
# models/core.py - Data model GHG Protocol completo
from sqlalchemy import Column, String, Float, Enum, DateTime, ForeignKey, JSON, Integer
from sqlalchemy.orm import relationship, DeclarativeBase
from datetime import datetime
from enum import Enum as PyEnum
import uuid
class Base(DeclarativeBase):
pass
class ScopeType(PyEnum):
SCOPE_1 = "scope_1"
SCOPE_2_LOCATION = "scope_2_location"
SCOPE_2_MARKET = "scope_2_market"
SCOPE_3 = "scope_3"
class Scope3Category(PyEnum):
CAT_1_PURCHASED_GOODS = "cat_1"
CAT_2_CAPITAL_GOODS = "cat_2"
CAT_3_FUEL_ENERGY = "cat_3"
CAT_4_UPSTREAM_TRANSPORT = "cat_4"
CAT_5_WASTE = "cat_5"
CAT_6_BUSINESS_TRAVEL = "cat_6"
CAT_7_EMPLOYEE_COMMUTING = "cat_7"
CAT_8_UPSTREAM_LEASED = "cat_8"
CAT_9_DOWNSTREAM_TRANSPORT = "cat_9"
CAT_10_PROCESSING = "cat_10"
CAT_11_USE_OF_PRODUCTS = "cat_11"
CAT_12_END_OF_LIFE = "cat_12"
CAT_13_DOWNSTREAM_LEASED = "cat_13"
CAT_14_FRANCHISES = "cat_14"
CAT_15_INVESTMENTS = "cat_15"
class Organization(Base):
__tablename__ = "organizations"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
name = Column(String(255), nullable=False)
legal_entity_id = Column(String(100)) # LEI o P.IVA
country_code = Column(String(3), nullable=False) # ISO 3166-1 alpha-3
nace_code = Column(String(10)) # Codice settore NACE Rev.2
reporting_year = Column(Integer, nullable=False)
consolidation_approach = Column(String(50)) # equity_share, financial_control, operational_control
base_year = Column(Integer) # anno di riferimento per i target
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
facilities = relationship("Facility", back_populates="organization")
calculations = relationship("Calculation", back_populates="organization")
class Facility(Base):
__tablename__ = "facilities"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
organization_id = Column(String, ForeignKey("organizations.id"), nullable=False)
name = Column(String(255), nullable=False)
facility_type = Column(String(50)) # manufacturing_plant, office, warehouse, data_center
address = Column(String(500))
country_code = Column(String(3), nullable=False)
latitude = Column(Float)
longitude = Column(Float)
grid_region = Column(String(50)) # es. "IT_NORD", "DE_TENNET" per Scope 2
floor_area_sqm = Column(Float)
is_owned = Column(String(10)) # owned, leased, operated
operational_start = Column(DateTime)
organization = relationship("Organization", back_populates="facilities")
emission_sources = relationship("EmissionSource", back_populates="facility")
class EmissionSource(Base):
"""Fonte di emissione specifica: caldaia, veicolo, processo, acquisto energia"""
__tablename__ = "emission_sources"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
facility_id = Column(String, ForeignKey("facilities.id"), nullable=False)
name = Column(String(255), nullable=False)
source_type = Column(String(100)) # natural_gas_boiler, diesel_generator, company_car, electricity
scope = Column(Enum(ScopeType), nullable=False)
scope_3_category = Column(Enum(Scope3Category)) # solo per Scope 3
fuel_type = Column(String(50)) # natural_gas, diesel, petrol, LPG
unit_of_measure = Column(String(20)) # kWh, liters, kg, km, tonne
description = Column(String(1000))
is_active = Column(String(10), default="true")
facility = relationship("Facility", back_populates="emission_sources")
activities = relationship("Activity", back_populates="emission_source")
class EmissionFactor(Base):
"""Fattore di emissione da database certificato"""
__tablename__ = "emission_factors"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
source_database = Column(String(50), nullable=False) # DEFRA, EPA, ecoinvent, Climatiq, IPCC
activity_id = Column(String(200)) # ID specifico del database sorgente
name = Column(String(500), nullable=False)
category = Column(String(100))
region = Column(String(50)) # country/region code
year = Column(Integer, nullable=False)
unit_type = Column(String(50)) # kgCO2e/kWh, kgCO2e/liter, kgCO2e/km, kgCO2e/tonne
co2e_factor = Column(Float, nullable=False) # valore principale in kgCO2e
co2_factor = Column(Float) # CO2 separato
ch4_factor = Column(Float) # CH4 separato
n2o_factor = Column(Float) # N2O separato
gwp_version = Column(String(20), default="AR6") # AR5, AR6
lca_activity = Column(String(50)) # upstream, combustion, downstream
source_url = Column(String(500))
last_updated = Column(DateTime)
class Activity(Base):
"""Registrazione di attivita con consumo misurato"""
__tablename__ = "activities"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
emission_source_id = Column(String, ForeignKey("emission_sources.id"), nullable=False)
emission_factor_id = Column(String, ForeignKey("emission_factors.id"))
period_start = Column(DateTime, nullable=False)
period_end = Column(DateTime, nullable=False)
quantity = Column(Float, nullable=False)
unit = Column(String(20), nullable=False)
data_quality = Column(String(20), default="measured") # measured, estimated, calculated, supplier
data_source = Column(String(200)) # nome sistema sorgente: SAP, utility_bill, travel_tool
raw_data = Column(JSON) # dati originali non elaborati
notes = Column(String(1000))
created_by = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
emission_source = relationship("EmissionSource", back_populates="activities")
emission_factor = relationship("EmissionFactor")
calculation = relationship("Calculation", back_populates="activity", uselist=False)
class Calculation(Base):
"""Risultato del calcolo emissioni, immutabile una volta creato"""
__tablename__ = "calculations"
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
organization_id = Column(String, ForeignKey("organizations.id"), nullable=False)
activity_id = Column(String, ForeignKey("activities.id"), nullable=False)
scope = Column(Enum(ScopeType), nullable=False)
scope_3_category = Column(Enum(Scope3Category))
co2e_tonnes = Column(Float, nullable=False) # risultato in tonnellate CO2e
co2_tonnes = Column(Float)
ch4_tonnes = Column(Float)
n2o_tonnes = Column(Float)
calculation_method = Column(String(50)) # spend_based, activity_based, hybrid, supplier_specific
emission_factor_value = Column(Float) # snapshot del fattore usato
emission_factor_unit = Column(String(50))
emission_factor_source = Column(String(100))
calculation_formula = Column(String(500)) # formula usata, per audit
calculated_at = Column(DateTime, default=datetime.utcnow)
calculated_by = Column(String(100)) # user o sistema automatico
version = Column(Integer, default=1) # versioning per ricertificazione
is_verified = Column(String(10), default="false")
organization = relationship("Organization", back_populates="calculations")
activity = relationship("Activity", back_populates="calculation")
Database di Emission Factors: DEFRA, EPA, ecoinvent, Climatiq
I fattori di emissione sono il cuore matematico del carbon accounting: convertono una quantità di attivita (litri di gasolio, kWh consumati, km percorsi, euro spesi) in tonnellate di CO2e. La qualità e l'aggiornamento dei fattori determina direttamente la qualità del report.
I Principali Database
| Database | Gestore | Copertura | Aggiornamento | Accesso |
|---|---|---|---|---|
| DEFRA/BEIS | UK Govt (DESNZ) | UK + internazionale, tutti gli scope | Annuale (luglio) | Gratuito (Excel) |
| EPA GHG Hub | US EPA | USA, mobilita, energia, Scope 3 | Annuale (gennaio) | Gratuito (Excel) |
| ecoinvent | ecoinvent Association | Globale, LCA completo, 18.000+ dataset | Semestrale | Licenza a pagamento |
| IPCC EF Database | IPCC | Globale, inventari nazionali | Con ogni Assessment Report | Gratuito |
| Climatiq API | Climatiq | Multi-source, 50.000+ fattori | Continuo (real-time) | API (freemium) |
| AIB Residual Mix | Association of Issuing Bodies | Europa, Scope 2 market-based | Annuale | Gratuito |
| IEA Electricity | International Energy Agency | Globale, fattori rete elettrica | Annuale | Parzialmente gratuito |
Integrazione Climatiq API
Climatiq fornisce un'API REST con più di 50.000 fattori di emissione verificati, copertura ISO 14067 e GHG Protocol, ed e una delle soluzioni più usate per integrare calcoli di emissioni in modo programmatico. L'API supporta Scope 1, 2 e 3 con metodi activity-based e spend-based.
# services/climatiq_client.py - Integrazione Climatiq API
import httpx
import os
from dataclasses import dataclass
from typing import Optional
from functools import lru_cache
CLIMATIQ_BASE_URL = "https://api.climatiq.io"
@dataclass
class EmissionEstimate:
co2e: float
co2e_unit: str
co2e_calculation_method: str
co2e_calculation_origin: str
emission_factor_name: str
emission_factor_id: str
source: str
year: int
region: str
@dataclass
class ActivityData:
activity_id: str
data: dict # { "energy": { "value": 1000, "energy_unit": "kWh" } }
region: Optional[str] = None
year: Optional[int] = None
class ClimatiqClient:
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or os.environ.get("CLIMATIQ_API_KEY")
if not self.api_key:
raise ValueError("CLIMATIQ_API_KEY not set")
self.client = httpx.AsyncClient(
base_url=CLIMATIQ_BASE_URL,
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
},
timeout=30.0
)
async def estimate_emission(self, activity: ActivityData) -> EmissionEstimate:
"""Calcola emissioni per una singola attivita"""
payload = {
"emission_factor": {
"activity_id": activity.activity_id,
},
**activity.data
}
if activity.region:
payload["emission_factor"]["region"] = activity.region
if activity.year:
payload["emission_factor"]["year"] = activity.year
response = await self.client.post("/estimate", json=payload)
response.raise_for_status()
result = response.json()
return EmissionEstimate(
co2e=result["co2e"],
co2e_unit=result["co2e_unit"],
co2e_calculation_method=result["co2e_calculation_method"],
co2e_calculation_origin=result.get("co2e_calculation_origin", ""),
emission_factor_name=result["emission_factor"]["name"],
emission_factor_id=result["emission_factor"]["activity_id"],
source=result["emission_factor"]["source"],
year=result["emission_factor"]["year"],
region=result["emission_factor"].get("region", "")
)
async def estimate_batch(self, activities: list[ActivityData]) -> list[EmissionEstimate]:
"""Calcolo batch fino a 100 attivita"""
payload = {
"requests": [
{
"emission_factor": {"activity_id": a.activity_id},
**a.data
}
for a in activities
]
}
response = await self.client.post("/batch", json=payload)
response.raise_for_status()
results = response.json()["results"]
return [
EmissionEstimate(
co2e=r["co2e"],
co2e_unit=r["co2e_unit"],
co2e_calculation_method=r["co2e_calculation_method"],
co2e_calculation_origin=r.get("co2e_calculation_origin", ""),
emission_factor_name=r["emission_factor"]["name"],
emission_factor_id=r["emission_factor"]["activity_id"],
source=r["emission_factor"]["source"],
year=r["emission_factor"]["year"],
region=r["emission_factor"].get("region", "")
)
for r in results
]
async def search_emission_factors(
self,
query: str,
region: Optional[str] = None,
year: Optional[int] = None,
source: Optional[str] = None
) -> list[dict]:
"""Ricerca fattori di emissione nel database Climatiq"""
params = {"query": query, "page": 1, "page_size": 20}
if region:
params["region"] = region
if year:
params["year"] = year
if source:
params["source"] = source
response = await self.client.get("/search", params=params)
response.raise_for_status()
return response.json()["results"]
async def __aenter__(self):
return self
async def __aexit__(self, *args):
await self.client.aclose()
# Esempio utilizzo - calcolo Scope 2 location-based per impianto italiano
async def calculate_scope2_italy(kwh_consumed: float) -> float:
"""
Calcola emissioni Scope 2 location-based per consumo elettrico in Italia.
Fattore IEA 2024 per l'Italia: ~0.233 kgCO2e/kWh
"""
async with ClimatiqClient() as client:
activity = ActivityData(
activity_id="electricity-supply_grid-source_supplier_mix",
data={
"energy": {
"value": kwh_consumed,
"energy_unit": "kWh"
}
},
region="IT", # Italia
year=2024
)
estimate = await client.estimate_emission(activity)
# Converti kgCO2e in tonnellate CO2e
return estimate.co2e / 1000 if estimate.co2e_unit == "kg" else estimate.co2e
Architettura a Microservizi per Carbon Accounting
Una piattaforma enterprise di carbon accounting deve gestire flussi di dati eterogenei (bollette energetiche, dati ERP, note spese, dati fornitori), calcoli complessi e auditabili, e la generazione di report in formati diversi. L'architettura a microservizi e la scelta naturale per questi requisiti.
I Quattro Servizi Core
1. Data Collection Service
Responsabile dell'acquisizione dati da fonti diverse: API ERP (SAP, Oracle), utility bills tramite OCR/parser, strumenti di travel management (Concur, TravelPerk), dati procurement, upload manuali CSV, API fornitori. Espone endpoint per l'ingestion e gestisce la normalizzazione delle unita di misura.
2. Calculation Engine
Il cuore computazionale: riceve attivita normalizzate, seleziona i fattori di emissione appropriati (locali o via Climatiq API), applica le formule GHG Protocol, produce risultati immutabili con formula di calcolo esplicita per l'audit. Supporta ricalcoli storici quando i fattori vengono aggiornati.
3. Reporting Service
Genera report nei formati richiesti dai framework: CSRD/ESRS (con tagging XBRL), CDP questionnaire (formato JSON/XML), GRI Standards disclosures, report interni per dashboard. Gestisce il versioning dei report e la firma digitale per l'assurance.
4. Audit Trail Service
Mantiene un log immutabile di ogni operazione: chi ha inserito i dati, quale fattore e stato usato, quando e stato calcolato, chi ha approvato. Supporta il data lineage completo: dal dato sorgente al numero nel report finale. Essenziale per l'assurance da parte dei revisori.
Diagramma Architetturale
┌─────────────────────────────────────────────────────────────────┐
│ CARBON ACCOUNTING PLATFORM │
├─────────────────────────────────────────────────────────────────┤
│ FRONTEND │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Dashboard │ │ Data Input │ │ Report Generator │ │
│ │ (Angular) │ │ Wizard │ │ (CSRD/CDP/GRI) │ │
│ └──────┬──────┘ └──────┬──────┘ └─────────────┬───────────┘ │
├─────────┼────────────────┼─────────────────────────┼────────────┤
│ API GATEWAY (FastAPI/Kong) │
│ │ │ │ │
├─────────┼────────────────┼─────────────────────────┼────────────┤
│ MICROSERVICES │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────────────▼─────────┐ │
│ │ Data │ │ Calculation │ │ Reporting │ │
│ │ Collection │→ │ Engine │→ │ Service │ │
│ │ Service │ │ (Python) │ │ (PDF/XBRL/JSON) │ │
│ └──────┬──────┘ └──────┬──────┘ └────────────────────────┘ │
│ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌────────────────────────┐ │
│ │ SAP │ │ Climatiq │ │ Audit Trail │ │
│ │ Connector │ │ API Client │ │ Service │ │
│ └─────────────┘ └─────────────┘ └────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ DATA LAYER │
│ ┌──────────────┐ ┌─────────────┐ ┌────────────────────────┐ │
│ │ PostgreSQL │ │ TimeSeries │ │ Object Storage │ │
│ │ (Core data) │ │ (InfluxDB) │ │ (S3 - documents) │ │
│ └──────────────┘ └─────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
INTEGRAZIONI ESTERNE:
- SAP S/4HANA (procurement, energy data)
- Oracle NetSuite (financials per spend-based)
- TravelPerk/Concur (travel data - Cat.6)
- Climatiq API (emission factors)
- AIB Registry (GO certificates - Scope 2 market-based)
- Utility providers API (bollette energia)
Calculation Engine: Implementazione Python con FastAPI
Il motore di calcolo e il componente più critico. Deve essere preciso, auditabile, versionato e in grado di gestire migliaia di calcoli in parallelo. Ecco un'implementazione completa con FastAPI.
# calculation_engine/main.py - FastAPI Calculation Engine
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
from enum import Enum
import pandas as pd
import uuid
import asyncio
from decimal import Decimal, ROUND_HALF_UP
app = FastAPI(
title="Carbon Calculation Engine",
description="GHG Protocol-compliant emission calculation service",
version="2.1.0"
)
# ── Pydantic Models ──────────────────────────────────────────────
class ScopeEnum(str, Enum):
scope_1 = "scope_1"
scope_2_location = "scope_2_location"
scope_2_market = "scope_2_market"
scope_3 = "scope_3"
class CalculationMethod(str, Enum):
activity_based = "activity_based" # quantità * fattore emissione
spend_based = "spend_based" # spesa in EUR * fattore intensità
average_data = "average_data" # media su periodi
supplier_specific = "supplier_specific" # dati diretti da fornitori
class ActivityInput(BaseModel):
activity_id: str = Field(..., description="ID univoco attivita sorgente")
emission_source_id: str
scope: ScopeEnum
scope_3_category: Optional[str] = None
period_start: datetime
period_end: datetime
quantity: float = Field(..., gt=0, description="Quantità attivita")
unit: str = Field(..., description="Unita di misura (kWh, liter, km, tonne, EUR)")
emission_factor_id: Optional[str] = None # se None, il motore cerca automaticamente
calculation_method: CalculationMethod = CalculationMethod.activity_based
region: Optional[str] = None
data_quality: str = "measured"
@validator("quantity")
def quantity_must_be_positive(cls, v):
if v <= 0:
raise ValueError("La quantità deve essere positiva")
return v
class CalculationResult(BaseModel):
calculation_id: str
activity_id: str
scope: ScopeEnum
co2e_tonnes: float
co2_tonnes: Optional[float]
ch4_tonnes: Optional[float]
n2o_tonnes: Optional[float]
emission_factor_value: float
emission_factor_unit: str
emission_factor_source: str
emission_factor_year: int
calculation_formula: str
calculation_method: str
uncertainty_percentage: Optional[float]
calculated_at: datetime
audit_reference: str
class BatchCalculationRequest(BaseModel):
organization_id: str
reporting_year: int
activities: List[ActivityInput] = Field(..., max_items=500)
class ScopeAggregation(BaseModel):
scope_1_tonnes: float
scope_2_location_tonnes: float
scope_2_market_tonnes: float
scope_3_tonnes: float
scope_3_by_category: dict
total_location_based: float
total_market_based: float
reporting_year: int
organization_id: str
# ── Calculation Logic ────────────────────────────────────────────
class EmissionCalculator:
"""
Implementazione GHG Protocol Corporate Standard.
Formula base: Emissioni (kgCO2e) = Attivita x Fattore di Emissione
"""
# Fattori GWP AR6 (IPCC Sixth Assessment Report, 2021)
GWP_AR6 = {
"CO2": 1.0,
"CH4": 27.9, # CH4 fossil
"CH4_bio": 27.9,
"N2O": 273.0,
"SF6": 25200.0,
"NF3": 17400.0,
"HFC134a": 1526.0,
"HFC32": 771.0,
}
# Fattori di incertezza per qualità dato (DEFRA methodology)
UNCERTAINTY_BY_DATA_QUALITY = {
"measured": 5.0, # dati misurati diretti
"calculated": 10.0, # calcolati da misure
"estimated": 20.0, # stime con dati proxy
"spend_based": 35.0, # spend-based (meno preciso)
"default": 25.0,
}
def calculate_activity_based(
self,
quantity: float,
unit: str,
emission_factor: dict,
gwp_version: str = "AR6"
) -> dict:
"""
Calcolo activity-based:
CO2e (kg) = Quantità (unita) x EF (kgCO2e/unita)
"""
ef_value = emission_factor["co2e_factor"]
ef_unit = emission_factor["unit_type"]
# Verifica compatibilità unita
if not self._units_compatible(unit, ef_unit):
raise ValueError(
f"Unita incompatibili: attivita in {unit}, "
f"fattore in {ef_unit}"
)
co2e_kg = quantity * ef_value
# Calcola componenti separate se disponibili
co2_kg = quantity * emission_factor.get("co2_factor", 0)
ch4_kg = quantity * emission_factor.get("ch4_factor", 0)
n2o_kg = quantity * emission_factor.get("n2o_factor", 0)
return {
"co2e_tonnes": round(co2e_kg / 1000, 6),
"co2_tonnes": round(co2_kg / 1000, 6) if co2_kg else None,
"ch4_tonnes": round(ch4_kg / 1000, 6) if ch4_kg else None,
"n2o_tonnes": round(n2o_kg / 1000, 6) if n2o_kg else None,
"formula": (
f"{quantity} {unit} x {ef_value} kgCO2e/{unit} "
f"= {co2e_kg:.4f} kgCO2e "
f"= {co2e_kg/1000:.6f} tCO2e"
)
}
def calculate_spend_based(
self,
spend_eur: float,
emission_intensity: float, # kgCO2e/EUR
currency: str = "EUR",
exchange_rate: float = 1.0
) -> dict:
"""
Calcolo spend-based (Scope 3 Cat.1 quando non si hanno dati attivita):
CO2e (kg) = Spesa (EUR) x Intensità (kgCO2e/EUR)
"""
spend_normalized = spend_eur * exchange_rate
co2e_kg = spend_normalized * emission_intensity
return {
"co2e_tonnes": round(co2e_kg / 1000, 6),
"co2_tonnes": None,
"ch4_tonnes": None,
"n2o_tonnes": None,
"formula": (
f"{spend_normalized:.2f} EUR x {emission_intensity} kgCO2e/EUR "
f"= {co2e_kg:.4f} kgCO2e"
)
}
def _units_compatible(self, activity_unit: str, ef_unit: str) -> bool:
"""Verifica compatibilità unita di misura"""
# Normalizza: il fattore e tipicamente espresso come kgCO2e/[unita_attivita]
ef_denominator = ef_unit.split("/")[-1].strip().lower() if "/" in ef_unit else ef_unit
return activity_unit.lower() == ef_denominator
def aggregate_by_scope(
self,
calculations: List[CalculationResult]
) -> ScopeAggregation:
"""Aggrega i calcoli per scope - usa pandas per performance"""
df = pd.DataFrame([c.dict() for c in calculations])
scope_1 = df[df["scope"] == "scope_1"]["co2e_tonnes"].sum()
scope_2_loc = df[df["scope"] == "scope_2_location"]["co2e_tonnes"].sum()
scope_2_mkt = df[df["scope"] == "scope_2_market"]["co2e_tonnes"].sum()
scope_3_df = df[df["scope"] == "scope_3"]
scope_3_total = scope_3_df["co2e_tonnes"].sum()
# Aggregazione Scope 3 per categoria
scope_3_by_cat = {}
if not scope_3_df.empty and "scope_3_category" in scope_3_df.columns:
scope_3_by_cat = (
scope_3_df.groupby("scope_3_category")["co2e_tonnes"]
.sum()
.to_dict()
)
return ScopeAggregation(
scope_1_tonnes=round(scope_1, 3),
scope_2_location_tonnes=round(scope_2_loc, 3),
scope_2_market_tonnes=round(scope_2_mkt, 3),
scope_3_tonnes=round(scope_3_total, 3),
scope_3_by_category=scope_3_by_cat,
total_location_based=round(scope_1 + scope_2_loc + scope_3_total, 3),
total_market_based=round(scope_1 + scope_2_mkt + scope_3_total, 3),
reporting_year=2024,
organization_id=""
)
calculator = EmissionCalculator()
# ── API Endpoints ────────────────────────────────────────────────
@app.post("/v1/calculate/single", response_model=CalculationResult)
async def calculate_single(activity: ActivityInput):
"""Calcola emissioni per una singola attivita"""
# In produzione: cerca emission factor dal DB o Climatiq
# Qui mock per illustrazione
mock_ef = {
"co2e_factor": 0.233, # kgCO2e/kWh - rete italiana 2024
"co2_factor": 0.228,
"ch4_factor": 0.002,
"n2o_factor": 0.001,
"unit_type": "kgCO2e/kWh",
"source": "IEA 2024",
"year": 2024
}
result = calculator.calculate_activity_based(
quantity=activity.quantity,
unit=activity.unit,
emission_factor=mock_ef
)
calculation_id = str(uuid.uuid4())
uncertainty = calculator.UNCERTAINTY_BY_DATA_QUALITY.get(
activity.data_quality, 25.0
)
return CalculationResult(
calculation_id=calculation_id,
activity_id=activity.activity_id,
scope=activity.scope,
co2e_tonnes=result["co2e_tonnes"],
co2_tonnes=result["co2_tonnes"],
ch4_tonnes=result["ch4_tonnes"],
n2o_tonnes=result["n2o_tonnes"],
emission_factor_value=mock_ef["co2e_factor"],
emission_factor_unit=mock_ef["unit_type"],
emission_factor_source=mock_ef["source"],
emission_factor_year=mock_ef["year"],
calculation_formula=result["formula"],
calculation_method=activity.calculation_method.value,
uncertainty_percentage=uncertainty,
calculated_at=datetime.utcnow(),
audit_reference=f"CALC-{calculation_id[:8].upper()}"
)
@app.post("/v1/calculate/batch", response_model=List[CalculationResult])
async def calculate_batch(request: BatchCalculationRequest):
"""Calcola emissioni per un batch di attivita (max 500)"""
tasks = [calculate_single(activity) for activity in request.activities]
results = await asyncio.gather(*tasks, return_exceptions=True)
successful = [r for r in results if isinstance(r, CalculationResult)]
failed = [r for r in results if isinstance(r, Exception)]
if failed:
# Log errori ma non blocca il batch
pass
return successful
@app.get("/v1/organizations/{org_id}/summary", response_model=ScopeAggregation)
async def get_emissions_summary(org_id: str, year: int = 2024):
"""Riepilogo emissioni per scope per un'organizzazione"""
# In produzione: query dal DB
pass
Automazione Scope 3: ERP, Procurement e Travel
Lo Scope 3 e la sfida più grande del carbon accounting: dati distribuiti tra decine di sistemi, fornitori con diversi livelli di maturita, metodologie di calcolo diverse per categoria. L'automazione e l'unico modo per renderlo sostenibile operativamente.
Integrazione SAP per Scope 3 Categoria 1 (Purchased Goods)
# integrations/sap_connector.py - Estrazione dati procurement da SAP
import httpx
from dataclasses import dataclass
from typing import List, Optional
from datetime import date
import pandas as pd
@dataclass
class ProcurementRecord:
purchase_order_id: str
vendor_id: str
vendor_name: str
vendor_country: str
material_code: str
material_description: str
quantity: float
unit: str
amount_eur: float
nace_code: Optional[str] # classificazione settore fornitore
delivery_date: date
class SAPConnector:
"""
Connettore SAP S/4HANA via OData API.
Estrae dati procurement per Scope 3 Cat.1 e Cat.4.
"""
def __init__(self, base_url: str, client_id: str, client_secret: str):
self.base_url = base_url
self.client_id = client_id
self.client_secret = client_secret
self._token: Optional[str] = None
async def authenticate(self):
"""OAuth 2.0 client credentials per SAP"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/oauth/token",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
)
response.raise_for_status()
self._token = response.json()["access_token"]
async def get_purchase_orders(
self,
year: int,
cost_center: Optional[str] = None
) -> List[ProcurementRecord]:
"""
Recupera ordini di acquisto dal modulo MM di SAP.
Endpoint OData: /sap/opu/odata/sap/MM_PUR_PO_MANAGE_SRV/
"""
if not self._token:
await self.authenticate()
params = {
"$filter": f"PostingDate ge datetime'{year}-01-01T00:00:00' and "
f"PostingDate le datetime'{year}-12-31T23:59:59'",
"$select": "PurchaseOrder,Supplier,SupplierName,OrderedQuantity,"
"PurchaseOrderQuantityUnit,NetPriceAmount,Currency",
"$format": "json",
"$top": 5000
}
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/sap/opu/odata/sap/MM_PUR_PO_MANAGE_SRV/"
"A_PurchaseOrder",
headers={"Authorization": f"Bearer {self._token}"},
params=params
)
response.raise_for_status()
data = response.json()["d"]["results"]
return [
ProcurementRecord(
purchase_order_id=item["PurchaseOrder"],
vendor_id=item["Supplier"],
vendor_name=item["SupplierName"],
vendor_country="IT", # da arricchire con master data
material_code=item.get("Material", ""),
material_description=item.get("MaterialName", ""),
quantity=float(item["OrderedQuantity"]),
unit=item["PurchaseOrderQuantityUnit"],
amount_eur=float(item["NetPriceAmount"]),
nace_code=None, # da mappare con codifica NACE
delivery_date=date.fromisoformat(item.get("ScheduleLine", f"{year}-12-31")[:10])
)
for item in data
]
def calculate_scope3_cat1_spend_based(
self,
records: List[ProcurementRecord],
emission_intensities: dict # { nace_code: kgCO2e_per_EUR }
) -> pd.DataFrame:
"""
Calcolo Scope 3 Cat.1 con metodo spend-based.
Da usare quando non si hanno dati attivita dai fornitori.
Fattori da EXIOBASE o WIOD (World Input-Output Database).
"""
df = pd.DataFrame([vars(r) for r in records])
# Mappa NACE -> intensità emissioni
default_intensity = emission_intensities.get("DEFAULT", 0.35) # kgCO2e/EUR
df["emission_intensity"] = df["nace_code"].map(emission_intensities).fillna(default_intensity)
df["co2e_kg"] = df["amount_eur"] * df["emission_intensity"]
df["co2e_tonnes"] = df["co2e_kg"] / 1000
# Aggregazione per fornitore
summary = df.groupby(["vendor_id", "vendor_name"]).agg(
total_spend_eur=("amount_eur", "sum"),
total_co2e_tonnes=("co2e_tonnes", "sum"),
transaction_count=("purchase_order_id", "count")
).reset_index()
summary["avg_intensity"] = summary["total_co2e_tonnes"] * 1000 / summary["total_spend_eur"]
return summary.sort_values("total_co2e_tonnes", ascending=False)
# integrations/travel_connector.py - Dati viaggi per Scope 3 Cat.6
@dataclass
class TravelRecord:
employee_id: str
travel_date: date
origin_iata: str # codice aeroporto IATA
destination_iata: str
transport_mode: str # air, rail, car, ferry
distance_km: float
travel_class: str # economy, business, first
booking_amount_eur: float
class TravelDataProcessor:
"""Processa dati di business travel per Scope 3 Cat.6"""
# Fattori emissione voli (kgCO2e/pkm) - DEFRA 2024
DEFRA_FLIGHT_FACTORS = {
("short_haul", "economy"): 0.151,
("short_haul", "business"): 0.227,
("medium_haul", "economy"): 0.131,
("medium_haul", "business"): 0.262,
("long_haul", "economy"): 0.195,
("long_haul", "business"): 0.585,
("long_haul", "first"): 0.780,
}
# Fattori treno (kgCO2e/pkm) - media europea
RAIL_FACTORS = {
"IT": 0.004, # Trenitalia (alta % rinnovabili)
"DE": 0.006,
"FR": 0.002,
"DEFAULT": 0.041,
}
def classify_flight(self, distance_km: float) -> str:
if distance_km < 1500:
return "short_haul"
elif distance_km < 4000:
return "medium_haul"
else:
return "long_haul"
def calculate_flight_emissions(self, record: TravelRecord) -> float:
"""
Calcolo emissioni volo con Radiative Forcing Index (RFI = 1.9)
per effetti non-CO2 ad alta quota (metodo DEFRA con uplift factor)
"""
haul = self.classify_flight(record.distance_km)
travel_class = record.travel_class.lower() if record.travel_class else "economy"
key = (haul, travel_class)
base_factor = self.DEFRA_FLIGHT_FACTORS.get(
key,
self.DEFRA_FLIGHT_FACTORS[(haul, "economy")]
)
# Applica Radiative Forcing (RFI) per effetti non-CO2 in alta quota
rfi_factor = 1.9
co2e_kg = record.distance_km * base_factor * rfi_factor
return co2e_kg / 1000 # in tonnellate
def process_travel_data(self, records: List[TravelRecord]) -> dict:
"""Elabora tutti i dati travel e restituisce riepilogo per Cat.6"""
results = []
for record in records:
if record.transport_mode == "air":
co2e = self.calculate_flight_emissions(record)
method = "DEFRA 2024 with RFI=1.9"
elif record.transport_mode == "rail":
country = record.origin_iata[:2] # approssimazione
factor = self.RAIL_FACTORS.get(country, self.RAIL_FACTORS["DEFAULT"])
co2e = record.distance_km * factor / 1000
method = f"Rail factor {country}"
else:
co2e = record.distance_km * 0.171 / 1000 # car average
method = "DEFRA car average"
results.append({
"employee_id": record.employee_id,
"travel_date": record.travel_date,
"transport_mode": record.transport_mode,
"distance_km": record.distance_km,
"co2e_tonnes": co2e,
"method": method
})
df = pd.DataFrame(results)
return {
"total_co2e_tonnes": df["co2e_tonnes"].sum(),
"by_mode": df.groupby("transport_mode")["co2e_tonnes"].sum().to_dict(),
"by_month": df.groupby(df["travel_date"].apply(lambda x: x.month))["co2e_tonnes"].sum().to_dict(),
"total_km": df["distance_km"].sum(),
"record_count": len(df)
}
Reporting Automatico: CSRD/ESRS, CDP e GRI
Generare i report regolamentari e spesso il processo più time-consuming del carbon accounting. L'automazione riduce drasticamente il tempo da settimane a ore, aumenta la consistenza e garantisce che ogni cifra sia tracciabile alla sorgente.
Struttura Report CSRD/ESRS E1 (Climate Change)
Lo standard ESRS E1 dedicato al clima richiede disclosure su: governance del clima, strategia e scenario analysis, gestione dei rischi, metriche e target. Le metriche di emissioni sono definite in ESRS E1-6 e richiedono dati per tutti e tre gli scope.
# reporting/csrd_generator.py - Generazione report CSRD/ESRS E1
from dataclasses import dataclass, asdict
from typing import List, Optional, Dict
from datetime import datetime
import json
@dataclass
class ESRS_E1_6_Disclosure:
"""
ESRS E1-6: Gross Scopes 1, 2 and 3 greenhouse gas emissions
Disclosure requirements per ESRS E1 (climate change)
"""
organization_name: str
legal_entity_identifier: str # LEI
reporting_period: str # es. "FY2024"
reporting_standard: str = "ESRS E1 - Climate Change"
# Scope 1 - Emissioni dirette
scope_1_total_gross_tco2e: float = 0.0
scope_1_breakdown_by_ghg: Dict[str, float] = None # { "CO2": x, "CH4": y ... }
scope_1_breakdown_by_source: Dict[str, float] = None
# Scope 2 - Emissioni indirette energia
scope_2_location_based_tco2e: float = 0.0
scope_2_market_based_tco2e: float = 0.0
scope_2_purchased_electricity_kwh: float = 0.0
scope_2_renewable_electricity_percentage: float = 0.0
# Scope 3 - Emissioni catena del valore
scope_3_total_tco2e: float = 0.0
scope_3_upstream_tco2e: float = 0.0
scope_3_downstream_tco2e: float = 0.0
scope_3_by_category: Dict[str, float] = None
# Intensità emissioni
revenue_intensity_tco2e_per_meur: Optional[float] = None # tCO2e/M EUR
employee_intensity_tco2e_per_fte: Optional[float] = None
# Confronto anno precedente e target
base_year: int = 2020
scope_1_2_reduction_vs_base: Optional[float] = None # percentuale
sbti_target: Optional[str] = None # es. "1.5°C aligned, -42% by 2030"
# Metodologia
ghg_accounting_standard: str = "GHG Protocol Corporate Standard"
emission_factor_sources: List[str] = None
data_quality_notes: str = ""
assurance_level: str = "limited_assurance"
assurance_provider: str = ""
def __post_init__(self):
if self.scope_3_by_category is None:
self.scope_3_by_category = {}
if self.scope_1_breakdown_by_ghg is None:
self.scope_1_breakdown_by_ghg = {}
if self.emission_factor_sources is None:
self.emission_factor_sources = []
@property
def total_ghg_location_based(self) -> float:
return (self.scope_1_total_gross_tco2e +
self.scope_2_location_based_tco2e +
self.scope_3_total_tco2e)
@property
def total_ghg_market_based(self) -> float:
return (self.scope_1_total_gross_tco2e +
self.scope_2_market_based_tco2e +
self.scope_3_total_tco2e)
class CSRDReportGenerator:
def generate_esrs_e1_json(self, disclosure: ESRS_E1_6_Disclosure) -> str:
"""Genera disclosure ESRS E1-6 in formato JSON strutturato"""
report = {
"metadata": {
"standard": disclosure.reporting_standard,
"generated_at": datetime.utcnow().isoformat(),
"reporting_period": disclosure.reporting_period,
"organization": disclosure.organization_name,
"lei": disclosure.legal_entity_identifier,
},
"ESRS_E1-6": {
"gross_scope_1_tco2e": disclosure.scope_1_total_gross_tco2e,
"scope_1_breakdown_by_ghg": disclosure.scope_1_breakdown_by_ghg,
"scope_2_location_based_tco2e": disclosure.scope_2_location_based_tco2e,
"scope_2_market_based_tco2e": disclosure.scope_2_market_based_tco2e,
"scope_2_purchased_electricity_kwh": disclosure.scope_2_purchased_electricity_kwh,
"scope_2_renewable_pct": disclosure.scope_2_renewable_electricity_percentage,
"scope_3_total_tco2e": disclosure.scope_3_total_tco2e,
"scope_3_by_category": disclosure.scope_3_by_category,
"total_ghg_location_based_tco2e": disclosure.total_ghg_location_based,
"total_ghg_market_based_tco2e": disclosure.total_ghg_market_based,
"ghg_intensity_revenue": disclosure.revenue_intensity_tco2e_per_meur,
"ghg_intensity_employee": disclosure.employee_intensity_tco2e_per_fte,
},
"methodology": {
"accounting_standard": disclosure.ghg_accounting_standard,
"emission_factor_sources": disclosure.emission_factor_sources,
"base_year": disclosure.base_year,
"consolidation_approach": "operational_control",
"data_quality": disclosure.data_quality_notes,
},
"assurance": {
"level": disclosure.assurance_level,
"provider": disclosure.assurance_provider,
},
"targets": {
"sbti_commitment": disclosure.sbti_target,
"scope_1_2_reduction_vs_base": disclosure.scope_1_2_reduction_vs_base,
}
}
return json.dumps(report, indent=2, ensure_ascii=False)
def generate_cdp_questionnaire_c6(self, disclosure: ESRS_E1_6_Disclosure) -> dict:
"""
Genera risposte per CDP questionnaire - sezione C6 (Emissions Data).
CDP condivide molti requisiti con CSRD, il che riduce il double reporting.
"""
return {
"C6.1": {
"question": "Provide total gross global Scope 1 emissions in metric tons CO2e",
"response": disclosure.scope_1_total_gross_tco2e,
"unit": "metric tons CO2e"
},
"C6.2": {
"question": "Describe Scope 1 emissions by constituent gases",
"response": disclosure.scope_1_breakdown_by_ghg
},
"C6.3": {
"question": "Provide total gross global Scope 2 emissions",
"response": {
"location_based": disclosure.scope_2_location_based_tco2e,
"market_based": disclosure.scope_2_market_based_tco2e,
}
},
"C6.5": {
"question": "Account for Scope 3 emissions",
"response": {
"total": disclosure.scope_3_total_tco2e,
"categories": disclosure.scope_3_by_category
}
},
"C6.10": {
"question": "Describe Scope 1 and 2 GHG emissions by location",
"note": "See facility-level breakdown in supporting data"
}
}
Audit Trail e Data Lineage
Con la CSRD che richiede limited assurance da un revisore abilitato, l'audit trail non e un requisito opzionale: e la spina dorsale della piattaforma. Ogni numero del report deve essere tracciabile fino alla sorgente originale con la formula di calcolo esplicita.
# audit/audit_trail.py - Sistema di audit immutabile
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Optional
import hashlib
import json
import uuid
@dataclass(frozen=True) # immutabile
class AuditEvent:
"""
Evento di audit immutabile. Non si modifica mai, solo si aggiungono nuovi eventi.
Il hash SHA256 garantisce l'integrita della catena.
"""
event_id: str
event_type: str # DATA_INGESTED, FACTOR_SELECTED, CALCULATION_PERFORMED, REPORT_GENERATED, APPROVED
entity_type: str # Activity, Calculation, EmissionFactor, Report
entity_id: str
actor_id: str # user_id o service_name per azioni automatiche
actor_type: str # human, system
timestamp: str # ISO 8601 UTC
data_before: Optional[str] # JSON snapshot before change
data_after: Optional[str] # JSON snapshot after change
metadata: str # JSON { formula, ef_source, notes ... }
previous_hash: str # hash dell'evento precedente (blockchain-like)
event_hash: str # SHA256 di tutti i campi
@classmethod
def create(
cls,
event_type: str,
entity_type: str,
entity_id: str,
actor_id: str,
actor_type: str = "human",
data_before: Optional[Dict] = None,
data_after: Optional[Dict] = None,
metadata: Optional[Dict] = None,
previous_hash: str = "GENESIS"
) -> "AuditEvent":
event_id = str(uuid.uuid4())
timestamp = datetime.utcnow().isoformat() + "Z"
data_before_str = json.dumps(data_before, sort_keys=True) if data_before else None
data_after_str = json.dumps(data_after, sort_keys=True) if data_after else None
metadata_str = json.dumps(metadata or {}, sort_keys=True)
# Calcola hash dell'evento per garanzia di integrita
hash_content = "|".join([
event_id, event_type, entity_type, entity_id,
actor_id, timestamp, data_after_str or "", previous_hash
])
event_hash = hashlib.sha256(hash_content.encode()).hexdigest()
return cls(
event_id=event_id,
event_type=event_type,
entity_type=entity_type,
entity_id=entity_id,
actor_id=actor_id,
actor_type=actor_type,
timestamp=timestamp,
data_before=data_before_str,
data_after=data_after_str,
metadata=metadata_str,
previous_hash=previous_hash,
event_hash=event_hash
)
class DataLineageTracker:
"""
Traccia il percorso completo di un dato dalla sorgente al report.
Permette ai revisori di verificare ogni numero del report finale.
"""
def __init__(self, db_session):
self.db = db_session
def trace_calculation(self, calculation_id: str) -> dict:
"""
Ricostruisce il lineage completo di un calcolo:
Report -> Calculation -> Activity -> EmissionFactor -> RawData -> Source System
"""
# In produzione: query al DB per tutti gli eventi correlati
lineage = {
"calculation_id": calculation_id,
"trace": [
{
"step": 1,
"description": "Raw data ingested from SAP S/4HANA",
"system": "SAP_CONNECTOR",
"timestamp": "2025-01-15T08:30:00Z",
"data_summary": "Natural gas consumption: 5,420 m3, Jan 2024",
"audit_event_id": "AE-001"
},
{
"step": 2,
"description": "Unit conversion applied: m3 -> kWh",
"formula": "5,420 m3 x 10.55 kWh/m3 = 57,181 kWh",
"timestamp": "2025-01-15T08:31:00Z",
"audit_event_id": "AE-002"
},
{
"step": 3,
"description": "Emission factor selected from DEFRA 2024",
"emission_factor": "Natural gas - Gross CV: 0.18306 kgCO2e/kWh",
"factor_id": "DEFRA_2024_NG_GROSS",
"timestamp": "2025-01-15T08:31:05Z",
"audit_event_id": "AE-003"
},
{
"step": 4,
"description": "Emission calculation performed",
"formula": "57,181 kWh x 0.18306 kgCO2e/kWh = 10,467 kgCO2e = 10.467 tCO2e",
"result_tco2e": 10.467,
"scope": "Scope 1",
"timestamp": "2025-01-15T08:31:06Z",
"audit_event_id": "AE-004"
},
{
"step": 5,
"description": "Calculation included in FY2024 CSRD Report - ESRS E1-6",
"report_id": "RPT-CSRD-2024-001",
"approved_by": "CFO - Mario Rossi",
"timestamp": "2025-03-10T14:00:00Z",
"audit_event_id": "AE-089"
}
],
"data_quality_flag": "HIGH",
"uncertainty_pct": 5.0,
"verification_status": "verified_by_auditor"
}
return lineage
Confronto Piattaforme Enterprise: Build vs Buy
Prima di costruire una piattaforma custom, e essenziale valutare le soluzioni enterprise disponibili. Il mercato del carbon accounting software e molto maturo e le piattaforme leader coprono la maggior parte dei casi d'uso standard.
| Piattaforma | Punto di Forza | Settore Target | Prezzo Indicativo | CSRD Ready |
|---|---|---|---|---|
| Persefoni | Investor-grade reporting, XBRL tagging, SEC-focused | Finance, Corporate | $50K-500K/anno | Si |
| Watershed | Speed to report, Scope 3 tooling avanzato | Tech, Enterprise | $100K-1M/anno | Si |
| Sphera | LCA integrato, compliance industriale, risk management | Manifattura, Energia, Chimico | Su richiesta | Si |
| Plan A | UX semplice, rapida implementazione | PMI, Medie imprese | $10K-100K/anno | Parziale |
| IBM Envizi | 40.000+ emission factors, integrazione ERP | Enterprise, Utility | Su richiesta | Si |
| Custom Build | Flessibilità totale, integrazione nativa sistemi interni | Grandi aziende con requisiti specifici | $500K-5M sviluppo | Dipende |
Quando Costruire Custom vs Acquistare
Acquista una piattaforma se: hai requisiti standard, vuoi andare live in 3-6 mesi, non hai team engineering dedicato alla sostenibilità, il volume di dati e gestibile.
Costruisci custom se: hai processi produttivi molto specifici con dati non gestibili da piattaforme standard, vuoi integrare nativamente con sistemi legacy, hai requisiti di data residency non compatibili con SaaS, o il volume di dati e dell'ordine di milioni di record/anno (costi SaaS diventano proibitivi).
Case Study: PMI Manifatturiera Italiana con CSRD 2025
Un'azienda manifatturiera italiana del settore meccanico di precisione con 1.200 dipendenti, 350 milioni di euro di fatturato e stabilimenti a Torino, Milano e Brescia, si trova nella Wave 1 della CSRD e deve presentare il primo report per l'esercizio 2024 entro giugno 2025.
Inventario Emissioni FY2024
# case_study/inventory_2024.py - Esempio inventario GHG realistico
# Dati rappresentativi per PMI manifatturiera italiana
INVENTORY_2024 = {
"organization": "MeccanicaPrecisione SpA",
"reporting_year": 2024,
"boundary": "Operational Control",
"currency": "EUR",
"scope_1": {
"natural_gas_combustion": {
"consumption_m3": 485_000, # caldaie industriali
"consumption_kwh": 5_116_750, # conversione: 1 m3 NG = 10.55 kWh
"emission_factor_kgco2e_kwh": 0.18306, # DEFRA 2024
"co2e_tonnes": 937.0,
"source": "DEFRA 2024 - Natural Gas Gross CV"
},
"diesel_mobile": {
"consumption_liters": 42_000, # carrelli elevatori, mezzi operativi
"emission_factor_kgco2e_liter": 2.56, # DEFRA 2024
"co2e_tonnes": 107.5,
"source": "DEFRA 2024 - Diesel"
},
"fugitive_refrigerants": {
"substance": "R-410A",
"kg_recharged": 45,
"gwp_ar6": 2088,
"co2e_tonnes": 94.0,
"source": "IPCC AR6 GWP100"
},
"total_co2e_tonnes": 1138.5
},
"scope_2": {
"purchased_electricity": {
"consumption_kwh": 8_250_000,
"location_based": {
"emission_factor": 0.233, # IEA Italy 2024 kgCO2e/kWh
"co2e_tonnes": 1922.3,
"source": "IEA 2024 Italy Grid"
},
"market_based": {
"go_certificates_kwh": 4_125_000, # 50% rinnovabili con GO
"residual_mix_factor": 0.395, # AIB Italy residual mix 2024
"co2e_tonnes": 1629.4, # solo su 50% non coperto da GO
"source": "AIB Italy Residual Mix 2024"
}
}
},
"scope_3": {
"cat_1_purchased_goods": {
"total_spend_eur": 145_000_000,
"method": "spend_based + supplier_specific (top 20 suppliers)",
"co2e_tonnes": 28_450.0,
"data_quality": "mix: 35% supplier-specific, 65% spend-based"
},
"cat_4_upstream_transport": {
"tonne_km": 3_200_000,
"emission_factor": 0.089, # kgCO2e/tonne-km road freight DEFRA
"co2e_tonnes": 284.8,
},
"cat_5_waste": {
"waste_tonnes": 1_850,
"co2e_tonnes": 185.0,
"method": "waste-type specific factors"
},
"cat_6_business_travel": {
"total_km_air": 1_250_000,
"total_km_rail": 320_000,
"co2e_tonnes": 312.5,
"source": "DEFRA 2024 with RFI=1.9"
},
"cat_7_employee_commuting": {
"employees": 1_200,
"avg_km_per_day": 22,
"working_days": 220,
"mode_split": {"car_solo": 0.65, "car_sharing": 0.05,
"public_transport": 0.25, "cycling": 0.05},
"co2e_tonnes": 892.5
},
"cat_11_use_of_products": {
"units_sold": 45_000,
"avg_energy_use_kwh_per_unit_per_year": 850,
"product_lifetime_years": 15,
"co2e_tonnes_per_year": 2_250.0,
"note": "Calcolato per anno di uso, non lifetime"
},
"total_co2e_tonnes": 32_374.8
},
"summary": {
"scope_1_tco2e": 1_138.5,
"scope_2_location_tco2e": 1_922.3,
"scope_2_market_tco2e": 1_629.4,
"scope_3_tco2e": 32_374.8,
"total_location_based_tco2e": 35_435.6,
"total_market_based_tco2e": 35_142.7,
"intensity_tco2e_per_meur_revenue": 101.3,
"intensity_tco2e_per_fte": 29.5,
"scope_3_percentage_of_total": 91.4, # tipico nel manifatturiero
}
}
Osservazioni sul Case Study
Il risultato più significativo e che lo Scope 3 rappresenta il 91.4% delle emissioni totali, con la Categoria 1 (materie prime e componenti) che da sola vale l'80% dello Scope 3. Questo e tipico del settore manifatturiero e spiega perchè la CSRD richiede la rendicontazione della supply chain: senza Scope 3, il carbon accounting coprirebbe meno del 10% dell'impatto reale.
La differenza tra Scope 2 location-based (1.922 tCO2e) e market-based (1.629 tCO2e) e dovuta all'acquisto di certificati di energia rinnovabile (Garanzie di Origine - GO) per il 50% del consumo elettrico. Il residual mix italiano del 2024 (0.395 kgCO2e/kWh) e più alto del fattore medio della rete (0.233 kgCO2e/kWh): questo e controintuitivo ma corretto metodologicamente, perchè il residual mix esclude le energie già certificate con GO e quindi "contiene" una quota più alta di fonti ad alta intensità.
Testing: Validazione dei Calcoli
I calcoli di emissioni devono essere testati con la stessa rigorosita del codice finanziario. Un errore di un fattore di conversione può portare a errori nell'ordine di grandezza delle centinaia di tonnellate di CO2e.
# tests/test_calculation_engine.py
import pytest
from decimal import Decimal
from calculation_engine.main import EmissionCalculator
@pytest.fixture
def calculator():
return EmissionCalculator()
class TestScope1Calculations:
def test_natural_gas_combustion_scope1(self, calculator):
"""
Test calcolo gas naturale con fattore DEFRA 2024.
Valore atteso: 5000 kWh x 0.18306 kgCO2e/kWh = 915.3 kgCO2e = 0.9153 tCO2e
"""
emission_factor = {
"co2e_factor": 0.18306,
"co2_factor": 0.18207,
"ch4_factor": 0.000291,
"n2o_factor": 0.000087,
"unit_type": "kgCO2e/kWh",
"source": "DEFRA 2024",
"year": 2024
}
result = calculator.calculate_activity_based(
quantity=5000,
unit="kWh",
emission_factor=emission_factor
)
assert abs(result["co2e_tonnes"] - 0.9153) < 0.001, (
f"Scope 1 gas naturale: atteso 0.9153, ottenuto {result['co2e_tonnes']}"
)
assert result["co2_tonnes"] is not None
assert "formula" in result
def test_diesel_combustion_scope1(self, calculator):
"""
Test calcolo gasolio.
Fattore DEFRA 2024: 2.56179 kgCO2e/liter
1000 litri -> 2.56179 kgCO2e -> 0.00256 tCO2e (arrotondato a 6 decimali)
"""
emission_factor = {
"co2e_factor": 2.56179,
"co2_factor": 2.51476,
"ch4_factor": 0.00179,
"n2o_factor": 0.00524,
"unit_type": "kgCO2e/liter",
"source": "DEFRA 2024",
"year": 2024
}
result = calculator.calculate_activity_based(
quantity=1000,
unit="liter",
emission_factor=emission_factor
)
expected = 2561.79 / 1000
assert abs(result["co2e_tonnes"] - expected) < 0.001
def test_unit_incompatibility_raises_error(self, calculator):
"""Test che unita incompatibili generino errore"""
emission_factor = {
"co2e_factor": 0.18306,
"unit_type": "kgCO2e/kWh",
}
with pytest.raises(ValueError, match="incompatibili"):
calculator.calculate_activity_based(
quantity=1000,
unit="liter", # incompatibile con kWh
emission_factor=emission_factor
)
class TestScope2Calculations:
def test_scope2_location_based_italy_2024(self, calculator):
"""
Fattore rete elettrica italiana IEA 2024: 0.233 kgCO2e/kWh
100.000 kWh -> 23.300 kgCO2e -> 23.3 tCO2e
"""
emission_factor = {
"co2e_factor": 0.233,
"unit_type": "kgCO2e/kWh",
"source": "IEA 2024 Italy",
"year": 2024
}
result = calculator.calculate_activity_based(
quantity=100_000,
unit="kWh",
emission_factor=emission_factor
)
assert abs(result["co2e_tonnes"] - 23.3) < 0.01
def test_scope2_market_based_with_go_certificates(self, calculator):
"""
Con GO certificates per energia rinnovabile: fattore = 0 per la quota coperta.
Residual mix IT 2024: 0.395 kgCO2e/kWh per la quota non coperta.
50% rinnovabili (GO): 50.000 kWh x 0 = 0
50% residual mix: 50.000 kWh x 0.395 = 19.750 kgCO2e = 19.75 tCO2e
"""
# Calcolo solo sulla quota non coperta da GO
emission_factor_residual = {
"co2e_factor": 0.395,
"unit_type": "kgCO2e/kWh",
"source": "AIB Italy Residual Mix 2024",
"year": 2024
}
non_go_kwh = 50_000 # 50% non coperto da GO
result = calculator.calculate_activity_based(
quantity=non_go_kwh,
unit="kWh",
emission_factor=emission_factor_residual
)
assert abs(result["co2e_tonnes"] - 19.75) < 0.01
class TestScope3Calculations:
def test_business_travel_air_short_haul_economy(self):
"""Test emissioni volo breve raggio con RFI"""
from integrations.travel_connector import TravelDataProcessor, TravelRecord
from datetime import date
processor = TravelDataProcessor()
record = TravelRecord(
employee_id="EMP001",
travel_date=date(2024, 3, 15),
origin_iata="LIN",
destination_iata="FCO",
transport_mode="air",
distance_km=490,
travel_class="economy",
booking_amount_eur=180
)
# 490 km x 0.151 kgCO2e/pkm x 1.9 RFI = 140.531 kgCO2e = 0.14053 tCO2e
co2e = processor.calculate_flight_emissions(record)
expected = 490 * 0.151 * 1.9 / 1000
assert abs(co2e - expected) < 0.001
def test_spend_based_with_nace_intensity(self, calculator):
"""Test calcolo spend-based per Category 1"""
result = calculator.calculate_spend_based(
spend_eur=100_000,
emission_intensity=0.35 # kgCO2e/EUR - manifattura metalli
)
# 100.000 EUR x 0.35 kgCO2e/EUR = 35.000 kgCO2e = 35.0 tCO2e
assert abs(result["co2e_tonnes"] - 35.0) < 0.01
Science Based Targets (SBTi) e Dashboard
Il Science Based Targets initiative (SBTi) ha definito criteri precisi per target di riduzione delle emissioni allineati con l'Accordo di Parigi. La corporate standard SBTi richiede: riduzione Scope 1+2 del 42% entro il 2030 (baseline 2020) per uno scenario 1.5°C, e copertura Scope 3 se rappresentano più del 40% delle emissioni totali (quasi sempre vera nel manifatturiero).
Il dashboard della piattaforma deve mostrare non solo le emissioni correnti, ma il percorso verso il target: la curva di riduzione richiesta da SBTi, le emissioni effettive anno per anno, e la proiezione basata sulle iniziative di riduzione pianificate (PPA per energia rinnovabile, elettrificazione fleet, efficienza processi).
# dashboard/sbti_tracker.py - Tracking verso target SBTi
from dataclasses import dataclass
from typing import List, Dict
import pandas as pd
import numpy as np
@dataclass
class SBTiTarget:
organization_id: str
base_year: int
base_year_scope_1_2_tco2e: float
base_year_scope_3_tco2e: float
target_year: int = 2030
scope_12_reduction_pct: float = 42.0 # % riduzione vs base year
scope_3_reduction_pct: float = 25.0 # % riduzione vs base year
scenario: str = "1.5C"
@property
def scope_12_target_tco2e(self) -> float:
return self.base_year_scope_1_2_tco2e * (1 - self.scope_12_reduction_pct / 100)
@property
def annual_reduction_rate(self) -> float:
"""Tasso di riduzione annuale lineare necessario"""
years = self.target_year - self.base_year
return self.scope_12_reduction_pct / years
def build_sbti_trajectory(target: SBTiTarget, actuals: Dict[int, float]) -> pd.DataFrame:
"""
Costruisce tabella confronto tra traiettoria SBTi e emissioni effettive.
actuals: { anno: tCO2e Scope1+2 effettivo }
"""
years = range(target.base_year, target.target_year + 1)
trajectory = []
for year in years:
years_elapsed = year - target.base_year
total_years = target.target_year - target.base_year
# Riduzione lineare richiesta da SBTi
required_reduction_pct = (years_elapsed / total_years) * target.scope_12_reduction_pct
sbti_budget = target.base_year_scope_1_2_tco2e * (1 - required_reduction_pct / 100)
actual = actuals.get(year)
on_track = actual <= sbti_budget if actual is not None else None
trajectory.append({
"year": year,
"sbti_budget_tco2e": round(sbti_budget, 1),
"actual_tco2e": actual,
"gap_tco2e": round(actual - sbti_budget, 1) if actual else None,
"on_track": on_track
})
df = pd.DataFrame(trajectory)
return df
# Utilizzo per MeccanicaPrecisione SpA
target = SBTiTarget(
organization_id="meccanica-precisione-spa",
base_year=2020,
base_year_scope_1_2_tco2e=3_850.0, # baseline 2020
base_year_scope_3_tco2e=35_000.0,
)
actuals_scope_12 = {
2020: 3850.0,
2021: 3720.0,
2022: 3650.0,
2023: 3320.0,
2024: 3060.8, # scope_1 + scope_2_market dal nostro inventory
}
trajectory = build_sbti_trajectory(target, actuals_scope_12)
print(trajectory.to_string(index=False))
# Output:
# year sbti_budget_tco2e actual_tco2e gap_tco2e on_track
# 2020 3850.0 3850.0 0.0 True
# 2021 3608.6 3720.0 111.4 False
# 2022 3367.2 3650.0 282.8 False
# 2023 3125.8 3320.0 194.2 False
# 2024 2884.4 3060.8 176.4 False
# -> L'azienda e fuori traiettoria: serve accelerare le iniziative di riduzione
Best Practices e Anti-Pattern
Best Practices
- Immutabilita dei calcoli: Una volta calcolata e salvata, una emissione non va mai modificata. Se cambiano i fattori, si crea una nuova versione del calcolo. L'audit trail deve mostrare entrambe le versioni.
- Rendiconta entrambi i metodi Scope 2: Location-based E market-based sono entrambi obbligatori secondo ESRS e GHG Protocol. Non reportare solo il market-based perchè e più basso.
- Documenta la materialita Scope 3: Non devi calcolare tutte le 15 categorie Scope 3. Ma devi dimostrare con una materiality analysis perchè hai incluso/escluso ciascuna categoria. Questo e un requisito esplicito ESRS.
- Versioning dei fattori di emissione: I fattori cambiano ogni anno. Mantieni uno snapshot del fattore usato per ogni calcolo. Non aggiornare retroattivamente senza produrre un ricalcolo esplicito con nuova versione.
- Data quality flags: Classifica ogni dato per qualità (misurato, calcolato, stimato, spend-based). Le ESRS richiedono di dichiarare la qualità dei dati Scope 3. Dati di qualità inferiore possono essere usati ma devono essere dichiarati.
- Boundary documentation: Documenta esplicitamente le entità incluse nel confine di rendicontazione e quelle escluse, con la giustificazione (threshold <5%, dati non disponibili, ecc.).
Anti-Pattern da Evitare
- Usare solo lo Scope 2 market-based: Il GHG Protocol richiede entrambi. Reportare solo il market-based (tipicamente più basso grazie a GO/PPA) senza il location-based e metodologicamente errato e potenziale greenwashing.
- Emission factors non aggiornati: Usare fattori vecchi di 5+ anni introduce errori significativi, specialmente per la rete elettrica che cambia ogni anno con la crescita delle rinnovabili.
- Duplicare le emissioni nel consolidamento: Se una controllata calcola le sue emissioni e la capogruppo le include nel consolidato, occorre usare il metodo equity share o operational control in modo consistente.
- Ignorare l'incertezza: Tutti i calcoli di emissioni hanno un'incertezza, specialmente lo Scope 3. Non presentare numeri senza indicare il range di confidenza e il metodo usato.
- Report senza assurance: Con la CSRD, i report senza limited assurance non soddisfano i requisiti normativi per le Wave 1. Pianifica l'assurance dal primo anno, non come afterthought.
Conclusioni e Roadmap Implementativa
Progettare una piattaforma di carbon accounting enterprise-grade e un progetto complesso che richiede competenze multidisciplinari. Ma la struttura concettuale e chiara: il GHG Protocol fornisce il framework contabile, i database di emission factors (DEFRA, EPA, Climatiq) forniscono i coefficienti, l'architettura a microservizi garantisce scalabilità e manutenibilità, e l'audit trail immutabile garantisce la credibilita per la CSRD.
Per una PMI che deve avvicinarsi alla CSRD nel 2025-2026, la raccomandazione pratica e di iniziare con una piattaforma SaaS matura (Persefoni, Plan A, o Watershed) per i primi due anni, raccogliere dati reali, capire i propri gap di dati e solo allora valutare se costruire una soluzione custom o restare su SaaS. Il 90% delle organizzazioni non ha bisogno di costruire da zero.
Per i team engineering che invece lavorano su piattaforme ESG come prodotto, i concetti illustrati in questo articolo - data model GHG Protocol, calculation engine auditabile, integrazione con API di emission factors e generazione report regolamentari - sono le fondamenta su cui costruire. Il mercato del carbon accounting software crescera del 27% annuo fino al 2030: c'è spazio per soluzioni specializzate, specialmente per settori industriali con requisiti di process data non coperti dalle piattaforme generaliste.
Roadmap Implementativa Consigliata
| Fase | Durata | Obiettivo | Output |
|---|---|---|---|
| Fase 1 | 1-2 mesi | Double Materiality Assessment + boundary | Lista categorie Scope 3 materiali, confine reporting |
| Fase 2 | 2-3 mesi | Data collection Scope 1 e 2 | Pipeline automatizzata con bollette, SAP, SCADA |
| Fase 3 | 3-4 mesi | Scope 3 categorie materiali (spend-based) | Calcoli Scope 3 per cat. 1, 4, 6, 7, 11 |
| Fase 4 | 1-2 mesi | Report CSRD/ESRS E1 e audit | Report pronto per limited assurance |
| Fase 5 | Continuo | Miglioramento qualità dati e supplier engagement | Riduzione spend-based, aumento supplier-specific Scope 3 |
Risorse Essenziali
- GHG Protocol Corporate Standard: ghgprotocol.org - lo standard di riferimento, gratuito e scaricabile
- DEFRA Emission Factors 2024: gov.uk/government/publications/ greenhouse-gas-reporting-conversion-factors-2024
- Climatiq API: climatiq.io/docs - documentazione completa e quickstart gratuito
- ESRS E1 Standard: efrag.org - European Financial Reporting Advisory Group, standard ESRS definitivi
- SBTi Corporate Manual: sciencebasedtargets.org - guida alla definizione di target scientificamente fondati
- ecoinvent Database: ecoinvent.org - database LCA per Scope 3 con dati processo dettagliati
Prossimi Articoli della Serie EnergyTech
Il prossimo articolo esplorera i Digital Twin Energetici: come creare repliche virtuali di impianti industriali per simulare scenari di riduzione delle emissioni prima di implementarli nel mondo fisico. Una tecnologia sempre più centrale nelle strategie di decarbonizzazione delle grandi industrie.
Per approfondire le tecnologie di AI applicate al business dei dati, consulta anche la serie Data Warehouse, AI e Trasformazione Digitale, in particolare gli articoli su Data Governance e Data Quality per AI Affidabile e su MLOps per Business, che trattano temi direttamente applicabili alla gestione dei modelli di calcolo delle emissioni in produzione.

Commenti
Caricamento commenti...
Accedi per commentare