Documentos¶
Los seis tipos comparten la misma forma. Lo que cambia es qué campos piden y qué acciones aceptan.
| Recurso | Documento | KuDE | Acciones propias |
|---|---|---|---|
sd.invoices |
Factura | ✓ | nominate, confirm_draft, discard_draft |
sd.receipts |
Recibo | ✓ | confirm_draft, discard_draft |
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 | — | — |
Todos aceptan cancel y resend_email. Todos menos el recibo aceptan además
resend_to_set, que reenvía a la DNIT un documento que quedó en error.
Establecimiento y punto de expedición¶
Se pueden dar por su código —el que va impreso en el documento— o por el id interno de SmartDoc:
smartdoc.Client(establishment="001", dispatch_point="001") # por código
smartdoc.Client(establishment=8, dispatch_point=14) # por id interno
También por llamada, que es lo que corresponde si emitís desde varias sucursales:
Si no se indica ninguno, el SDK los descubre solo cuando el contribuyente tiene
exactamente uno. Si hay más, levanta ConfigurationError con las opciones
disponibles en vez de elegir: el código elegido queda impreso en el documento y
en su CDC, y equivocarlo obliga a anular y reemitir.
Crear y editar¶
create() emite; update() reemplaza. No es un parche: hay que pasar el
documento entero, porque la API valida el cuerpo completo igual que al crear.
factura = sd.invoices.create(recipient=..., items=[...])
factura = sd.invoices.update(factura.id, recipient=..., items=[...])
Solo antes de subir a la DNIT
Una vez que el documento pasó a uploaded_to_set, la API rechaza la
edición. A partir de ahí la vía es anular y reemitir.
Ítems y montos¶
smartdoc.Item(
"Consultoría",
quantity=2,
unit_amount=550000, # con el IVA ya incluido
iva=smartdoc.Iva.TEN, # por omisión
code="SKU-123", # si no se pasa, se numera por posición
measure_unit=smartdoc.MeasureUnit.UNIT,
discount=10000,
)
El descuento es por unidad, no de la línea
Un discount de 10.000 sobre 3 unidades descuenta 30.000 en total.
El SDK calcula amount y el desglose completo de IVA, contemplando el descuento
por ítem, el global y el de SEDECO. Los montos se calculan con Decimal.
smartdoc.builders.totals_for([smartdoc.Item("X", quantity=1, unit_amount=1100000)])
# {"amount": 1100000, "amount10Percent": 1100000,
# "taxed10Percent": 1000000, "iva10Percent": 100000}
IVA mixto¶
Cuando parte de la línea está gravada y parte exenta:
smartdoc.Item(
"Combo", quantity=1, unit_amount=1000000,
iva=smartdoc.Iva.MIXED_TEN,
iva_base=60, # 60% gravado al 10%, 40% exento
)
El receptor¶
smartdoc.Recipient.entity(ruc="80012345-1", social_name="Cliente S.A.")
smartdoc.Recipient.person(ruc="5000001-1", name="Juan Pérez")
smartdoc.Recipient.not_nominated()
smartdoc.Recipient.foreign(name="Cliente do Brasil", country_code="BRA")
smartdoc.Recipient.government(ruc="...", social_name="...")
smartdoc.Recipient.diplomatic(ruc="...", social_name="...")
Cada constructor se expande a los campos client* que corresponden, con sus
reglas condicionales.
Direcciones¶
Un documento cuyo receptor lleva dirección necesita seis campos: departamento, distrito y ciudad, cada uno con código y descripción. El SDK embebe el catálogo geográfico, así que una búsqueda por nombre los resuelve:
smartdoc.Recipient.person(
ruc="5000001-1",
name="Juan Pérez",
address="Av. España 1234",
house_number=1234,
city=smartdoc.geo.find_city("Asunción"),
)
Cuando el nombre es ambiguo, find_city levanta ValidationError con las
opciones en vez de elegir una:
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") # desempata
smartdoc.geo.city(2252) # o por código
El receptor extranjero es el único que puede llevar dirección sin códigos geográficos paraguayos.
Estados¶
Los seis tipos comparten el mismo vocabulario. El flujo habitual:
factura.status # DocumentStatus.APPROVED_BY_SET
factura.is_approved
factura.is_terminal
factura.is_error
factura.error_code # None si el documento no está en error
factura.error_message
Preguntá por las propiedades, no por el estado
is_approved, is_terminal e is_error conocen las reglas de cada tipo de
documento. Comparar contra estados a mano es más frágil.
recoverable_error no es un estado final
SmartDoc lo reintenta por su cuenta, así que el documento todavía puede
llegar a aprobarse. wait_until_final no corta ahí.
Para el recibo, generated es el estado final de éxito
El recibo no se envía a la DNIT, así que nunca llega a approved_by_set.
is_approved y is_terminal ya lo contemplan.
Esperar¶
Consulta hasta llegar a un estado terminal, respetando el límite de tasa. Acepta
timeout y poll_interval; si se agota el plazo levanta TimeoutError, que no
significa que el documento haya fallado sino que se acabó la espera.
En producción conviene escuchar webhooks en lugar de esto.
Borradores¶
Facturas y recibos se pueden crear en borrador: quedan guardados y no se envían a la DNIT hasta confirmarlos.
borrador = sd.invoices.create(recipient=..., items=[...], draft=True)
sd.invoices.confirm_draft(borrador.id) # ahora sí sale
sd.invoices.discard_draft(borrador.id) # o se descarta
Acciones¶
sd.invoices.cancel(factura.id, reason="Error en el monto")
sd.invoices.resend_email(factura.id)
sd.invoices.resend_to_set(factura.id)
Estas acciones no devuelven el documento: se ejecutan de forma asíncrona, así que para ver cómo quedó hay que volver a consultarlo.
sd.invoices.cancel(factura.id, reason="Error en el monto")
factura = sd.invoices.wait_until_final(factura.id) # DocumentStatus.CANCELLED
Anular exige que el documento esté aprobado y que el motivo no vaya vacío. Si el
estado no lo permite, la API responde ACTION_NOT_ALLOWED.
Facturas innominadas¶
Venta de mostrador sin identificar al comprador:
Si después el cliente pide la factura a su nombre:
cliente = sd.clients.create(ruc="80012345-1", socialName="Cliente S.A.")
sd.invoices.nominate(factura.id, client_id=cliente.id)
Notas de crédito y débito¶
Corrigen una factura ya emitida. El documento asociado es obligatorio: sin él la DNIT no sabe sobre qué aplicar la corrección.
sd.credit_notes.create(
recipient=...,
items=[smartdoc.Item("Devolución", quantity=1, unit_amount=1100000)],
associated_document=smartdoc.AssociatedDocument.electronic(factura.cdc),
)
Para un documento anterior a la facturación electrónica:
smartdoc.AssociatedDocument.printed(
stamp_identifier="12345678",
establishment_code="001",
dispatch_point_code="001",
number="0000001",
document_date="2024-05-01",
)
Recibos¶
sd.receipts.create(
recipient=smartdoc.Recipient.person(ruc="5000001-1", name="Juan Pérez"),
amount=1100000,
payment_method=smartdoc.constants.ReceiptPaymentMethod.CASH,
associated_document=smartdoc.AssociatedDocument.electronic(factura.cdc),
)
Aceptan tres tipos de receptor: persona, entidad y extranjero. Sus ítems son más
simples —descripción y monto— porque el recibo no liquida impuestos. El documento
asociado es opcional: si no se pasa, va AssociatedDocument.none().
Notas de remisión¶
Amparan un traslado, así que no llevan monto, ni moneda, ni IVA. Lo que sí llevan, y ningún otro documento tiene, son los datos del traslado.
from datetime import date
ciudad = smartdoc.geo.find_city("Luque")
sd.remission_notes.create(
recipient=smartdoc.Recipient.entity(
ruc="80012345-1",
social_name="Cliente S.A.",
address="Av. Principal 123",
house_number=123,
city=ciudad,
email="cliente@ejemplo.com.py",
),
items=[smartdoc.RemissionItem("Caja de tornillos", quantity=10)],
departure=smartdoc.TransportEndpoint(
address="Depósito Central", city=ciudad, house_number=100
),
arrival=smartdoc.TransportEndpoint(
address="Sucursal Norte", city=ciudad, house_number=200
),
vehicle=smartdoc.Vehicle(
brand="Toyota", type="CAMION", registration_number="ABC123"
),
carrier=smartdoc.Carrier(
social_name="Transporte S.A.",
ruc="80099999-1",
is_taxpayer=True,
document_number="80099999",
address="Ruta 2 km 15",
driver_name="Pedro Gómez",
driver_document_number="3456789",
driver_address="Calle 5 casi 8",
),
estimated_kilometers=25,
start_date=date(2026, 8, 6),
end_date=date(2026, 8, 7),
)
La remisión exige más datos que el resto
Campos que en los otros documentos son opcionales y acá no:
- El receptor, con dirección completa —
address,house_numberycity— yemail. - Los dos extremos del traslado, con
house_number. estimated_kilometers,start_dateyend_date.- El transportista completo: su documento y dirección, más el nombre, documento y dirección del chofer.
El SDK valida todo esto antes de emitir e indica qué falta.
Los datos del vehículo tienen largo acotado
| Campo | Qué va |
|---|---|
type |
Descripción libre de 4 a 10 caracteres: "CAMION", "Camioneta", "Furgón" |
brand |
De 1 a 10 caracteres. "Mercedes-Benz" no entra: abreviala |
registration_number |
La matrícula, de 6 o 7 caracteres |
document_number |
El número de identificación, de 1 a 20 caracteres |
document_type decide cuál de los dos números hay que dar, y el otro se
ignora: con REGISTRATION —el valor por omisión— va registration_number;
con VIN, document_number.
type no es transport_type: ese es un código y define si el transporte es
propio o de terceros.
El motivo condiciona quién puede ser el receptor
Con reason=RemissionReason.BETWEEN_COMPANY_LOCATIONS —traslado entre
locales de la misma empresa— el RUC del receptor tiene que coincidir con el
del emisor. El motivo por omisión, SALE, no tiene esa restricción.
Autofacturas¶
Para cuando se le compra a alguien que no puede emitir factura. No hay receptor: va el proveedor, y aparte el lugar donde se hizo la operación.
sd.auto_invoices.create(
provider=smartdoc.Provider.no_ruc(
full_name="Proveedor Informal",
document_number="1234567",
address="Calle 1",
city=ciudad,
house_number=50,
),
items=[smartdoc.Item("Materia prima", quantity=1, unit_amount=500000)],
transaction_address="Depósito Central",
transaction_city=ciudad,
transaction_house_number=100,
certificate_code="12345678", # exactamente 8 caracteres
certificate_number="12345678901", # exactamente 11 caracteres
)
Son obligatorios el house_number del proveedor y el del lugar de la operación,
más el código y el número de la constancia, que tienen largo exacto.
Listar¶
pagina = sd.invoices.list(status="approved_by_set", date_from="2026-01-01")
for factura in pagina: # solo esta página
...
for factura in pagina.auto_paging_iter(): # todas, siguiendo el cursor
...
El tamaño por omisión es 20 y el máximo 100. auto_paging_iter() respeta el
límite de tasa, así que recorrer un listado largo lleva su tiempo.