Saltar a contenido

Apéndice P — Notas de crédito automáticas (E34)

Cuando se anula factura ya emitida (e-CF aceptado), NO se borra: se emite Nota de Crédito E34 que la referencia.

P.1. Modelo

class SaleInvoice(models.Model):
    nota_credito_de = models.ForeignKey(
        "self", on_delete=models.PROTECT, null=True, blank=True,
        related_name="notas_credito",
    )
    motivo_anulacion = models.CharField(
        max_length=20,
        choices=[
            ("DEVOLUCION_TOTAL", "Devolución total"),
            ("DEVOLUCION_PARCIAL", "Devolución parcial"),
            ("ERROR_FACTURACION", "Error de facturación"),
            ("ANULACION_OPERACION", "Anulación de operación"),
            ("DESCUENTO_POSTERIOR", "Descuento posterior"),
            ("OMISION_OPERACION", "Omisión de operación"),
        ],
        blank=True, null=True,
    )

P.2. Servicio de generación

@transaction.atomic
def crear_nota_credito(factura_original_pk, motivo, items_a_devolver=None, usuario=None):
    original = SaleInvoice.objects.select_for_update().get(pk=factura_original_pk)
    if original.ecf_estado not in ("ACEPTADO", "ACEPTADO_CONDICIONAL"):
        raise ValueError(f"Solo NC sobre facturas aceptadas")
    if original.ecf_tipo not in ("E31", "E32", "E45", "E46"):
        raise ValueError(f"Tipo {original.ecf_tipo} no admite NC directa")

    nc = SaleInvoice.objects.create(
        ecf_tipo="E34",
        ecf_ambiente=original.ecf_ambiente,
        ecf_estado="BORRADOR",
        cliente=original.cliente,
        nota_credito_de=original,
        motivo_anulacion=motivo,
        subtotal=-original.subtotal,
        itbis=-original.itbis,
        total=-original.total,
    )
    # Copiar items con cantidades negativas
    for item in original.items.all():
        SaleInvoiceItem.objects.create(
            invoice=nc,
            producto=item.producto,
            cantidad=-item.cantidad,
            precio_unitario=item.precio_unitario,
            subtotal=-item.subtotal,
        )

    asignar_encf(nc, nc.ecf_ambiente)
    if motivo in ("DEVOLUCION_TOTAL", "ANULACION_OPERACION"):
        original.ecf_estado = "ANULADO"
        original.save(update_fields=["ecf_estado"])

    AccountingAuditLog.objects.create(
        usuario=usuario, accion="EMITIR_NC", modelo="SaleInvoice",
        objeto_id=nc.pk,
        detalles={"factura_original": original.ecf_encf, "motivo": motivo},
    )
    return nc

P.3. Plazos DGII

Plazo Acción
0-24h Receptor puede rechazar ACR → emisor obligado a E34
1-30 días E34 sin restricciones
31-90 días E34 permitido, receptor debe re-aprobar
>90 días E34 requiere justificación ante DGII
>1 año No permitido — facturas cierran al cierre fiscal

Buena práctica: bloquear botón de anulación >90 días salvo rol ADMIN. Registrar siempre en AccountingAuditLog.


FIN DEL MANUAL — v1.0 · 2026-05-14