Metadata-Version: 2.5
Name: smartdocpy
Version: 0.2.0
Summary: SDK de Python para SmartDoc — facturación electrónica en Paraguay (DNIT/SIFEN)
Project-URL: Homepage, https://www.smartdoc.com.py/
Project-URL: API de SmartDoc, https://api.smartdoc.araitek.com/api/v2/docs/
License-Expression: MIT
Keywords: dnit,factura-electronica,facturacion-electronica,paraguay,set,sifen,smartdoc
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Spanish
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocs>=1.6; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
Description-Content-Type: text/markdown

# smartdoc

SDK de Python para [SmartDoc](https://smartdoc.com.py), el sistema de facturación
electrónica para Paraguay. Cubre la API pública v2 y la recepción de sus webhooks.

## Instalación

```bash
pip install smartdocpy
```

El paquete se instala como `smartdocpy` y se importa como `smartdoc`.

Requiere Python 3.10 o superior. La única dependencia es `httpx`.

## Emitir un documento

```python
import smartdoc

sd = smartdoc.Client()          # toma la API Key del entorno

factura = sd.invoices.create(
    recipient=smartdoc.Recipient.entity(
        ruc="80012345-1",
        social_name="Cliente Ejemplo S.A.",
        email="facturas@cliente.com.py",
    ),
    items=[
        smartdoc.Item("Consultoría de octubre", quantity=1, unit_amount=1100000),
    ],
)

factura = sd.invoices.wait_until_final(factura.id)
print(factura.status, factura.cdc)
```

El SDK completa los datos del emisor, la clave de idempotencia y el desglose de
IVA. En Paraguay el IVA va **incluido** en el precio: sobre ese ejemplo emite
1.000.000 de base gravada y 100.000 de IVA.

Están los seis tipos de documento electrónico, cada uno con sus acciones:

| Recurso | Documento |
|---|---|
| `sd.invoices` | Factura |
| `sd.receipts` | Recibo |
| `sd.credit_notes` | Nota de crédito |
| `sd.debit_notes` | Nota de débito |
| `sd.remission_notes` | Nota de remisión |
| `sd.auto_invoices` | Autofactura |

Hay un cliente asíncrono con la misma superficie:

```python
sd = smartdoc.AsyncClient()
factura = await sd.invoices.create(recipient=..., items=[...])
```

## Configuración

La API Key sale de `SMARTDOC_API_KEY`. La URL apunta a la instancia de
producción; solo hace falta `SMARTDOC_BASE_URL` con una instancia propia.

```bash
export SMARTDOC_API_KEY="pk_..."
```

Con más de un establecimiento o punto de expedición hay que decir cuál, porque el
SDK no elige por su cuenta:

```python
sd = smartdoc.Client(establishment="001", dispatch_point="001")
```

o por llamada, si emitís desde varias sucursales:

```python
sd.invoices.create(..., establishment="002", dispatch_point="001")
```

## Idempotencia

Cada `create()` va con una clave nueva que genera el SDK, así que reintentar
nunca emite dos veces. Si preferís que la clave venga de tu sistema:

```python
factura = sd.invoices.create(..., idempotency_key=f"venta-{venta.id}")
```

Repetir la llamada con la misma clave devuelve el documento ya creado.

## Errores

```python
try:
    sd.invoices.create(recipient=..., items=[...])
except smartdoc.ValidationError as e:
    ...     # datos mal armados; no sirve reintentar
except (smartdoc.RateLimitError, smartdoc.ServerError) as e:
    ...     # transitorio; el SDK ya reintentó
```

Todos heredan de `SmartDocError`. Las validaciones que el SDK puede hacer sin red
—largos, catálogos cerrados, campos condicionales— fallan antes de salir, con el
mensaje de qué corregir.

## Webhooks

En producción conviene escuchar los eventos en vez de hacer polling.

```python
webhooks = smartdoc.Webhooks(secret=os.environ["SMARTDOC_WEBHOOK_SECRET"])

@webhooks.on("invoice.approved")
def factura_aprobada(evento):
    marcar_aprobada(evento.entity_id, evento.cdc)

@webhooks.on_any_error()
def algo_falló(evento):
    avisar(f"{evento.event_type}: {evento.error_code} {evento.error_message}")
```

El SDK verifica la firma HMAC, descarta las entregas repetidas y trae adaptadores
para los tres frameworks —`webhooks.fastapi_route()`, `webhooks.flask_view()`,
`webhooks.django_view()`— más un `webhooks.serve()` para desarrollo. No hace falta
tener ninguno de los tres instalado.

Para identificar el documento va el par `(entity_type, entity_id)`, porque el id
se numera por tipo: el recibo 20 y la nota de débito 20 existen a la vez.

```python
entidad, numero = evento.document_key    # ("receipt", 20)
```

## Catálogos

Los catálogos de SIFEN, centralizados y con validación local.

```python
import smartdoc

smartdoc.Iva.TEN                    # "10_percent", etiqueta "10%"
smartdoc.SaleType.CASH              # "cash"
smartdoc.MeasureUnit.UNIT           # 77
smartdoc.TransactionType.SERVICES   # 2

smartdoc.Iva.TEN.rate               # 0.10
smartdoc.SaleType.labels()          # {"cash": "Contado", "credit": "Crédito"}
```

Los catálogos numéricos de SIFEN son **abiertos**: aceptan cualquier código del
catálogo oficial de la DNIT aunque el SDK no lo liste, así que un código nuevo no
obliga a esperar una versión del paquete.

```python
smartdoc.TransactionType.coerce(99)   # pasa
```

Los catálogos propios de SmartDoc son **cerrados**, y un valor inválido falla
antes de salir a la red, con las opciones disponibles:

```python
smartdoc.SaleType.coerce("Contado")
# ValidationError: 'Contado' no es un valor válido de SaleType.
#                  ¿Quisiste decir 'cash'? Valores válidos: 'cash', 'credit'.
```

## Geografía

Todo documento cuyo receptor lleve dirección necesita seis campos —departamento,
distrito y ciudad, cada uno con su código y su descripción—. El SDK embebe el
catálogo, así que una búsqueda por nombre los resuelve.

```python
ciudad = smartdoc.geo.find_city("Asunción")

ciudad.code                 # 1
ciudad.district.name        # "ASUNCION (DISTRITO)"
ciudad.department.name      # "CAPITAL"
```

Cuando el nombre es ambiguo no se elige uno: la ciudad queda impresa en el
documento, así que el SDK lista las opciones y pide desempatar.

```python
smartdoc.geo.find_city("Concepción")
# ValidationError: 'Concepción' es ambiguo: hay 3 ciudades con ese nombre...

smartdoc.geo.find_city("Concepción", department="Misiones")   # listo
```

## Estados

Los seis tipos de documento comparten el mismo vocabulario de estados, pero no
todos alcanzan los mismos.

```python
smartdoc.is_terminal("recoverable_error")                          # False
smartdoc.is_terminal("generated", smartdoc.DocumentType.RECEIPT)   # True
smartdoc.is_terminal("generated", smartdoc.DocumentType.INVOICE)   # False
```

El recibo no se envía a la DNIT, así que nunca llega a `approved_by_set`: su
estado final de éxito es `generated`, que para los demás documentos es un paso
intermedio.

Emitir es asíncrono: `create()` vuelve enseguida y la aprobación llega después.
`wait_until_final` consulta hasta que el documento llegue a un estado final, con
un plazo holgado por omisión porque cada salto lo da una pasada del planificador
del servidor. Las acciones —`cancel`, `resend_email`, `resend_to_set`,
`nominate`— tampoco devuelven el documento, porque la API responde
identificadores: hay que releerlo.

## Documentación

La documentación completa está en [`docs/`](docs/): primeros pasos, un tutorial
de integración de punta a punta, la guía de cada tipo de documento, webhooks,
manejo de errores y la referencia de la API.

```bash
pip install -e ".[docs]"
mkdocs serve
```

## Desarrollo

```bash
pip install -e ".[dev]"
pytest
```

## Licencia

MIT
