Salta ai contenuti

Import XML — Riferimento tecnico

Questa pagina è il complemento avanzato a Import XML FatturaPA. Qui trovi i dettagli implementativi rilevanti per integrazioni, automazione e debug. La guida utente è quella linkata sopra.

XIQUIL accetta tutti i TipoDocumento previsti dallo schema FatturaPA v1.2.2. La tabella elenca i codici più frequenti e il trattamento applicato:

CodiceDescrizioneComportamento XIQUIL
TD01FatturaStandard. ImportoTotaleDocumento opzionale, fallback sulla somma righe.
TD02Acconto/Anticipo su fatturaStandard.
TD03Acconto/Anticipo su parcellaStandard.
TD04Nota di creditoSegno negativo applicato automaticamente a imponibile, IVA, sconto e totale (header e voci). Quantità e prezzo unitario delle voci restano positivi.
TD05Nota di debitoStandard, segno positivo.
TD06ParcellaStandard. Tipico per fatture professionisti.
TD16–TD27Reverse charge / autofattura / integrazioneStandard. Il regime IVA (es. N6.x per inversione contabile) viene letto dal DatiRiepilogo e replicato sulle voci.

Determinata da services/import_documento_service.py::detect_xml_direction():

  1. Confronta cessionario.p_iva con i Soggetti dell’utente → match → Uscita (l’utente è il compratore)
  2. Confronta cedente.p_iva con i Soggetti → match → Entrata (l’utente è il venditore)
  3. Entrambi match o nessun match → ambiguous, l’utente sceglie nel dialog

La direzione viene propagata nel payload ImportConfirmData.direzione ('uscita' | 'entrata') e seleziona la branch backend (_create_uscita_from_xml_preview vs _create_entrata_from_xml_preview).

Per una fattura importata, ImportoTotaleDocumento e gli aggregati di DatiRiepilogo restano autorevoli. La modifica successiva di una riga non normalizza totale, imponibile o iva dell’owner: il frontend calcola e mostra invece gli scarti rispetto alle righe come diagnostica non contabile.

Categoria e focus continuano a essere salvati sulle righe e restano modificabili, perché sono classificazioni XIQUIL e non campi fiscali autoritativi dell’XML.

L’upload prova ad associare cedente o cessionario a una anagrafica esistente tramite identità fiscale. Se non trova un match ma il preview contiene una identità utilizzabile, la revisione precompila un controparte_draft e seleziona l’opzione sintetica di creazione alla conferma. La selezione di un’anagrafica esistente elimina invece il draft.

Nessuna controparte viene creata durante la raccolta dei dati. Alla conferma atomica, _resolve_atomic_counterparty usa l’ID scelto oppure crea l’anagrafica dal preview XML autorevole, includendo i fallback fiscali esteri, nella stessa transazione del documento e degli allegati.

XIQUIL applica due controlli complementari:

  1. All’upload XML, SHA-256 del contenuto e chiave fiscale intercettano i re-upload dello stesso file o dello stesso corpo FatturaPA. Gli import scartati o in errore restano esclusi per consentire il recupero.
  2. Prima della mutazione, il preflight confronta una identità canonica e indipendente dal formato: numero documento, identità fiscali di cedente e cessionario, data, totale e valuta. I candidati possono essere import in coda, Uscite o Entrate già confermate.

Il confronto produce un punteggio spiegabile e distingue campi coincidenti, diversi e mancanti. I livelli full, strong e suspect generano un conflitto HTTP 409 con le azioni eseguibili; un match weak non blocca il salvataggio. La decisione include identità e revisioni del match, così una situazione cambiata deve essere valutata di nuovo prima di scrivere.

I percorsi manuali espongono create_separate; la UI offre anche la navigazione al documento esistente. La decisione viene registrata e quella stessa coppia è esclusa dai controlli sulle modifiche successive. In revisione import sono disponibili anche merge_into_staging e merge_attach: foto, PDF e XML della stessa fattura convergono così sul medesimo owner e ogni sorgente viene conservata come allegato.

Response di upload:

{
imported: ImportDocumentoSummary[];
duplicates: DuplicateInfo[]; // { filename, reason: 'pending'|'confirmed', existing_codice, existing_data, existing_numero }
errors: { filename, error_message }[];
files_received: number;
}

Il parser legge tutta la struttura DatiPagamento (FePagamento + FeDettaglioPagamento) e la espone nel preview JSON come array pagamenti:

{
"pagamenti": [
{
"condizioni_pagamento": "TP01",
"dettagli": [
{
"modalita_pagamento": "MP05",
"data_scadenza_pagamento": "2026-06-30",
"importo_pagamento": 1220.00,
"iban": "IT60X0542811101000000123456",
"istituto_finanziario": "Unicredit"
}
]
}
]
}

Il dialog di revisione mostra la card Suggerimento dalla fattura con:

  • Codici condizioni pagamento (TP01–TP03) e modalità (MP01–MP23) decodificati in italiano
  • Tabella riepilogativa righe (modalità, IBAN, importo, scadenza)
  • Bottone CTA “Crea scadenze in attesa”. Il bottone:
    • È disabilitato finché non è selezionato l’ambito
    • Salta righe senza data o con importo ≤ 0
    • Sostituisce il draft locale con una scadenza per ogni riga valida, rinumerata da 1
    • L’IBAN della riga viene messo nelle note della scadenza
    • Salva documento, scadenze e allegati insieme alla conferma atomica

Niente auto-creazione silenziosa: la scelta di applicare i DatiPagamento è sempre esplicita dell’utente.

Il parser XML può produrre valori Python come Decimal, date e datetime. Prima di salvare il preview in ImportDocumento.dati_preview JSONB, XIQUIL normalizza ricorsivamente il payload in tipi JSON-safe: numeri, stringhe ISO, liste e oggetti plain.

Questa normalizzazione vale solo al confine di persistenza/API: la logica di parsing e calcolo può continuare a usare tipi precisi finché non deve scrivere JSONB o restituire payload serializzati.

Il parser legge entrambi:

  • DatiGenerali/DatiBollo/BolloVirtuale (boolean: SI/NO)
  • DatiGenerali/DatiBollo/ImportoBollo (decimal)

L’importo viene salvato nel campo bollo di Uscita o Entrata (esistente già in entrambi i model, NUMERIC(12,2) nullable). Il valore non diventa una riga IVA: la marca da bollo è classificata contabilmente come voce B14 (oneri diversi di gestione), non come imposta sul valore aggiunto.

Su TD04 il bollo resta positivo: il credit non rimborsa la marca già pagata.

Il backend impone rate limit configurabile (default 10 req/min per endpoint upload, slowapi in-memory). Quando viene superato, gli endpoint ritornano HTTP 429 con header Retry-After.

Il frontend ha doppia protezione per gestire batch reali fino a 200-300 file:

  1. Interceptor 429 in api/client.ts: backoff esponenziale automatico, max 5 tentativi (1s/2s/4s/8s/16s). Se l’header Retry-After è presente, viene preferito alla schedule. Esauriti i tentativi, rilancia un errore con messaggio italiano user-facing.

  2. Chunking in uploadXmlFilesBulk / uploadDocumentFilesBulk: per batch >10 file, spezza in chunk di 10 e processa 2 chunk in parallelo. I risultati vengono accumulati in un singolo ImportUploadResult. Una callback onProgress aggiorna la UI con {done, total, inFlight}.

La combinazione chunking + retry idempotente (XML-DEDUP-01: re-upload produce DuplicateInfo, mai duplicati) rende l’import bulk affidabile anche sotto carico.

Tutti sotto /api/import (auth richiesta, feature import con livello ≥ SOLA_LETTURA per GET, ≥ PERSONALE per POST).

EndpointBodyRisposta
POST /uploadmultipart files[].xmlImportUploadResult
POST /upload-filemultipart files[] (img/pdf)ImportUploadResult
GET /pending/review-items—ImportReviewItem[]
GET /pending/count—{ count: number }
GET /{id}—ImportDocumentoRead (include dati_preview JSON)
POST /{id}/confirm-atomicImportGroupAtomicConfirmDatadocumento creato e import confermato
POST /groups/{group_id}/confirm-atomicImportGroupAtomicConfirmDatadocumento e PDF multi-pagina creati
POST /{id}/reconcileImportReconciliationRequestesito unione/allegato
POST /{id}/confirm-link-existingImportLinkExistingDataallegato collegato all’owner
DELETE /{id}—204
DELETE /pendingquery ids opzionaleconteggio import scartati

Gli endpoint confirm-atomic sono il percorso primario della revisione: owner, satellite, righe, scadenze draft, allegati e stato import vengono confermati nella stessa transazione. Il 409 di preflight va risolto tramite la decisione proposta e, per le azioni di unione, tramite /{id}/reconcile.

Il file dati_preview è un JSON con questa shape (campi rilevanti):

{
header: { formato_trasmissione, codice_destinatario, pec_destinatario },
cedente: { denominazione, p_iva, cf, indirizzo, ... },
cessionario: { denominazione, p_iva, cf, indirizzo, ... }, // solo se presente
dati_generali: {
tipo_documento: string,
numero: string, data: string, valuta: string,
importo_totale_documento?: number,
arrotondamento?: number,
bollo_virtuale: boolean,
importo_bollo?: number,
art73: boolean,
},
causali: string[],
ritenute: [...],
casse_previdenziali: [...],
sconti_maggiorazioni: [...],
linee: [
{
numero_linea, descrizione, quantita, prezzo_unitario,
imponibile, iva, totale, aliquota_iva, natura?,
...
}
],
riepilogo_iva: [...],
pagamenti: [{ condizioni_pagamento, dettagli: [...] }],
totale_status?: { severity, dichiarato, calcolato, differenza, arrotondamento_dichiarato },
proposed_fornitore: { ... }, // pre-built al momento dell'upload (path Uscita)
}