Metadata-Version: 2.5
Name: folyo
Version: 0.2.0
Summary: SDK oficial de Python para la API de Folyo (facturacion electronica chilena, DTE + SII).
Project-URL: Homepage, https://folyo.cl
Project-URL: Documentation, https://docs.folyo.cl/sdks
Author-email: Folyo <soporte@folyo.cl>
License: MIT
License-File: LICENSE
Keywords: boleta,chile,dte,factura-electronica,facturacion,folyo,sii
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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: Topic :: Office/Business :: Financial :: Accounting
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=5.1; extra == 'dev'
Description-Content-Type: text/markdown

# Folyo — SDK oficial de Python

SDK de Python para la API de [Folyo](https://folyo.cl): facturacion electronica
chilena (DTE + SII) standalone. Del codigo al SII, sin escalas.

- Cliente tipado sobre `httpx` (unica dependencia de runtime).
- Errores tipados con mapeo de los codigos del servidor.
- Soporte del patron de emision asincrona (encolar + polling con backoff).
- Idempotencia, override de ambiente del SII y reintentos seguros ante 429/503.
- Redaccion de secretos (la API key/JWT nunca aparecen en `repr` ni en errores).

## Instalacion

```bash
pip install folyo
```

Requiere Python 3.9 o superior.

## Autenticacion

El cliente exige exactamente uno de los dos esquemas:

- **API key** (recomendada para integraciones server-side, no expira). Se envia
  como header `X-API-Key`. Fija el tenant y la empresa.
- **JWT Bearer** (sesion de usuario). Se envia como `Authorization: Bearer ...`.

```python
from folyo import Folyo

# Con API key
folyo = Folyo(api_key="<tu-api-key>")

# O con un token JWT
folyo = Folyo(token="eyJhbGciOi...")
```

Tambien funciona como context manager (cierra el cliente HTTP al salir):

```python
with Folyo(api_key="<tu-api-key>") as folyo:
    ...
```

## Quickstart: emitir una Factura Electronica (DTE 33)

La emision es **asincrona**: `emitir()` encola y devuelve un `job_id`; el helper
`emitir_y_esperar()` hace el polling por ti hasta que el job termina.

```python
import uuid
from folyo import Folyo, DTERequest, Receptor, DetalleLinea, Referencia

with Folyo(api_key="<tu-api-key>") as folyo:
    req = DTERequest(
        tipo_dte=33,  # Factura Electronica
        receptor=Receptor(
            rut="12.345.678-9",
            razon_social="Cliente SpA",
            giro="Comercio",
        ),
        detalle=[
            DetalleLinea(nombre="Servicio de consultoria", monto=100000),
        ],
        # Referencias opcionales. tipo_doc_ref acepta codigos del SII (ej. 801 =
        # orden de compra) o un codigo de texto de hasta 3 caracteres acordado
        # con el receptor, como "HES" (Hoja de Entrada de Servicios). Para
        # facturar al MOP usa tipo_doc_ref "UDP" (equivale al 802), el codigo de
        # la unidad de pago que entrega el mandante en folio_ref y su
        # descripcion en razon_ref.
        referencia=[
            Referencia(tipo_doc_ref=801, folio_ref="OC-2026-1487"),
            Referencia(tipo_doc_ref="HES", folio_ref="4500123456"),
            Referencia(tipo_doc_ref="UDP", folio_ref="2020", razon_ref="NIVEL_CENTRAL_SUBSECRETARIA"),
        ],
    )

    # Encolar y esperar el resultado (con Idempotency-Key para reintentar seguro)
    job = folyo.dte.emitir_y_esperar(req, idempotency_key=str(uuid.uuid4()))

    if job.estado == "completed" and job.result is not None:
        print("Folio:", job.result.folio)
        print("Track ID:", job.result.track_id)
        print("Total:", job.result.monto_total)

        # Descargar el PDF
        pdf = folyo.dte.descargar_pdf(33, job.result.folio)
        with open(f"factura-{job.result.folio}.pdf", "wb") as f:
            f.write(pdf)
    else:
        print("La emision fallo:", job.estado)
```

> **Nota:** `completed` significa que el SII **recibio** el documento (tienes
> folio y track_id). El veredicto (aceptacion o rechazo) llega despues: el
> documento queda en estado `enviado` y Folyo lo consulta automaticamente hasta
> resolverlo. El SII puede demorar desde minutos hasta mas de una hora; no
> reemitas por eso — suscribete al webhook de cambio de estado.

Si prefieres resolver el resultado por webhook o SSE, usa `emitir()` directo:

```python
encolada = folyo.dte.emitir(req)
print(encolada.job_id)  # resuelve luego via webhook dte.emitido o polling
```

## Impuestos adicionales y retenciones por linea

Una linea puede llevar `impuestos` con el codigo del impuesto adicional del
DL 825 (art. 42). Tu mandas el codigo; Folyo resuelve la tasa desde su catalogo,
calcula el monto sobre la misma base que el IVA y lo desglosa en el documento.

```python
import uuid
from folyo import Folyo, DTERequest, Receptor, DetalleLinea, ImpuestoLinea

with Folyo(api_key="<tu-api-key>") as folyo:
    req = DTERequest(
        tipo_dte=33,
        receptor=Receptor(rut="12.345.678-9", razon_social="Distribuidora SpA", giro="Comercio"),
        detalle=[
            DetalleLinea(
                nombre="Cerveza lager 330 ml",
                cantidad=120,
                precio=12500,
                monto=1500000,
                impuestos=[ImpuestoLinea(codigo="26")],  # cervezas: 20,5%
            ),
        ],
    )
    job = folyo.dte.emitir_y_esperar(req, idempotency_key=str(uuid.uuid4()))
    # Neto 1.500.000 + IVA 285.000 + impuesto 307.500 = total 2.092.500
    if job.result is not None:
        for imp in job.result.impuestos:
            print(imp.codigo, imp.glosa, imp.tasa, imp.monto, imp.retencion)
            # 26 Cervezas y otras bebidas alcoholicas 20.5 307500 False
```

| Codigo | Impuesto | Tasa |
|---|---|---|
| `"24"` | Licores, piscos y whisky (incluye aguardientes y vinos licorosos) | 31,5% |
| `"25"` | Vinos | 20,5% |
| `"26"` | Cervezas y otras bebidas alcoholicas | 20,5% |
| `"27"` | Bebidas analcoholicas y minerales | 10% |
| `"271"` | Bebidas analcoholicas con azucar elevada | 18% |
| `"15"` | IVA retenido total generico por cambio de sujeto | 19% |
| `"30/32/33/34/36/37/48"` | Retencion parcial: legumbres, ganado, madera, trigo, arroz, hidrobiologicos y frambuesas | 10/8/8/4/10/10/14% |
| `"31/38/39/41/47"` | Retencion total: silvestres, chatarra, PPA, construccion y cartones | 19% IVA |
| `"23/44/45"` | Suntuarios | 15/15/50% |
| `"28/35/51/52"` | Especificos de diesel, gasolina y gases | monto fijo, sin tasa |
| `"17"` | IVA anticipado de faenamiento | 5% de la base especial `faenamiento` |
| `"18/19"` | IVA anticipado de carne y harina | 5/12% |
| `"46"` | IVA retenido oro | 100% del IVA, solo 33 y NC/ND 56/61 referenciarias |

La constante `folyo.IMPUESTOS_ADICIONALES` conserva el mapa de las bebidas y
`folyo.IMPUESTOS_DTE` expone todos los codigos habilitados; sus valores `None`
identifican los impuestos de monto fijo.

Los codigos se validan por tipo de DTE. Las retenciones parciales van en una
factura de compra `46` o en su NC/ND `56`/`61` de referencia. Para que una de
ellas retenga el IVA completo, envia el codigo base y `ndf=True`: Folyo usa la
variante total (`30→301`, `32→321`, `33→331`, `34→341`, `36→361`, `37→371`,
`48→481`) y omite `iva_no_retenido`. La API no consulta nominas SII; quien emite
debe cumplir la calidad de agente retenedor que exija el regimen.

Envia normalmente el codigo parcial con `ndf=True`, sin `tasa` ni `monto`:
Folyo lo normaliza a la variante total. Tambien puede enviarse la variante total
canonica directamente con `ndf=True`; solo para revalidacion admite `tasa=19`
y un `monto` que cuadre exactamente con la base del codigo.

Una linea exenta no puede llevar impuesto. Con impuestos o retenciones, un
descuento/recargo global solo puede ser porcentual: se prorratea en cada base
antes de calcular los montos. Un descuento global en pesos responde
`400 INVALID_REQUEST` antes de reservar folio. El tipo `43` solo admite
adicionales, nunca retenciones ni `iva_no_retenido`. Los codigos `17` y `46`
siguen cerrados. Mientras una familia no este validada en certificacion para tu
empresa, produccion responde `422 IMPUESTO_PENDIENTE_CERT`.

El codigo `15` conserva su compatibilidad historica sin descuento ni recargo
global. Si se combina con uno porcentual, produccion exige que el codigo `15`
este habilitado explicitamente; de lo contrario responde
`422 IMPUESTO_PENDIENTE_CERT`.

El codigo `17` exige `faenamiento` en cada linea afecta: `codigo_cpcs` (`1701`
a `1706`), `cantidad_cabezas` positiva y `monto_base_faena` positivo. La linea
usa cantidades de hasta 6 decimales y se normaliza a micro-unidades cuyo valor
escalado no excede `9007199254740991` (maximo nominal
`9007199254.740991`); la base y su suma no superan `9007199254740991`.
La linea usa `cantidad` en `KG`; Folyo deriva CPCS, Retenedor, QtyRef `UN`, el impuesto
17 y `monto_base` del resultado. Solo aplica a 33 y a 56/61 que referencian una
33 compatible; no admite descuentos o recargos globales ni mezcla con otros
impuestos. El emisor debe contar previamente con acreditacion operativa como
agente retenedor: Folyo no hace lookup automatico ni afirma una aprobacion
digital persistida; produccion se habilita despues de certificacion. El codigo
`46` es una retencion total de oro para 33 y sus 56/61
referenciarias; no se usa en la Factura de Compra DTE 46.

`tasa` y `monto` son opcionales y normalmente no se envian: el catalogo de
Folyo resuelve la tasa vigente. Si entregas una `tasa`, debe ser finita, estar
entre 0,01% y 100% y tener como maximo dos decimales; debes enviarla con el
mismo valor en todas las lineas que llevan ese codigo, o en ninguna. Si
entregas `monto`, debes entregarlo en todas esas lineas y la suma debe ser
exactamente `round(base_del_codigo * tasa / 100)`: no se acepta redondear cada
linea por separado. `monto` es obligatorio para los especificos fijos `28`,
`35`, `51` y `52`, que no llevan `tasa`.

Para la retencion total del IVA en una factura de compra (`46`), la forma
recomendada es `impuestos=[ImpuestoLinea(codigo="15")]` en cada linea afecta,
sin `tasa` ni `monto`: el codigo 15 siempre retiene el IVA total al 19% y
produce el mismo XML que `retencion_iva_total=True`, que queda obsoleto pero
sigue funcionando.

Para los servicios agricolas de la Res. Ex. SII 83/2026, el uso del codigo `15`
es una inferencia pendiente de confirmacion tributaria y certificacion. La
capacidad tecnica no acredita que el emisor cumpla los requisitos de ese
regimen; la API no consulta ni acredita esa condicion ante el SII.

El desglose tambien vuelve en `impuestos` del listado de documentos, junto con
`monto_neto`, `monto_exento`, `monto_iva` e `iva_no_retenido` cuando la
retencion fue parcial:

```python
for doc in folyo.dte.listar_documentos(limite=1):
    for imp in doc.impuestos:
        print(imp.codigo, imp.glosa, imp.tasa, imp.monto)
```

## Manejo de errores

```python
from folyo import (
    Folyo,
    FolyoAuthError,
    FolyoQuotaError,
    FolyoRateLimitError,
    FolyoValidationError,
    FolyoSiiUnavailableError,
)

try:
    folyo.dte.emitir(req)
except FolyoRateLimitError as e:
    print("Reintentar en", e.retry_after, "segundos")
except FolyoQuotaError as e:
    print("Limite de plan o pago requerido:", e.code)
except FolyoAuthError:
    print("Credenciales invalidas o sin permisos")
except FolyoValidationError as e:
    print("Datos invalidos:", e.message)
except FolyoSiiUnavailableError:
    print("El SII no esta disponible, reintenta mas tarde")
```

El SDK reintenta automaticamente (con backoff exponencial y respetando
`Retry-After`) ante `429` y `503` en operaciones seguras: peticiones `GET` y
emisiones con `Idempotency-Key`.

## Recursos disponibles

| Recurso | Metodos principales |
|---|---|
| `folyo.dte` | `emitir`, `get_emision`, `emitir_y_esperar`, `listar_documentos`, `descargar_xml`, `descargar_pdf`, `regenerar_pdf`, `consultar_estado`, `consultar_envio`, `emitidos`, `recibidos`, `contribuyente`, `situacion_tributaria`, `enviar_rcof`, `resumen_rcof` |
| `folyo.folios` | `info`, `solicitar` |
| `folyo.rcv` | `periodos`, `periodo`, `sync`, `resumen_iva` |
| `folyo.clientes` | `listar`, `upsert`, `importar`, `buscar`, `actualizar`, `eliminar` |
| `folyo.empresa` | `listar`, `seleccionar` |
| `folyo.acuse` | `registrar`, `pendientes`, `estado` |
| `folyo.webhooks` | `listar`, `crear`, `actualizar`, `eliminar` |
| `folyo.api_keys` | `listar`, `crear`, `eliminar` |

Algunos endpoints (clientes, RCV, listado de documentos) requieren "panel
operativo" y responden `403` en planes solo-API.

## Seguridad

- La API key y el JWT nunca se incluyen en `repr(cliente)` ni en los errores.
- Los errores exponen solo el mensaje sanitizado del servidor, el codigo
  estable, el status HTTP y el `request_id`: nunca el cuerpo de la respuesta
  (que puede traer datos sensibles como el XML firmado o secrets de webhook).
- El SDK no escribe logs por defecto.

## Licencia

MIT. Ver [LICENSE](./LICENSE).
