Fix integración VeriFactu AEAT: XML, hash y encadenamiento
Bugs corregidos tras validación directa contra la plataforma de pruebas: - Hash: formato key=valor& en lugar de separador | - TipoHuella: código 01 en lugar de literal SHA-256 - FechaHoraHusoGenRegistro: formato ISO 8601 con huso horario - IdSistemaInformatico: 2 caracteres (01) - Namespaces SOAP: URLs correctas de www2.agenciatributaria.gob.es - Estructura XML: eliminados wrappers Content/AltaReq sobrantes - Encadenamiento: xs:choice correcto (PrimerRegistro XOR RegistroAnterior) - RegistroAnterior: referencia datos de la factura anterior, no la actual - Destinatarios: bloque obligatorio para facturas F1/F3/rectificativas - wsu:Id: eliminado atributo con namespace no declarado - Parser respuesta: mapeado a RespuestaRegFactuSistemaFacturacion real - Campo emisor_nombre añadido al input de factura La API envía y recibe correctamente contra prewww1.aeat.es con respuesta EstadoEnvio=Correcto y CSV asignado por la AEAT. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b9899684cc
commit
1ad84a3b14
27
README.md
27
README.md
|
|
@ -21,10 +21,12 @@ server:
|
|||
port: 6789
|
||||
|
||||
verifactu:
|
||||
environment: test
|
||||
production: false # true para producción
|
||||
|
||||
certificates:
|
||||
storage_path: ./certs/
|
||||
storage_path: ./data/certs/
|
||||
cert_file: ./data/certs/personal.p12
|
||||
cert_password: TU_CONTRASEÑA
|
||||
|
||||
crypto:
|
||||
keys_path: ./keys/
|
||||
|
|
@ -81,6 +83,7 @@ Authorization: Bearer <token>
|
|||
"tipo": "alta",
|
||||
"factura": {
|
||||
"emisor_nif": "A12345678",
|
||||
"emisor_nombre": "NOMBRE REGISTRADO EN AEAT",
|
||||
"num_serie": "2024-001",
|
||||
"fecha_expedicion": "13-09-2024",
|
||||
"tipo_factura": "F1",
|
||||
|
|
@ -95,7 +98,7 @@ Authorization: Bearer <token>
|
|||
"importe_total": 121.00
|
||||
},
|
||||
"sistema": {
|
||||
"nombre": "Software",
|
||||
"nombre": "NOMBRE REGISTRADO EN AEAT PARA EL NIF PROVEEDOR",
|
||||
"nif_proveedor": "A12345678",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
|
@ -126,13 +129,17 @@ Authorization: Bearer <token>
|
|||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| `tipo` | string | `alta` o `anulacion` |
|
||||
| `emisor_nif` | string | NIF emisor (9 caracteres) |
|
||||
| `num_serie` | string | Número de serie |
|
||||
| `fecha_expedicion` | string | Fecha (`dd-mm-yyyy`) |
|
||||
| `tipo_factura` | string | `F1`, `F2`, `R1`-`R5` |
|
||||
| `iva[]` | array | Al menos un registro |
|
||||
| `importe_total` | number | > 0 |
|
||||
| `sistema.*` | object | Datos del software |
|
||||
| `factura.emisor_nif` | string | NIF emisor (9 caracteres) |
|
||||
| `factura.emisor_nombre` | string | Nombre del emisor exactamente como figura en el censo AEAT |
|
||||
| `factura.num_serie` | string | Número de serie |
|
||||
| `factura.fecha_expedicion` | string | Fecha (`dd-mm-yyyy`) |
|
||||
| `factura.tipo_factura` | string | `F1`, `F2`, `R1`-`R5` |
|
||||
| `factura.destinatario` | object | Obligatorio para F1, F3 y rectificativas |
|
||||
| `factura.iva[]` | array | Al menos un registro |
|
||||
| `factura.importe_total` | number | > 0 |
|
||||
| `sistema.nombre` | string | Nombre del proveedor del software exactamente como figura en el censo AEAT para su NIF |
|
||||
| `sistema.nif_proveedor` | string | NIF del proveedor del software |
|
||||
| `sistema.version` | string | Versión del software |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,32 @@
|
|||
# Obtener Certificado de Pruebas AEAT
|
||||
# Certificado Digital para VeriFactu
|
||||
|
||||
## Introducción
|
||||
|
||||
Para enviar facturas al entorno de pruebas de VeriFactu (no en producción), necesitas un certificado digital de pruebas registrado en el sistema de la AEAT.
|
||||
VeriFactu requiere autenticación mTLS: la API presenta tu certificado al conectarse con la AEAT. Tanto en pruebas como en producción se usa el mismo certificado real — no existe un certificado especial de pruebas.
|
||||
|
||||
## Opciones de Certificados
|
||||
## Certificado válido: FNMT Persona Física
|
||||
|
||||
### Opción 1: Certificado FNMT (Fábrica Nacional de Moneda y Timbre)
|
||||
El **Certificado de Ciudadano (Persona Física) de la FNMT** es gratuito y válido tanto para el entorno de pruebas como para producción.
|
||||
|
||||
Es el más común para empresas y autónomos en España.
|
||||
**Obtención gratuita — opciones:**
|
||||
|
||||
**Pasos:**
|
||||
1. Ir a https://www.fnmt.es/consultas-y-resolucion-de-incidentes/centros-de-emision-y-oficinas
|
||||
2. Buscar una oficina cercana FNMT
|
||||
3. Acudir presencialmente con DNI
|
||||
4. Solicitar certificado de persona física
|
||||
5. Te lo emiten en el acto
|
||||
1. **Presencial en oficina AEAT** (recomendado): llevar el DNI a cualquier delegación de la Agencia Tributaria. Trámite en 2 minutos, sin cita previa en la mayoría.
|
||||
2. **Con DNIe + NFC**: si tu DNI es posterior a 2015, puedes hacer todo el proceso online desde `sede.fnmt.gob.es` usando el chip NFC del DNI con tu móvil o un lector de tarjetas.
|
||||
3. **Videollamada** (coste 2,99 € + IVA): servicio de identificación remota sin desplazamiento.
|
||||
|
||||
### Opción 2: Certificado de la AEAT (solo pruebas)
|
||||
> La renovación online es gratuita mientras el certificado esté vigente (hasta 60 días antes de su vencimiento).
|
||||
|
||||
La AEAT ofrece certificados de pruebas:
|
||||
## Verificar el certificado (PowerShell)
|
||||
|
||||
**Pasos:**
|
||||
1. Ir a https://preportal.aeat.es
|
||||
2. Registro como usuario
|
||||
3. Solicitar certificado de prueba
|
||||
4. Descargar archivo .p12
|
||||
```powershell
|
||||
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(".\data\certs\personal.p12", "TU_CONTRASEÑA")
|
||||
Write-Host "Válido desde:" $cert.NotBefore
|
||||
Write-Host "Válido hasta:" $cert.NotAfter
|
||||
Write-Host "Asunto:" $cert.Subject
|
||||
Write-Host "Vigente:" ($cert.NotAfter -gt (Get-Date))
|
||||
```
|
||||
|
||||
> **Nota**: Los certificados de prueba de la AEAT suelen tener validez limitada.
|
||||
|
||||
### Opción 3: Certificado personal (prod)
|
||||
|
||||
Si ya tienes tu certificado personal (el que usaste antes), puede que funcione en producción.
|
||||
|
||||
**Verificar:**
|
||||
1. ¿Está vigente? (no expired)
|
||||
2. ¿Es cualificado? (firmado por autoridad reconocida)
|
||||
El campo `Subject` debe contener `OU=CIUDADANOS` (persona física) y el NIF del titular.
|
||||
|
||||
## Registrar Nuevo Certificado
|
||||
|
||||
|
|
@ -96,17 +87,6 @@ VerifactuMidAPI/
|
|||
└── config.yml # Configuración
|
||||
```
|
||||
|
||||
## Verificar Certificado
|
||||
|
||||
Para verificar que el certificado es válido:
|
||||
|
||||
```powershell
|
||||
openssl pkcs12 -in .\data\certs\personal.p12 -passin pass:TU_PASS -nokeys -clcerts | openssl x509 -noout -dates
|
||||
```
|
||||
|
||||
Muestra:
|
||||
- Not Before: fecha de inicio de validez
|
||||
- Not After: fecha de caducidad
|
||||
|
||||
## Problemas Comunes
|
||||
|
||||
|
|
|
|||
|
|
@ -33,36 +33,42 @@ Buscar facturas previamente enviadas.
|
|||
|
||||
## Encadenamiento de Hash
|
||||
|
||||
Cada factura incluye el hash de la anterior:
|
||||
Cada factura incluye el hash de la anterior. El formato exacto validado por la AEAT es:
|
||||
|
||||
```
|
||||
Hash(N) = SHA256(Datos(N) + Hash(N-1))
|
||||
SHA256("IDEmisorFactura=NIF&NumSerieFactura=SERIE&FechaExpedicionFactura=dd-mm-yyyy&TipoFactura=F1&CuotaTotal=21.00&ImporteTotal=121.00&Huella=HASH_ANTERIOR&FechaHoraHusoGenRegistro=yyyy-mm-ddThh:mm:ss+hh:mm")
|
||||
```
|
||||
|
||||
Esto garantiza la integridad del registro histórico.
|
||||
Para el primer registro de un emisor, `Huella` se deja vacío y en el XML se envía `<PrimerRegistro>S</PrimerRegistro>`. Para el resto, se envía `<RegistroAnterior>` con los datos de la factura anterior (nunca ambos a la vez).
|
||||
|
||||
## URLs
|
||||
|
||||
### Testing (Preproducción)
|
||||
```
|
||||
https://prewww2.aeat.es/.../SistemaFacturacion
|
||||
https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
|
||||
```
|
||||
|
||||
### Producción
|
||||
```
|
||||
https://www2.agenciatributaria.gob.es/.../SistemaFacturacion
|
||||
https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
|
||||
```
|
||||
|
||||
## Formato
|
||||
|
||||
SOAP 1.1 sobre HTTPS con:
|
||||
- Certificado cliente cualificado
|
||||
- XML con namespaces específicos
|
||||
- Response con CSV (Código de Verificación)
|
||||
SOAP 1.1 sobre HTTPS con mTLS (certificado cliente cualificado). Namespaces requeridos:
|
||||
|
||||
- `xmlns:sum` → `https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd`
|
||||
- `xmlns:sum1` → `https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd`
|
||||
|
||||
Campos específicos a tener en cuenta:
|
||||
- `TipoHuella`: valor `01` (no `SHA-256`)
|
||||
- `FechaHoraHusoGenRegistro`: formato ISO 8601 con huso horario (`yyyy-mm-ddThh:mm:ss+hh:mm`)
|
||||
- `IdSistemaInformatico`: 2 caracteres (`01`, no `1`)
|
||||
- `Encadenamiento`: usa `xs:choice` — o `PrimerRegistro` o `RegistroAnterior`, nunca los dos
|
||||
|
||||
## CSV
|
||||
|
||||
Código de Verificación de 13 caracteres (o hash SHA-256 en desarrollo) que acredita el registro en Hacienda.
|
||||
Código de Verificación asignado por la AEAT con formato `A-XXXXXXXXXXXXX`. Acredita el registro de la factura en Hacienda. Cuando la API opera en modo fallback local devuelve el hash SHA-256 en su lugar y el estado indica `Correcto (local)`.
|
||||
|
||||
## Fallback Local
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
|
|
@ -63,7 +64,8 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
prevHash := ""
|
||||
var prevHash, prevNumSerie string
|
||||
var prevFecha time.Time
|
||||
if s.hashStorage != nil {
|
||||
record, err := s.hashStorage.GetLastRecord(input.Factura.EmisorNIF)
|
||||
if err != nil {
|
||||
|
|
@ -74,6 +76,8 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
|
|||
}
|
||||
if record != nil {
|
||||
prevHash = record.Huella
|
||||
prevNumSerie = record.NumSerie
|
||||
prevFecha = record.Fecha
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +96,7 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
|
|||
data.Huella = currentHash
|
||||
data.FechaGen = now
|
||||
|
||||
altaData := ToAltaData(&input.InvoiceInput, data, currentHash, prevHash)
|
||||
altaData := ToAltaData(&input.InvoiceInput, data, currentHash, prevHash, prevNumSerie, prevFecha)
|
||||
|
||||
if s.verifactu != nil {
|
||||
log.Printf("Sending to AEAT...")
|
||||
|
|
@ -108,8 +112,12 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
|
|||
goto saveLocal
|
||||
}
|
||||
|
||||
if resp.Body.RegistroRespuesta != nil {
|
||||
if resp.Body.RegistroRespuesta.Estado == "Correcto" {
|
||||
if resp.Body.Respuesta != nil {
|
||||
estado := resp.Body.Respuesta.EstadoEnvio
|
||||
csv := resp.Body.Respuesta.CSV
|
||||
log.Printf("AEAT EstadoEnvio: %s CSV: %s", estado, csv)
|
||||
|
||||
if estado == "Correcto" {
|
||||
if s.hashStorage != nil {
|
||||
if err := s.hashStorage.SaveLastRecord(lastRecord); err != nil {
|
||||
return &AltaOutput{
|
||||
|
|
@ -118,17 +126,22 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
|
|||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &AltaOutput{
|
||||
Success: true,
|
||||
CSV: resp.Body.RegistroRespuesta.CSV,
|
||||
Estado: resp.Body.RegistroRespuesta.Estado,
|
||||
CSV: csv,
|
||||
Estado: "Correcto",
|
||||
}, nil
|
||||
}
|
||||
|
||||
errMsg := estado
|
||||
if len(resp.Body.Respuesta.RespuestaLineas) > 0 {
|
||||
l := resp.Body.Respuesta.RespuestaLineas[0]
|
||||
errMsg = fmt.Sprintf("%s [%s] %s", estado, l.CodigoError, l.DescripcionError)
|
||||
}
|
||||
log.Printf("AEAT error: %s", errMsg)
|
||||
return &AltaOutput{
|
||||
Success: false,
|
||||
Error: "aeat_error:" + resp.Body.RegistroRespuesta.Estado,
|
||||
Error: "aeat_error: " + errMsg,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +201,7 @@ func (s *FacturaService) ProcessAnulacion(input AnulacionInput) (*AnulacionOutpu
|
|||
}, nil
|
||||
}
|
||||
|
||||
func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) verifactu.AltaData {
|
||||
func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash, prevNumSerie string, prevFecha time.Time) verifactu.AltaData {
|
||||
ivaData := make([]verifactu.IVARegularizacionData, len(data.IVA))
|
||||
for i, iva := range data.IVA {
|
||||
ivaData[i] = verifactu.IVARegularizacionData{
|
||||
|
|
@ -208,7 +221,7 @@ func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) ve
|
|||
}
|
||||
|
||||
return verifactu.AltaData{
|
||||
EmisorNombre: in.Factura.EmisorNIF,
|
||||
EmisorNombre: in.Factura.EmisorNombre,
|
||||
EmisorNIF: data.EmisorNIF,
|
||||
NumSerie: data.NumSerie,
|
||||
FechaExpedicion: data.Fecha,
|
||||
|
|
@ -227,8 +240,10 @@ func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) ve
|
|||
NumeroInstalacion: data.Sistema.NumeroInstalacion,
|
||||
TipoUsoVerifactu: data.Sistema.TipoUsoVerifactu,
|
||||
},
|
||||
Huella: huella,
|
||||
PrevHash: prevHash,
|
||||
FechaGen: data.FechaGen,
|
||||
Huella: huella,
|
||||
PrevHash: prevHash,
|
||||
PrevNumSerie: prevNumSerie,
|
||||
PrevFecha: prevFecha,
|
||||
FechaGen: data.FechaGen,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
|
||||
type HashService struct {
|
||||
lastRecord *LastRecord
|
||||
}
|
||||
|
|
@ -37,9 +38,10 @@ func (s *HashService) GetLastRecord() *LastRecord {
|
|||
}
|
||||
|
||||
func (s *HashService) CalculateHash(data *InvoiceData, previousHash string) string {
|
||||
fechaGen := data.FechaGen.Format(time.RFC3339)
|
||||
fechaGen := data.FechaGen.Format("2006-01-02T15:04:05-07:00")
|
||||
|
||||
fields := fmt.Sprintf("%s|%s|%s|%s|%.2f|%.2f|%s|%s",
|
||||
fields := fmt.Sprintf(
|
||||
"IDEmisorFactura=%s&NumSerieFactura=%s&FechaExpedicionFactura=%s&TipoFactura=%s&CuotaTotal=%.2f&ImporteTotal=%.2f&Huella=%s&FechaHoraHusoGenRegistro=%s",
|
||||
data.EmisorNIF,
|
||||
data.NumSerie,
|
||||
data.Fecha.Format("02-01-2006"),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type InvoiceInput struct {
|
|||
|
||||
type FacturaInput struct {
|
||||
EmisorNIF string `json:"emisor_nif"`
|
||||
EmisorNombre string `json:"emisor_nombre"`
|
||||
NumSerie string `json:"num_serie"`
|
||||
FechaExpedicion string `json:"fecha_expedicion"`
|
||||
TipoFactura string `json:"tipo_factura"`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Peticion directa al endpoint VeriFactu de la AEAT (sin pasar por la API).
|
||||
Usa mTLS con los PEM ya convertidos en data/certs/.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import requests
|
||||
|
||||
# Certificado (los PEM no tienen contrasena)
|
||||
CERT_PEM = "data/certs/cert_cert.pem"
|
||||
KEY_PEM = "data/certs/cert_key.pem"
|
||||
|
||||
# Identidad
|
||||
NIF = "53950250R"
|
||||
NOMBRE = "JOSEP VICENT MESTRE LLOBELL"
|
||||
|
||||
# Endpoint de pruebas AEAT
|
||||
URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
|
||||
|
||||
# Datos de la factura de prueba
|
||||
NUM_SERIE = "TEST-DIRECTO-003"
|
||||
FECHA_EXP = "13-05-2026"
|
||||
TIPO_FACTURA = "F1"
|
||||
BASE = 100.00
|
||||
CUOTA = 21.00
|
||||
TOTAL = 121.00
|
||||
|
||||
# Registro anterior (encadenamiento)
|
||||
PREV_NUM_SERIE = "TEST-DIRECTO-002"
|
||||
PREV_FECHA_EXP = "13-05-2026"
|
||||
PREV_HUELLA = "B4F12C2C6407438501BBB5C81A8443E78860CD2D736D614C032CEDB4CC521D90"
|
||||
|
||||
# Timestamp generacion (mismo formato para hash y XML)
|
||||
_now = datetime.now(timezone.utc)
|
||||
FECHA_GEN = _now.strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||
|
||||
|
||||
def calcular_huella(nif, num_serie, fecha_exp, tipo, cuota, total, prev_hash, fecha_gen):
|
||||
"""SHA-256 segun la especificacion VeriFactu (formato key=value&)."""
|
||||
campos = (
|
||||
f"IDEmisorFactura={nif}&"
|
||||
f"NumSerieFactura={num_serie}&"
|
||||
f"FechaExpedicionFactura={fecha_exp}&"
|
||||
f"TipoFactura={tipo}&"
|
||||
f"CuotaTotal={cuota:.2f}&"
|
||||
f"ImporteTotal={total:.2f}&"
|
||||
f"Huella={prev_hash}&"
|
||||
f"FechaHoraHusoGenRegistro={fecha_gen}"
|
||||
)
|
||||
print(f" Campos huella : {campos}")
|
||||
return hashlib.sha256(campos.encode()).hexdigest().upper()
|
||||
|
||||
|
||||
huella = calcular_huella(NIF, NUM_SERIE, FECHA_EXP, TIPO_FACTURA, CUOTA, TOTAL, PREV_HUELLA, FECHA_GEN)
|
||||
|
||||
SOAP = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soapenv:Envelope
|
||||
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd"
|
||||
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<sum:RegFactuSistemaFacturacion>
|
||||
<sum:Cabecera>
|
||||
<sum1:ObligadoEmision>
|
||||
<sum1:NombreRazon>{NOMBRE}</sum1:NombreRazon>
|
||||
<sum1:NIF>{NIF}</sum1:NIF>
|
||||
</sum1:ObligadoEmision>
|
||||
</sum:Cabecera>
|
||||
<sum:RegistroFactura>
|
||||
<sum1:RegistroAlta>
|
||||
<sum1:IDVersion>1.0</sum1:IDVersion>
|
||||
<sum1:IDFactura>
|
||||
<sum1:IDEmisorFactura>{NIF}</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>{NUM_SERIE}</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>{FECHA_EXP}</sum1:FechaExpedicionFactura>
|
||||
</sum1:IDFactura>
|
||||
<sum1:NombreRazonEmisor>{NOMBRE}</sum1:NombreRazonEmisor>
|
||||
<sum1:TipoFactura>{TIPO_FACTURA}</sum1:TipoFactura>
|
||||
<sum1:DescripcionOperacion>Factura de prueba directa</sum1:DescripcionOperacion>
|
||||
<sum1:Destinatarios>
|
||||
<sum1:IDDestinatario>
|
||||
<sum1:NombreRazon>{NOMBRE}</sum1:NombreRazon>
|
||||
<sum1:NIF>{NIF}</sum1:NIF>
|
||||
</sum1:IDDestinatario>
|
||||
</sum1:Destinatarios>
|
||||
<sum1:Desglose>
|
||||
<sum1:DetalleDesglose>
|
||||
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
|
||||
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
|
||||
<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>
|
||||
<sum1:BaseImponibleOimporteNoSujeto>{BASE:.2f}</sum1:BaseImponibleOimporteNoSujeto>
|
||||
<sum1:CuotaRepercutida>{CUOTA:.2f}</sum1:CuotaRepercutida>
|
||||
</sum1:DetalleDesglose>
|
||||
</sum1:Desglose>
|
||||
<sum1:CuotaTotal>{CUOTA:.2f}</sum1:CuotaTotal>
|
||||
<sum1:ImporteTotal>{TOTAL:.2f}</sum1:ImporteTotal>
|
||||
<sum1:Encadenamiento>
|
||||
<sum1:RegistroAnterior>
|
||||
<sum1:IDEmisorFactura>{NIF}</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>{PREV_NUM_SERIE}</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>{PREV_FECHA_EXP}</sum1:FechaExpedicionFactura>
|
||||
<sum1:Huella>{PREV_HUELLA}</sum1:Huella>
|
||||
</sum1:RegistroAnterior>
|
||||
</sum1:Encadenamiento>
|
||||
<sum1:SistemaInformatico>
|
||||
<sum1:NombreRazon>{NOMBRE}</sum1:NombreRazon>
|
||||
<sum1:NIF>{NIF}</sum1:NIF>
|
||||
<sum1:NombreSistemaInformatico>VerifactuMidAPI</sum1:NombreSistemaInformatico>
|
||||
<sum1:IdSistemaInformatico>01</sum1:IdSistemaInformatico>
|
||||
<sum1:Version>1.0.0</sum1:Version>
|
||||
<sum1:NumeroInstalacion>1</sum1:NumeroInstalacion>
|
||||
<sum1:TipoUsoPosibleSoloVerifactu>S</sum1:TipoUsoPosibleSoloVerifactu>
|
||||
<sum1:TipoUsoPosibleMultiOT>N</sum1:TipoUsoPosibleMultiOT>
|
||||
<sum1:IndicadorMultiplesOT>N</sum1:IndicadorMultiplesOT>
|
||||
</sum1:SistemaInformatico>
|
||||
<sum1:FechaHoraHusoGenRegistro>{FECHA_GEN}</sum1:FechaHoraHusoGenRegistro>
|
||||
<sum1:TipoHuella>01</sum1:TipoHuella>
|
||||
<sum1:Huella>{huella}</sum1:Huella>
|
||||
</sum1:RegistroAlta>
|
||||
</sum:RegistroFactura>
|
||||
</sum:RegFactuSistemaFacturacion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>"""
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Endpoint : {URL}")
|
||||
print(f"NIF : {NIF}")
|
||||
print(f"Num serie : {NUM_SERIE}")
|
||||
print(f"Huella : {huella}")
|
||||
print(f"Fecha gen : {FECHA_GEN}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
URL,
|
||||
data=SOAP.encode("utf-8"),
|
||||
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": ""},
|
||||
cert=(CERT_PEM, KEY_PEM),
|
||||
verify=True,
|
||||
timeout=30,
|
||||
)
|
||||
print(f"HTTP Status : {resp.status_code}")
|
||||
print(f"\nRespuesta AEAT:\n{resp.text}")
|
||||
|
||||
except requests.exceptions.SSLError as e:
|
||||
print(f"[ERROR SSL] Problema con el certificado: {e}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"[ERROR CONEXION] No se pudo conectar: {e}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {type(e).__name__}: {e}")
|
||||
|
|
@ -97,14 +97,13 @@ type Reference2 struct {
|
|||
type SOAPBody struct {
|
||||
XMLName xml.Name `xml:"soap:Body"`
|
||||
Content interface{} `xml:",any"`
|
||||
Id string `xml:"wsu:Id,attr"`
|
||||
}
|
||||
|
||||
func BuildSOAPEnvelope(payload interface{}) *SOAPEnvelope {
|
||||
return &SOAPEnvelope{
|
||||
XmlnsSOAP: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
XmlnsSUM: "https://www.agenciatributaria.gob.es/static/files/common/xsd/sum/information.xsd",
|
||||
XmlnsSUM1: "https://www.agenciatributaria.gob.es/static/files/common/xsd/sum/suministroInformacion.xsd",
|
||||
XmlnsSUM: "https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd",
|
||||
XmlnsSUM1: "https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd",
|
||||
Body: SOAPBody{
|
||||
Content: payload,
|
||||
},
|
||||
|
|
@ -136,23 +135,11 @@ type EnviarAnulacionRequest struct {
|
|||
}
|
||||
|
||||
func BuildAltaSOAPRequest(data AltaData) (*SOAPEnvelope, error) {
|
||||
altaReq := BuildAltaRequest(data)
|
||||
|
||||
env := BuildSOAPEnvelope(EnviarFacturaRequest{
|
||||
AltaReq: altaReq,
|
||||
})
|
||||
|
||||
return env, nil
|
||||
return BuildSOAPEnvelope(BuildAltaRequest(data)), nil
|
||||
}
|
||||
|
||||
func BuildAnulacionSOAPRequest(data AltaData) (*SOAPEnvelope, error) {
|
||||
anulReq := BuildAnulacionRequest(data)
|
||||
|
||||
env := BuildSOAPEnvelope(EnviarAnulacionRequest{
|
||||
AnulReq: anulReq,
|
||||
})
|
||||
|
||||
return env, nil
|
||||
return BuildSOAPEnvelope(BuildAnulacionRequest(data)), nil
|
||||
}
|
||||
|
||||
func ParseResponse(data []byte) (*Response, error) {
|
||||
|
|
|
|||
132
verifactu/xml.go
132
verifactu/xml.go
|
|
@ -7,8 +7,13 @@ import (
|
|||
)
|
||||
|
||||
type AltaRequest struct {
|
||||
Cabecera Cabecera `xml:"sum:RegFactuSistemaFacturacion>sum:Cabecera"`
|
||||
RegistroAlta RegistroAlta `xml:"sum:RegFactuSistemaFacturacion>sum:RegistroFactura>sum1:RegistroAlta"`
|
||||
XMLName xml.Name `xml:"sum:RegFactuSistemaFacturacion"`
|
||||
Cabecera Cabecera `xml:"sum:Cabecera"`
|
||||
RegistroFactura AltaRegistroFactura `xml:"sum:RegistroFactura"`
|
||||
}
|
||||
|
||||
type AltaRegistroFactura struct {
|
||||
RegistroAlta RegistroAlta `xml:"sum1:RegistroAlta"`
|
||||
}
|
||||
|
||||
type Cabecera struct {
|
||||
|
|
@ -26,6 +31,7 @@ type RegistroAlta struct {
|
|||
NombreRazonEmisor string `xml:"sum1:NombreRazonEmisor"`
|
||||
TipoFactura string `xml:"sum1:TipoFactura"`
|
||||
DescripcionOperacion string `xml:"sum1:DescripcionOperacion"`
|
||||
Destinatarios *Destinatarios `xml:"sum1:Destinatarios,omitempty"`
|
||||
Desglose Desglose `xml:"sum1:Desglose"`
|
||||
CuotaTotal string `xml:"sum1:CuotaTotal"`
|
||||
ImporteTotal string `xml:"sum1:ImporteTotal"`
|
||||
|
|
@ -88,8 +94,13 @@ type SistemaInformatico struct {
|
|||
}
|
||||
|
||||
type AnulacionRequest struct {
|
||||
Cabecera Cabecera `xml:"sum:RegFactuSistemaFacturacion>sum:Cabecera"`
|
||||
RegistroAnulacion RegistroAnulacion `xml:"sum:RegFactuSistemaFacturacion>sum:RegistroFactura>sum1:RegistroAnulacion"`
|
||||
XMLName xml.Name `xml:"sum:RegFactuSistemaFacturacion"`
|
||||
Cabecera Cabecera `xml:"sum:Cabecera"`
|
||||
RegistroFactura AnulacionRegistroFactura `xml:"sum:RegistroFactura"`
|
||||
}
|
||||
|
||||
type AnulacionRegistroFactura struct {
|
||||
RegistroAnulacion RegistroAnulacion `xml:"sum1:RegistroAnulacion"`
|
||||
}
|
||||
|
||||
type RegistroAnulacion struct {
|
||||
|
|
@ -114,9 +125,9 @@ type Response struct {
|
|||
}
|
||||
|
||||
type ResponseBody struct {
|
||||
XMLName xml.Name `xml:"Body"`
|
||||
Fault *Fault `xml:"Fault,omitempty"`
|
||||
RegistroRespuesta *RegistroRespuesta `xml:"RegistroRespuesta,omitempty"`
|
||||
XMLName xml.Name `xml:"Body"`
|
||||
Fault *Fault `xml:"Fault"`
|
||||
Respuesta *RespuestaRegFactu `xml:"RespuestaRegFactuSistemaFacturacion"`
|
||||
}
|
||||
|
||||
type Fault struct {
|
||||
|
|
@ -124,9 +135,16 @@ type Fault struct {
|
|||
FaultString string `xml:"faultstring"`
|
||||
}
|
||||
|
||||
type RegistroRespuesta struct {
|
||||
CSV string `xml:"CSV"`
|
||||
Estado string `xml:"Estado"`
|
||||
type RespuestaRegFactu struct {
|
||||
CSV string `xml:"CSV"`
|
||||
EstadoEnvio string `xml:"EstadoEnvio"`
|
||||
RespuestaLineas []RespuestaLinea `xml:"RespuestaLinea"`
|
||||
}
|
||||
|
||||
type RespuestaLinea struct {
|
||||
EstadoRegistro string `xml:"EstadoRegistro"`
|
||||
CodigoError string `xml:"CodigoErrorRegistro"`
|
||||
DescripcionError string `xml:"DescripcionErrorRegistro"`
|
||||
}
|
||||
|
||||
type AltaData struct {
|
||||
|
|
@ -145,6 +163,8 @@ type AltaData struct {
|
|||
Huella string
|
||||
FechaGen time.Time
|
||||
PrevHash string
|
||||
PrevNumSerie string
|
||||
PrevFecha time.Time
|
||||
}
|
||||
|
||||
type IVARegularizacionData struct {
|
||||
|
|
@ -166,7 +186,7 @@ type SistemaData struct {
|
|||
|
||||
func BuildAltaRequest(data AltaData) *AltaRequest {
|
||||
fechaExp := data.FechaExpedicion.Format("02-01-2006")
|
||||
fechaGen := data.FechaGen.Format("02-01-2006T15:04:05")
|
||||
fechaGen := data.FechaGen.Format("2006-01-02T15:04:05-07:00")
|
||||
|
||||
ivaList := make([]DetalleDesglose, len(data.IVA))
|
||||
for i, iva := range data.IVA {
|
||||
|
|
@ -184,16 +204,24 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
|
|||
enc = Encadenamiento{PrimerRegistro: "S"}
|
||||
} else {
|
||||
enc = Encadenamiento{
|
||||
PrimerRegistro: "N",
|
||||
RegistroAnterior: &RegistroAnterior{
|
||||
IDEmisorFactura: data.EmisorNIF,
|
||||
NumSerieFactura: data.NumSerie,
|
||||
FechaExpedicionFactura: fechaExp,
|
||||
NumSerieFactura: data.PrevNumSerie,
|
||||
FechaExpedicionFactura: data.PrevFecha.Format("02-01-2006"),
|
||||
Huella: data.PrevHash,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var destinatarios *Destinatarios
|
||||
if data.DestinatarioNIF != "" {
|
||||
destinatarios = &Destinatarios{
|
||||
IDDestinatario: []IDDestinatario{
|
||||
{NombreRazon: data.DestinatarioNombre, NIF: data.DestinatarioNIF},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
req := &AltaRequest{
|
||||
Cabecera: Cabecera{
|
||||
ObligadoEmision: ObligadoEmision{
|
||||
|
|
@ -201,36 +229,39 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
|
|||
NIF: data.EmisorNIF,
|
||||
},
|
||||
},
|
||||
RegistroAlta: RegistroAlta{
|
||||
IDVersion: "1.0",
|
||||
IDFactura: IDFactura{
|
||||
IDEmisorFactura: data.EmisorNIF,
|
||||
NumSerieFactura: data.NumSerie,
|
||||
FechaExpedicionFactura: fechaExp,
|
||||
RegistroFactura: AltaRegistroFactura{
|
||||
RegistroAlta: RegistroAlta{
|
||||
IDVersion: "1.0",
|
||||
IDFactura: IDFactura{
|
||||
IDEmisorFactura: data.EmisorNIF,
|
||||
NumSerieFactura: data.NumSerie,
|
||||
FechaExpedicionFactura: fechaExp,
|
||||
},
|
||||
NombreRazonEmisor: data.EmisorNombre,
|
||||
TipoFactura: data.TipoFactura,
|
||||
DescripcionOperacion: data.Descripcion,
|
||||
Destinatarios: destinatarios,
|
||||
Desglose: Desglose{
|
||||
DetalleDesglose: ivaList,
|
||||
},
|
||||
CuotaTotal: fmt.Sprintf("%.2f", data.CuotaTotal),
|
||||
ImporteTotal: fmt.Sprintf("%.2f", data.ImporteTotal),
|
||||
Encadenamiento: enc,
|
||||
SistemaInformatico: SistemaInformatico{
|
||||
NombreRazon: data.Sistema.Nombre,
|
||||
NIF: data.Sistema.NIFProveedor,
|
||||
NombreSistemaInformatico: data.Sistema.NombreSistema,
|
||||
IdSistemaInformatico: "01",
|
||||
Version: data.Sistema.Version,
|
||||
NumeroInstalacion: data.Sistema.NumeroInstalacion,
|
||||
TipoUsoPosibleSoloVerifactu: data.Sistema.TipoUsoVerifactu,
|
||||
TipoUsoPosibleMultiOT: "N",
|
||||
IndicadorMultiplesOT: "N",
|
||||
},
|
||||
FechaHoraHusoGenRegistro: fechaGen,
|
||||
TipoHuella: "01",
|
||||
Huella: data.Huella,
|
||||
},
|
||||
NombreRazonEmisor: data.EmisorNombre,
|
||||
TipoFactura: data.TipoFactura,
|
||||
DescripcionOperacion: data.Descripcion,
|
||||
Desglose: Desglose{
|
||||
DetalleDesglose: ivaList,
|
||||
},
|
||||
CuotaTotal: fmt.Sprintf("%.2f", data.CuotaTotal),
|
||||
ImporteTotal: fmt.Sprintf("%.2f", data.ImporteTotal),
|
||||
Encadenamiento: enc,
|
||||
SistemaInformatico: SistemaInformatico{
|
||||
NombreRazon: data.Sistema.Nombre,
|
||||
NIF: data.Sistema.NIFProveedor,
|
||||
NombreSistemaInformatico: data.Sistema.NombreSistema,
|
||||
IdSistemaInformatico: "1",
|
||||
Version: data.Sistema.Version,
|
||||
NumeroInstalacion: data.Sistema.NumeroInstalacion,
|
||||
TipoUsoPosibleSoloVerifactu: data.Sistema.TipoUsoVerifactu,
|
||||
TipoUsoPosibleMultiOT: "N",
|
||||
IndicadorMultiplesOT: "N",
|
||||
},
|
||||
FechaHoraHusoGenRegistro: fechaGen,
|
||||
TipoHuella: "SHA-256",
|
||||
Huella: data.Huella,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -239,18 +270,17 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
|
|||
|
||||
func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
|
||||
fechaExp := data.FechaExpedicion.Format("02-01-2006")
|
||||
fechaGen := data.FechaGen.Format("02-01-2006T15:04:05")
|
||||
fechaGen := data.FechaGen.Format("2006-01-02T15:04:05-07:00")
|
||||
|
||||
var enc Encadenamiento
|
||||
if data.PrevHash == "" {
|
||||
enc = Encadenamiento{PrimerRegistro: "S"}
|
||||
} else {
|
||||
enc = Encadenamiento{
|
||||
PrimerRegistro: "N",
|
||||
RegistroAnterior: &RegistroAnterior{
|
||||
IDEmisorFactura: data.EmisorNIF,
|
||||
NumSerieFactura: data.NumSerie,
|
||||
FechaExpedicionFactura: fechaExp,
|
||||
NumSerieFactura: data.PrevNumSerie,
|
||||
FechaExpedicionFactura: data.PrevFecha.Format("02-01-2006"),
|
||||
Huella: data.PrevHash,
|
||||
},
|
||||
}
|
||||
|
|
@ -263,7 +293,7 @@ func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
|
|||
NIF: data.EmisorNIF,
|
||||
},
|
||||
},
|
||||
RegistroAnulacion: RegistroAnulacion{
|
||||
RegistroFactura: AnulacionRegistroFactura{RegistroAnulacion: RegistroAnulacion{
|
||||
IDVersion: "1.0",
|
||||
IDFacturaAnulada: IDFacturaAnulada{
|
||||
IDEmisorFacturaAnulada: data.EmisorNIF,
|
||||
|
|
@ -275,7 +305,7 @@ func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
|
|||
NombreRazon: data.Sistema.Nombre,
|
||||
NIF: data.Sistema.NIFProveedor,
|
||||
NombreSistemaInformatico: data.Sistema.NombreSistema,
|
||||
IdSistemaInformatico: "1",
|
||||
IdSistemaInformatico: "01",
|
||||
Version: data.Sistema.Version,
|
||||
NumeroInstalacion: data.Sistema.NumeroInstalacion,
|
||||
TipoUsoPosibleSoloVerifactu: data.Sistema.TipoUsoVerifactu,
|
||||
|
|
@ -283,9 +313,9 @@ func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
|
|||
IndicadorMultiplesOT: "N",
|
||||
},
|
||||
FechaHoraHusoGenRegistro: fechaGen,
|
||||
TipoHuella: "SHA-256",
|
||||
TipoHuella: "01",
|
||||
Huella: data.Huella,
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
return req
|
||||
|
|
|
|||
Loading…
Reference in New Issue