Como corrigir o erro de codificação UTF-8 do SAF-T em Portugal
Porque a AT rejeita silenciosamente ficheiros SAF-T XML declarados como UTF-8 e a correção de uma linha que faz com que passem.
O erro que não consegue ver
Submete o seu SAF-T XML mensal à AT, o portal mostra "ficheiro inválido" e nada mais. Sem número de linha. Sem razão. O conselho habitual é voltar a exportar do ERP e submeter outra vez. Faz isso. Continua a falhar.
Se o seu ERP for Xero, QuickBooks, Datev ou uma exportação SAP genérica, é quase de certeza o mesmo bug: o seu ficheiro está codificado em UTF-8, mas a AT só aceita Windows-1252.
Porque é que a AT rejeita UTF-8 em silêncio
A especificação SAF-T (PT) da AT, esquema atual 1.04_01, exige Windows-1252 (CP-1252) como codificação do ficheiro. O validador da AT não devolve um erro útil quando a codificação está errada — o ficheiro é simplesmente rejeitado à entrada. O XML está bem formado; o esquema passa nas verificações. Mas a codificação ao nível dos bytes dos caracteres acentuados (ç, ã, é, ó) não corresponde ao que as ferramentas a jusante da AT esperam.
Os ERPs estrangeiros usam UTF-8 por defeito porque UTF-8 é o padrão moderno em todo o lado, exceto neste pipeline fiscal português específico. Eles escrevem o prólogo XML assim:
<?xml version="1.0" encoding="UTF-8"?>
A AT espera:
<?xml version="1.0" encoding="Windows-1252"?>
Porque é que mexer só na declaração não resolve
O primeiro instinto é abrir o ficheiro, substituir UTF-8 por Windows-1252 no prólogo, gravar e voltar a submeter. Isso piora as coisas. A declaração agora diz Windows-1252 mas os bytes continuam a ser UTF-8. Qualquer caractere fora de ASCII fica em garbage quando a AT ou qualquer outra pessoa lê o ficheiro com a codificação declarada.
A correção que funciona mesmo
Precisa de fazer duas coisas, pela ordem certa:
- Transcodifique os bytes do corpo de UTF-8 para Windows-1252. Cada Á (0xC3 0x81 em UTF-8) passa a um único byte 0xC1. Cada ç (0xC3 0xA7) passa a 0xE7. E por aí adiante.
- Reescreva a declaração de codificação para corresponder:
encoding="Windows-1252".
Se saltar o passo 1, fica com garbage. Se saltar o passo 2, a AT continua a rejeitar.
Fazê-lo em PHP
$xml = file_get_contents('saft.xml');
$converted = mb_convert_encoding($xml, 'Windows-1252', 'UTF-8');
$converted = preg_replace(
'/(<\?xml[^>]*encoding=)["\'][^"\']+["\']/i',
'$1"Windows-1252"',
$converted,
1,
);
file_put_contents('saft.fixed.xml', $converted);
Fazê-lo num clique
O SAFTCheck faz exatamente esta transformação, mais retira quaisquer bytes BOM UTF-8 (outra causa de rejeição silenciosa), valida o ficheiro corrigido contra o esquema AT 1.04_01 e devolve um XML limpo para download. Grátis para uma correção; €7 para a correção paga com relatório completo e PDF.
Como confirmar que a correção pegou
Depois da conversão, corra file saft.fixed.xml num terminal Unix — deve devolver "Non-ISO extended-ASCII text" ou "ISO-8859 text", e não "UTF-8 Unicode". Abra o ficheiro num editor hexadecimal: qualquer caractere português acentuado deve ser um único byte na gama 0xA0–0xFF, e não uma sequência de dois bytes com prefixo 0xC2/0xC3.
Submeta o ficheiro à AT outra vez. O "ficheiro inválido" deve desaparecer. Se não desaparecer, a codificação não era o único problema — mas geralmente é.
Regras relacionadas que o SAFTCheck apanha ao mesmo tempo
- BOM no início do ficheiro. A AT rejeita BOM UTF-8 (0xEF 0xBB 0xBF) e BOMs UTF-16.
- Dígito de controlo do NIF. Algoritmo Mod-11; um dígito errado e o ficheiro é inválido.
- Formato ATCUD. Obrigatório desde 2023, tem de corresponder a
^[A-Z0-9]{8}-[1-9]\d{0,9}$. - Datas do cabeçalho. StartDate antes de EndDate, FiscalYear corresponde ao ano de StartDate, EndDate não está no futuro.