FacturaService es la capa central de negocio de LEFA que centraliza todas las operaciones transaccionales sobre facturas. Cada método público es estático y opera sobre la base de datos SQLite a través de SQLAlchemy, garantizando integridad en cada escritura mediante contextos de sesión (session_scope y session_scope_immediate). El servicio también coordina la asignación de numeración correlativa (delegada en NumeracionService), la creación de registros VeriFactu encadenados y el guardado de las preferencias del último cliente utilizado.
DTOs
Los objetos de transferencia de datos se definen comodataclass de Python y se utilizan para comunicar la capa de interfaz de usuario con FacturaService sin exponer directamente los modelos ORM.
LineaFacturaDTO
Representa una línea individual de una factura en tránsito desde la UI.
str
required
Texto descriptivo del concepto o servicio facturado.
float
required
Número de unidades del concepto.
float
required
Precio por unidad en euros (sin impuestos).
TotalesFactura
Resultado inmutable del cálculo de importes, devuelto por calcular_totales.
float
Suma de
cantidad × precio_unitario para todas las líneas, redondeada a 2 decimales.float
Importe de IVA calculado sobre el subtotal.
float
Retención de IRPF calculada sobre el subtotal.
float
Importe neto:
subtotal + iva − irpf.ResumenHoy
Tres cifras para el panel de inicio de LEFA, calculadas en tiempo real sin analítica adicional.
int
Número de facturas con
fecha_emision igual a hoy y estado distinto de BORRADOR.int
Número de facturas en estado
EMITIDA (enviadas pero aún no cobradas).float
Suma de los totales de todas las facturas en estado
EMITIDA.Métodos
calcular_totales
subtotal × (porcentaje / 100) para IVA e IRPF y redondea cada resultado a dos decimales.
list[LineaFacturaDTO]
required
Lista de líneas de factura con descripción, cantidad y precio unitario.
float
required
Porcentaje de IVA a aplicar (p. ej.
21.0).float
required
Porcentaje de retención de IRPF a aplicar (p. ej.
15.0).TotalesFactura con los cuatro importes calculados.
listar_todas
id descendente. Las relaciones cliente y lineas se cargan con joinedload para evitar consultas N+1 en la UI.
Retorna: Lista de objetos Factura desvinculados de la sesión (expunged).
resumen_hoy
ResumenHoy con los tres campos poblados.
listar_por_cliente
id descendente.
int
required
Identificador del cliente en la base de datos.
Factura en estado EMITIDA o COBRADA.
total_facturado_cliente
listar_por_cliente internamente.
int
required
Identificador del cliente.
float con el total acumulado en euros, redondeado a 2 decimales.
obtener_por_id
cliente, lineas y factura_rectificada precargadas.
int
required
Identificador de la factura.
Factura desvinculada de la sesión, o None si no existe.
guardar_borrador
BORRADOR o actualiza un borrador existente. En ningún caso se asigna número oficial (numero_factura permanece NULL hasta la emisión). Si se proporciona factura_rectificada_id, la serie se fuerza automáticamente a "RECT".
int
required
Cliente al que se emitirá la factura.
list[LineaFacturaDTO]
required
Líneas de la factura. Debe contener al menos un elemento.
float
required
Porcentaje de IVA (p. ej.
21.0).float
required
Porcentaje de retención de IRPF (p. ej.
15.0).int | None
Si se indica, actualiza el borrador existente con ese ID en lugar de crear uno nuevo.
str | None
Prefijo de serie (p. ej.
"FACT", "CLI"). Si es None, se lee de las preferencias del emisor.date | None
Fecha límite de pago. Si es
None, se calculará automáticamente en la emisión según dias_vencimiento de las preferencias.int | None
ID de la factura original que rectifica este borrador. Activa la serie
"RECT" automáticamente.int | None
ID del presupuesto del que proviene esta factura (trazabilidad bidireccional).
Factura en estado BORRADOR desvinculada de la sesión.
Lanza ValueError si:
lineasestá vacía.- Se proporciona
factura_idpero la factura no existe. - La factura encontrada con
factura_idno está en estadoBORRADOR.
emitir_factura
BEGIN IMMEDIATE (SQLite). El flujo interno es:
- Valida que la factura sea un borrador con al menos una línea.
- Obtiene el siguiente correlativo con
NumeracionService.siguiente_secuencia. - Formatea el número oficial (p. ej.
FACT-2026-0044). - Fija
fecha_emisiona hoy y calculafecha_vencimientosi no fue establecida. - Genera el registro VeriFactu encadenado (hash SHA-256) dentro de la misma transacción.
- Hace commit y, tras él, persiste el JSON VeriFactu en disco.
int
required
ID de la factura borrador a emitir.
Factura en estado EMITIDA con número oficial asignado.
Lanza ValueError si:
- La factura no existe.
- No está en estado
BORRADOR. - No tiene líneas.
marcar_enviada
enviada, guarda la fecha actual en fecha_envio y almacena la dirección del destinatario.
int
required
ID de la factura enviada.
str
required
Dirección de correo electrónico del destinatario (se recortan espacios en blanco).
Factura actualizada.
Lanza ValueError si: la factura no existe.
duplicar_factura
int
required
ID de la factura a duplicar. Debe estar en estado
EMITIDA o COBRADA.Factura en estado BORRADOR.
Lanza ValueError si:
- La factura no existe.
- La factura está en estado
BORRADOR(solo se duplican facturas ya emitidas).
rectificar_factura
RECT) vinculado a la factura original mediante factura_rectificada_id. Copia las líneas tal cual; el usuario puede modificar importes (p. ej. negativos para devoluciones) antes de emitir.
int
required
ID de la factura a rectificar. Debe estar en estado
EMITIDA o COBRADA y no ser ella misma rectificativa.Factura en estado BORRADOR con serie RECT.
Lanza ValueError si:
- La factura no existe.
- No está en estado
EMITIDAniCOBRADA. - La factura ya es rectificativa (
es_rectificativa == True).
marcar_cobrada
EMITIDA a COBRADA. No admite transiciones desde otros estados.
int
required
ID de la factura a marcar como cobrada.
Factura en estado COBRADA.
Lanza ValueError si:
- La factura no existe.
- No está en estado
EMITIDA.
Ejemplo de uso
El siguiente fragmento muestra el flujo completo desde la creación de un borrador hasta su cobro:emitir_factura utiliza BEGIN IMMEDIATE para garantizar que dos emisiones concurrentes nunca compartan el mismo número correlativo, incluso en entornos con varios procesos que accedan a la misma base de datos SQLite.