In breve
In Italia le fatture tra aziende sono file elettronici in un formato ufficiale rigoroso. Il sistema fiscale ricalcola ogni totale e, se uno sbaglia anche di un centesimo, la fattura viene scartata come se non fosse mai stata emessa.
Ho scritto una piccola libreria gratuita che costruisce questi file a partire dai normali dati di vendita. Fa i conti nello stesso modo in cui li controlla il sistema fiscale e, quando qualcosa non va, elenca tutti gli errori insieme, così chi vende può correggerli in una volta sola. È il motore di fatturazione di un marketplace che sto costruendo per le aziende alimentari.
Le sezioni qui sotto entrano nel dettaglio tecnico. Sono chiuse: apri quelle che ti incuriosiscono.
Perché l’ho scritta
Dal 2019 le fatture tra imprese italiane sono elettroniche. Il cedente produce un file XML nel formato FatturaPA e lo invia tramite lo SDI, il Sistema di Interscambio gestito dall’Agenzia delle Entrate, che lo controlla e lo recapita al cessionario. Un file che non supera un controllo viene scartato e la fattura si considera non emessa.
Sto costruendo un marketplace B2B per aziende alimentari e a ottobre ho deciso che chi compra paga direttamente chi vende tramite bonifico bancario, senza commissioni d’incasso nel mezzo. I pagamenti con carta tramite una piattaforma di pagamento erano l’alternativa, troppo complessa per ciò che serve al mercato. Questa scelta ha spostato un compito sulla piattaforma: quando il venditore segna una consegna come completata, il marketplace emette la fattura per suo conto.
La maggior parte dei pacchetti FatturaPA trasforma in XML un albero JSON che hai già compilato tu. I totali, il riepilogo IVA e i controlli di coerenza restano a carico tuo, ed è proprio su quelli che lo SDI scarta i file. Ho quindi scritto un modulo che riceve i dati commerciali e si occupa dell’aritmetica. L’ho sviluppato dentro l’API del marketplace, poi il 21 ottobre l’ho spostato in un repository pubblico dedicato, ho aggiunto gli sconti di riga e i clienti esteri e l’ho pubblicato su npm con licenza MIT.
Repository su GitHub ↗Forgejo (canonico) ↗Mirror su Codeberg ↗Pacchetto npm ↗
Tutta l’interfaccia è una sola funzione. buildInvoice riceve il cedente, il cessionario, il soggetto trasmittente, le righe e i dati di pagamento, con ogni importo espresso in centesimi interi di euro: 2490 corrisponde a 24,90 €. Restituisce l’XML, il nome file atteso dallo SDI e i totali calcolati. Nell’input non c’è alcun totale, quindi non può esserci alcun totale in disaccordo con le righe.
Il nome file è composto dal codice paese e dal codice fiscale del trasmittente seguiti da un progressivo univoco, ad esempio IT20000000008_00001.xml. Lo SDI rifiuta i nomi file già ricevuti, perciò chi chiama la funzione mantiene unica quella sequenza, ad esempio con un contatore salvato nel database.
import { buildInvoice } from "@lucianonerydev/fatturapa";
const { xml, fileName, totalCents } = buildInvoice({
number: "1/A",
date: "2026-10-20",
seller: {
vatNumber: "20000000008",
name: "Macelleria Bresciana Srl",
taxRegime: "RF01",
address: { street: "Via Milano 12", postalCode: "25126", city: "Brescia", province: "BS" },
},
buyer: {
vatNumber: "10000000009",
name: "Trattoria Il Portico",
address: { street: "Via Milano 10", postalCode: "25126", city: "Brescia", province: "BS" },
},
transmitter: { code: "20000000008", sequence: "00001" },
lines: [{
description: "Controfiletto di manzo",
quantity: 3,
unit: "kg",
unitPriceCents: 2490,
vatRate: 10,
adjustments: [{ percent: 10 }],
}],
payment: { method: "MP05", dueDate: "2026-11-19", iban: "IT60X0542811101000000123456" },
});
// fileName: "IT20000000008_00001.xml", totalCents: 7395
Lo SDI non si fida dei totali presenti nel file: li ricalcola. L’elenco ufficiale dei controlli ne esplicita tre. Il PrezzoTotale di ogni riga deve coincidere con il prezzo unitario, al netto di sconti e maggiorazioni applicati in cascata, moltiplicato per la quantità, entro un centesimo (errore 00423). L’ImponibileImporto di ciascuna aliquota IVA deve corrispondere alla somma delle sue righe, entro un euro (00422). L’Imposta di ogni blocco di riepilogo deve essere pari all’aliquota per quell’imponibile, arrotondata al centesimo con criterio half-up, sempre entro un centesimo (00421).
L’ultima regola è quella che trae in inganno il codice scritto in casa. L’IVA si calcola una volta sola per aliquota sull’imponibile sommato, non riga per riga. Due righe da 0,25 € al 10% portano 2,5 centesimi di IVA ciascuna; arrotondando per riga fanno 6 centesimi, ma lo SDI se ne aspetta 5 sul totale di 0,50 €. Dieci righe simili sballerebbero di 5 centesimi, cinque volte la tolleranza. computeTotals raggruppa le righe per aliquota e natura e arrotonda ogni gruppo una sola volta.
Prezzi, totali di riga e IVA non passano mai per i numeri in virgola mobile. Gli importi sono centesimi interi e le quantità vengono convertite in valori bigint a virgola fissa con otto decimali, il massimo consentito dal campo Quantita. Una riga con sconti in cascata viene mantenuta come frazione esatta e arrotondata solo alla fine: 24,90 € meno il 10% fa 22,41 €, meno 1,00 € fa 21,41 €, e per tre unità il totale è 64,23 €. Passare una quantità come "1.25" sotto forma di stringa evita sorprese di virgola mobile binaria prima ancora che il valore arrivi alla libreria.
/** Divide e arrotonda half away from zero, come richiesto dallo SDI per prezzi e IVA. */
export function divideRounded(numerator: bigint, denominator: bigint): bigint {
if (numerator < 0n) return -divideRounded(-numerator, denominator);
return (numerator * 2n + denominator) / (denominator * 2n);
}
/** IVA su un imponibile, arrotondata al centesimo. */
export function vatAmount(taxableCents: number, basisPoints: number): number {
return Number(divideRounded(BigInt(taxableCents) * BigInt(basisPoints), 10_000n));
}Una fattura scartata che segnala un solo problema, viene corretta e poi viene scartata di nuovo per quello successivo crea un ciclo lento, e in un marketplace chi deve correggerla è un venditore che compila un modulo. Per questo la validazione non si ferma mai al primo errore. Ogni controllo aggiunge un elemento a una lista e buildInvoice solleva un unico FatturaPAError i cui elementi riportano il percorso del campo di input, pronto per essere associato ai campi del form. Il blocco in fondo a questa sezione è l’output reale per una fattura con sette errori.
I controlli coprono ciò per cui lo SDI scarta i file e ciò che è verificabile dalla sola fattura. Numeri di fattura e nomi file duplicati dipendono dall’archivio del chiamante, quindi restano a suo carico.
- Cifra di controllo della Partita IVA, codice fiscale con sostituzioni per omocodia e checksum IBAN mod-97.
- Codice destinatario e PEC: la PEC va indicata solo insieme al codice destinatario 0000000.
- Natura presente esattamente quando l’aliquota IVA è 0, con rifiuto dei codici generici N2, N3 e N6 che lo SDI non accetta più dal 2021.
- Venditori in regime forfettario (RF19): tutte le righe ad aliquota 0 con natura N2.2.
- Le note di credito e di debito devono fare riferimento alla fattura che rettificano.
- Date, lunghezze dei campi e set di caratteri: virgolette e trattini tipografici vengono convertiti in ASCII, mentre i caratteri fuori da Latin-1, come le emoji, vengono segnalati invece di essere rimossi in silenzio.
FatturaPAError.issues:
number must contain at least one digit
date must be a valid YYYY-MM-DD date
seller.vatNumber is not a valid Partita IVA
seller.address.postalCode must be 5 digits
buyer needs a vatNumber or a fiscalCode
lines[0].nature is required when the VAT rate is 0
lines[0].description contains "🍷", which SDI does not acceptL’emissione per conto terzi ha un posto dedicato nel tracciato. Quando viene impostato un emittente (issuer), il file include il blocco TerzoIntermediarioOSoggettoEmittente e il codice SoggettoEmittente TZ. La fattura resta del venditore: sua la Partita IVA, sua la numerazione e suoi i riferimenti bancari. Per farlo la piattaforma ha bisogno della delega del venditore.
I clienti esteri non rientrano nei campi italiani, quindi la libreria li compila come richiesto dallo SDI: identificativo fiscale estero privato del prefisso paese (sia EL sia GR per la Grecia), codice destinatario XXXXXXX e CAP 00000, con il vero codice postale anteposto al nome della città. Lo SDI non recapita queste fatture al cliente estero, perciò il venditore gli invia comunque una copia di cortesia.
Le fatture esenti o non soggette a IVA devono l’imposta di bollo virtuale da 2 € quando la somma degli importi senza IVA supera i 77,47 €. Per impostazione predefinita la libreria somma le righe la cui natura è soggetta a bollo e dichiara l’imposta al superamento della soglia. Il bollo viene dichiarato nel blocco dedicato, non aggiunto automaticamente al totale; il venditore che lo addebita al cliente aggiunge una riga apposita.
- Fatture (TD01), note di credito (TD04), note di debito (TD05) e fatture differite (TD24).
- Sconti e maggiorazioni di riga in cascata, in percentuale o a importo unitario (ScontoMaggiorazione).
- Riferimenti agli ordini di acquisto e coordinate per il pagamento tramite bonifico.
- allocateCents, che ripartisce proporzionalmente un importo, come un costo di consegna, tra più righe garantendo che le parti sommino sempre al totale esatto.
Agenzia delle Entrate: guida al bollo sulle fatture elettroniche ↗
L’aritmetica è metà del lavoro; l’altra metà è la struttura. Gli elementi di FatturaPA devono comparire nell’ordine esatto definito dallo schema XSD, perciò l’XML viene costruito come un piccolo albero già ordinato e serializzato da poche decine di righe in xml.ts, senza alcuna libreria XML esterna.
npm run test:schema scarica lo schema ufficiale FatturaPA e lo schema W3C XML Signature da esso importato, punta l’import alla copia locale e valida con xmllint fatture ordinarie, note di credito, fatture scontate e fatture verso clienti esteri. GitHub Actions esegue questo controllo insieme al type check e al controllo di formattazione su Node.js 20, 22 e 24.
Il test utilizza lo schema 1.2.2. L’Agenzia delle Entrate pubblica anche la versione 1.2.3, che accompagna le specifiche tecniche 1.4 in vigore dal 1° aprile 2025; aggiunge soltanto il tipo documento TD29 e il regime fiscale RF20, e gli stessi file generati risultano validi anche rispetto ad essa.
npm install
npm test # unit test
npm run test:schema # valida anche i file generati contro l'XSD ufficiale (richiede curl e xmllint)Documentazione del formato FatturaPA ↗Lo script per lo schema ↗Test ↗
Cosa non fa
Non firma e non trasmette le fatture. L’XML passa attraverso un intermediario SDI o il proprio canale accreditato, ed è lì che appartiene quella parte. Non è nemmeno consulenza fiscale: l’aliquota IVA, il codice natura e il riferimento normativo di ciascuna riga spettano al venditore e al suo commercialista. La libreria verifica che i dati siano coerenti per lo SDI, non che siano quelli fiscalmente corretti per la specifica vendita.
Nel marketplace le fatture vengono generate a ogni consegna completata, ma l’invio allo SDI è ancora simulato. Nessun file generato è ancora passato dal sistema reale; accadrà non appena verrà collegato un intermediario.
La versione 0.1.1 è disponibile su npm come @lucianonerydev/fatturapa. Richiede Node.js 20 o successivo, distribuisce moduli ES con dichiarazioni di tipo e non ha dipendenze a runtime. Il marketplace ne mantiene una copia integrata e le modifiche arrivano prima nel repository pubblico.
I prossimi passi in programma, all’incirca in quest’ordine:
- Ritenuta d’acconto e cassa previdenziale, per le fatture dei professionisti.
- Pagamenti a rate, ciascuno con la propria scadenza e il proprio importo.
- Fatture verso la Pubblica Amministrazione (FPA12), con codici CIG, CUP e codici univoci ufficio.
- Rappresentanti fiscali e cedenti esteri con Partita IVA italiana.
- Parsing delle fatture ricevute nelle stesse strutture dati.
- Generazione della copia di cortesia in PDF, come pacchetto separato.
npm install @lucianonerydev/fatturapa
