digifact-sdk 2.2.0__tar.gz → 2.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digifact_sdk-2.2.2/PKG-INFO +325 -0
- digifact_sdk-2.2.2/README.md +301 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk/builder.py +86 -18
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk/client.py +57 -1
- digifact_sdk-2.2.2/digifact_sdk.egg-info/PKG-INFO +325 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/pyproject.toml +1 -1
- digifact_sdk-2.2.0/PKG-INFO +0 -278
- digifact_sdk-2.2.0/README.md +0 -254
- digifact_sdk-2.2.0/digifact_sdk.egg-info/PKG-INFO +0 -278
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk/__init__.py +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk/exceptions.py +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk/tax.py +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk.egg-info/SOURCES.txt +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk.egg-info/dependency_links.txt +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk.egg-info/requires.txt +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/digifact_sdk.egg-info/top_level.txt +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/setup.cfg +0 -0
- {digifact_sdk-2.2.0 → digifact_sdk-2.2.2}/tests/test_integration.py +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: digifact-sdk
|
|
3
|
+
Version: 2.2.2
|
|
4
|
+
Summary: Python SDK for Digifact FEL Guatemala e-invoicing API
|
|
5
|
+
Author-email: AALONZOLU <noreply@github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: digifact,fel,guatemala,factura,e-invoicing,sat
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: requests>=2.28
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
23
|
+
Requires-Dist: python-dotenv; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# Digifact FEL Guatemala — SDK para Python
|
|
26
|
+
|
|
27
|
+
SDK en Python para la API de facturación electrónica en línea (FEL) de Guatemala de [Digifact](https://www.digifact.com.gt/).
|
|
28
|
+
|
|
29
|
+
## Instalación
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install digifact-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
O desde el código fuente:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e sdk/python/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Inicio rápido
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from digifact_sdk import DigifactClient
|
|
45
|
+
|
|
46
|
+
client = DigifactClient(
|
|
47
|
+
taxid="12345678",
|
|
48
|
+
username="FELUSER",
|
|
49
|
+
password="secret",
|
|
50
|
+
environment="test", # o "production"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# FACT CF — consumidor final, el IVA se calcula automáticamente
|
|
54
|
+
result = client.invoice(
|
|
55
|
+
buyer="CF",
|
|
56
|
+
items=[{"description": "Consultoría", "qty": 1, "price": 100.00}]
|
|
57
|
+
)
|
|
58
|
+
print(result.auth_number)
|
|
59
|
+
|
|
60
|
+
# FACT a NIT — el nombre del receptor se consulta automáticamente en SAT
|
|
61
|
+
result = client.invoice(
|
|
62
|
+
buyer="12345678",
|
|
63
|
+
items=[
|
|
64
|
+
{"description": "Laptop", "qty": 1, "price": 5000.00, "type": "Bien"},
|
|
65
|
+
{"description": "Soporte anual", "qty": 1, "price": 500.00},
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# FACT a receptor con CUI
|
|
70
|
+
result = client.invoice(
|
|
71
|
+
buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
|
|
72
|
+
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Receptor NIT con datos explícitos (sin consulta automática)
|
|
76
|
+
result = client.invoice(
|
|
77
|
+
buyer={
|
|
78
|
+
"taxid": "12345678",
|
|
79
|
+
"name": "EMPRESA EJEMPLO S.A.",
|
|
80
|
+
"address": "6 AV 6-48 ZONA 9",
|
|
81
|
+
"city": "01009",
|
|
82
|
+
"district": "GUATEMALA",
|
|
83
|
+
"state": "GUATEMALA",
|
|
84
|
+
"country": "GT",
|
|
85
|
+
"email": "facturacion@empresa.com", # opcional
|
|
86
|
+
},
|
|
87
|
+
items=[{"description": "Producto", "qty": 1, "price": 100.00}]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# FCAM (Factura Cambiaria)
|
|
91
|
+
result = client.invoice(
|
|
92
|
+
buyer="12345678",
|
|
93
|
+
items=[{"description": "Servicio", "qty": 1, "price": 500.00}],
|
|
94
|
+
doc_type="FCAM",
|
|
95
|
+
payment_terms=[{"date": "2026-04-18", "amount": 500.00}]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Nota de crédito (NCRE)
|
|
99
|
+
result = client.credit_note(
|
|
100
|
+
buyer="12345678",
|
|
101
|
+
items=[{"description": "Devolución", "qty": 1, "price": 100.00}],
|
|
102
|
+
origin={
|
|
103
|
+
"auth_number": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
|
104
|
+
"date": "2026-03-18",
|
|
105
|
+
"series": "XXXXXXXX",
|
|
106
|
+
"number": "123456",
|
|
107
|
+
},
|
|
108
|
+
reason="Producto defectuoso"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Nota de débito (NDEB)
|
|
112
|
+
result = client.debit_note(
|
|
113
|
+
buyer="12345678",
|
|
114
|
+
items=[{"description": "Cargo adicional", "qty": 1, "price": 50.00}],
|
|
115
|
+
origin={...},
|
|
116
|
+
reason="Cargo por entrega express"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Anular un DTE
|
|
120
|
+
result = client.cancel(
|
|
121
|
+
auth_number="XXXXXXXX-...",
|
|
122
|
+
receiver_id="CF",
|
|
123
|
+
issue_datetime="2026-03-18 21:40:14",
|
|
124
|
+
reason="Error en monto"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Nota de crédito total
|
|
128
|
+
result = client.credit_note_total(
|
|
129
|
+
auth_number="XXXXXXXX-...",
|
|
130
|
+
issue_datetime="2026-03-18 21:40:14",
|
|
131
|
+
reason="Nota de crédito total"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Consulta de NIT
|
|
135
|
+
info = client.lookup_nit("12345678")
|
|
136
|
+
print(info["name"])
|
|
137
|
+
|
|
138
|
+
# Obtener DTE
|
|
139
|
+
doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Tipos de documento
|
|
143
|
+
|
|
144
|
+
| Tipo | Descripción | IVA |
|
|
145
|
+
|------|-------------|-----|
|
|
146
|
+
| `FACT` | Factura estándar | Sí |
|
|
147
|
+
| `FCAM` | Factura Cambiaria (con abonos) | Sí |
|
|
148
|
+
| `NDEB` | Nota de débito | Sí |
|
|
149
|
+
| `NCRE` | Nota de crédito | Sí |
|
|
150
|
+
| `NABN` | Nota de abono | No |
|
|
151
|
+
| `FESP` | Factura especial | Sí |
|
|
152
|
+
| `RDON` | Recibo por donación | No |
|
|
153
|
+
| `FPEQ` | Factura pequeño contribuyente | No |
|
|
154
|
+
| `RECI` | Recibo universitario | No |
|
|
155
|
+
| `CCA` | Cobro por cuenta ajena | Sí |
|
|
156
|
+
|
|
157
|
+
## Cálculo del IVA
|
|
158
|
+
|
|
159
|
+
Los precios incluyen IVA (es lo que paga el cliente):
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
total_linea = qty × price
|
|
163
|
+
base_imponible = total_linea / 1.12
|
|
164
|
+
monto_iva = total_linea − base_imponible
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Todos los montos se formatean como cadenas con 6 decimales.
|
|
168
|
+
|
|
169
|
+
## Campos del ítem
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
{
|
|
173
|
+
"description": str, # requerido
|
|
174
|
+
"price": float | Decimal, # requerido — precio unitario, incluye IVA
|
|
175
|
+
"qty": float | Decimal, # opcional, por defecto 1
|
|
176
|
+
"type": str, # opcional: "Servicio" (por defecto) | "Bien"
|
|
177
|
+
"unit_of_measure": str, # opcional, por defecto "UNI"
|
|
178
|
+
"discount": float | None, # opcional — descuento de la línea
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Facturas de combustible (FACT Combustible)
|
|
183
|
+
|
|
184
|
+
Las facturas de combustible emiten IVA **y** un impuesto PETROLEO según la
|
|
185
|
+
especificación de SAT. Los ítems sin `petroleo_amount` se tratan como ítems
|
|
186
|
+
regulares (sólo IVA) y pueden coexistir en la misma factura.
|
|
187
|
+
|
|
188
|
+
### Opción A — tarifas fijadas al inicializar el cliente (recomendada para gasolineras)
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# Fijar tarifas PETROLEO una sola vez por tipo de combustible (Q/galón, según MEM o factura del proveedor)
|
|
192
|
+
client = DigifactClient(
|
|
193
|
+
taxid="12345678",
|
|
194
|
+
username="FELUSER",
|
|
195
|
+
password="secret",
|
|
196
|
+
petroleo_rates={"1": 4.70, "2": 4.60, "4": 1.30}, # SUPER / REGULAR / DIESEL
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Sólo hace falta petroleo_code — petroleo_amount se completa automáticamente
|
|
200
|
+
result = client.fuel_invoice(
|
|
201
|
+
buyer="CF",
|
|
202
|
+
items=[
|
|
203
|
+
{"description": "GASOLINA SUPER", "qty": 30, "price": 35.00, "petroleo_code": "1", "type": "Bien"},
|
|
204
|
+
{"description": "GASOLINA REGULAR", "qty": 20, "price": 34.00, "petroleo_code": "2", "type": "Bien"},
|
|
205
|
+
{"description": "GASOLINA DIESEL", "qty": 50, "price": 32.00, "petroleo_code": "4", "type": "Bien"},
|
|
206
|
+
# Ítems regulares (sin petroleo_code): sólo IVA, pueden coexistir
|
|
207
|
+
{"description": "FILTRO DE ACEITE", "qty": 1, "price": 45.00, "type": "Bien"},
|
|
208
|
+
{"description": "SET DE CANDELAS NGK", "qty": 1, "price": 400.00, "type": "Bien"},
|
|
209
|
+
],
|
|
210
|
+
)
|
|
211
|
+
print(result.auth_number)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Opción B — monto explícito por ítem
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
result = client.fuel_invoice(
|
|
218
|
+
buyer="CF",
|
|
219
|
+
items=[
|
|
220
|
+
{"description": "GASOLINA SUPER", "qty": 1, "price": 35.00, "petroleo_amount": 4.70, "petroleo_code": "1", "type": "Bien"},
|
|
221
|
+
{"description": "GASOLINA DIESEL", "qty": 1, "price": 32.00, "petroleo_amount": 1.30, "petroleo_code": "4", "type": "Bien"},
|
|
222
|
+
],
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Campos del ítem de combustible
|
|
227
|
+
|
|
228
|
+
| Campo | Tipo | Por defecto | Descripción |
|
|
229
|
+
|-----|------|---------|-------------|
|
|
230
|
+
| `description` | `str` | requerido | Descripción de la línea |
|
|
231
|
+
| `price` | `float\|Decimal` | requerido | Precio unitario completo al consumidor (incluye PETROLEO + IVA). Es lo que paga el cliente en la bomba. Si la factura del proveedor muestra un precio unitario *sin* PETROLEO/IDP (p. ej. `37.99`), suma la tarifa IDP por unidad: `price = 37.99 + 4.70 = 42.69`. |
|
|
232
|
+
| `qty` | `float\|Decimal` | `1` | Cantidad |
|
|
233
|
+
| `type` | `str` | `"Servicio"` | `"Bien"` o `"Servicio"` |
|
|
234
|
+
| `unit_of_measure` | `str` | `"UNI"` | Código de unidad de SAT |
|
|
235
|
+
| `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
|
|
236
|
+
| `petroleo_code` | `str` | `"1"` | `"1"`=SUPER, `"2"`=REGULAR, `"4"`=DIESEL. Obligatorio cuando se omite `petroleo_amount` y `petroleo_rates` está configurado; lanza `DigifactValidationError` si el código no está en el diccionario de tarifas. |
|
|
237
|
+
|
|
238
|
+
## Configuración de frases (TipoFrase / CodigoEscenario)
|
|
239
|
+
|
|
240
|
+
Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
|
|
241
|
+
SDK elige valores por defecto adecuados, por lo que **no** hace falta
|
|
242
|
+
configurar nada en el caso común.
|
|
243
|
+
|
|
244
|
+
**Orden de precedencia:** argumentos por llamada → globales del constructor (`tipo_frase` / `escenario`) → tabla de valores por defecto.
|
|
245
|
+
|
|
246
|
+
**Tabla de valores por defecto:**
|
|
247
|
+
|
|
248
|
+
| DTE | Afiliación | TipoFrase | CodigoEscenario | Notas |
|
|
249
|
+
|-------------|-----------:|:---------:|:---------------:|-------|
|
|
250
|
+
| FESP | — | — | — | Sin bloque `AdditionlInfo` |
|
|
251
|
+
| FPEQ | PEQ | `2` | `1` | Pequeño contribuyente |
|
|
252
|
+
| RDON | cualquiera | `4` | `4` | Donaciones |
|
|
253
|
+
| RECI | cualquiera | `4` | `5` | Recibos (universidades) |
|
|
254
|
+
| NABN | cualquiera | `1` | `1` | Abonos |
|
|
255
|
+
| FACT / FCAM / NCRE / NDEB | **GEN** | `1` | `1` | Por defecto: ISR **régimen sobre utilidades trimestrales** |
|
|
256
|
+
| FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
|
|
257
|
+
| FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
|
|
258
|
+
|
|
259
|
+
Tanto `tipo_frase` como `escenario` se pueden sobreescribir de forma
|
|
260
|
+
independiente — por llamada (como argumentos con nombre) o globalmente al
|
|
261
|
+
construir el cliente. Cuando se omiten, cada uno cae al global del
|
|
262
|
+
constructor y luego a la tabla de valores por defecto.
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# Sobreescritura por llamada (uno o ambos)
|
|
266
|
+
client.invoice("CF", items, escenario="1")
|
|
267
|
+
client.invoice("CF", items, tipo_frase="2", escenario="1")
|
|
268
|
+
|
|
269
|
+
# Funciona igual en los demás métodos de DTE
|
|
270
|
+
client.credit_note("12345678", items, origin, "...", tipo_frase="2", escenario="1")
|
|
271
|
+
client.fuel_invoice("CF", items, tipo_frase="2", escenario="1")
|
|
272
|
+
|
|
273
|
+
# O globalmente al construir el cliente (p. ej. GEN + ISR régimen opcional simplificado)
|
|
274
|
+
client = DigifactClient(
|
|
275
|
+
taxid="12345678", username="FELUSER", password="...",
|
|
276
|
+
afiliacion_iva="GEN",
|
|
277
|
+
tipo_frase="1", # opcional — la tabla ya devuelve "1" para GEN
|
|
278
|
+
escenario="2", # ISR régimen opcional simplificado (sobreescribe el "1" por defecto)
|
|
279
|
+
)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Para descubrir el par correcto en un caso particular, revisa las afiliaciones
|
|
283
|
+
del RTU en el portal de SAT.
|
|
284
|
+
|
|
285
|
+
## Ejecutar las pruebas
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# Pruebas unitarias (no requieren credenciales)
|
|
289
|
+
python -m pytest tests/ -v
|
|
290
|
+
|
|
291
|
+
# Pruebas de integración
|
|
292
|
+
export DIGIFACT_TAXID=12345678
|
|
293
|
+
export DIGIFACT_USERNAME=FELUSER
|
|
294
|
+
export DIGIFACT_PASSWORD=tu_contraseña
|
|
295
|
+
python -m pytest tests/ -v
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Variables de entorno
|
|
299
|
+
|
|
300
|
+
| Variable | Descripción |
|
|
301
|
+
|----------|-------------|
|
|
302
|
+
| `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
|
|
303
|
+
| `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
|
|
304
|
+
| `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
|
|
305
|
+
|
|
306
|
+
## Manejo de errores
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from digifact_sdk import (
|
|
310
|
+
DigifactError, # base
|
|
311
|
+
DigifactAuthError, # fallo de autenticación
|
|
312
|
+
DigifactApiError, # error HTTP / de API
|
|
313
|
+
DigifactValidationError, # rechazo de SAT
|
|
314
|
+
DigifactNitNotFoundError, # NIT no encontrado
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
result = client.invoice("CF", [...])
|
|
319
|
+
except DigifactValidationError as exc:
|
|
320
|
+
print(f"SAT rechazó: {exc}")
|
|
321
|
+
print(f"Código: {exc.code}")
|
|
322
|
+
print(f"Respuesta: {exc.raw}")
|
|
323
|
+
except DigifactError as exc:
|
|
324
|
+
print(f"Error del SDK: {exc}")
|
|
325
|
+
```
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Digifact FEL Guatemala — SDK para Python
|
|
2
|
+
|
|
3
|
+
SDK en Python para la API de facturación electrónica en línea (FEL) de Guatemala de [Digifact](https://www.digifact.com.gt/).
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install digifact-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
O desde el código fuente:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e sdk/python/
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Inicio rápido
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from digifact_sdk import DigifactClient
|
|
21
|
+
|
|
22
|
+
client = DigifactClient(
|
|
23
|
+
taxid="12345678",
|
|
24
|
+
username="FELUSER",
|
|
25
|
+
password="secret",
|
|
26
|
+
environment="test", # o "production"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# FACT CF — consumidor final, el IVA se calcula automáticamente
|
|
30
|
+
result = client.invoice(
|
|
31
|
+
buyer="CF",
|
|
32
|
+
items=[{"description": "Consultoría", "qty": 1, "price": 100.00}]
|
|
33
|
+
)
|
|
34
|
+
print(result.auth_number)
|
|
35
|
+
|
|
36
|
+
# FACT a NIT — el nombre del receptor se consulta automáticamente en SAT
|
|
37
|
+
result = client.invoice(
|
|
38
|
+
buyer="12345678",
|
|
39
|
+
items=[
|
|
40
|
+
{"description": "Laptop", "qty": 1, "price": 5000.00, "type": "Bien"},
|
|
41
|
+
{"description": "Soporte anual", "qty": 1, "price": 500.00},
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# FACT a receptor con CUI
|
|
46
|
+
result = client.invoice(
|
|
47
|
+
buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
|
|
48
|
+
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Receptor NIT con datos explícitos (sin consulta automática)
|
|
52
|
+
result = client.invoice(
|
|
53
|
+
buyer={
|
|
54
|
+
"taxid": "12345678",
|
|
55
|
+
"name": "EMPRESA EJEMPLO S.A.",
|
|
56
|
+
"address": "6 AV 6-48 ZONA 9",
|
|
57
|
+
"city": "01009",
|
|
58
|
+
"district": "GUATEMALA",
|
|
59
|
+
"state": "GUATEMALA",
|
|
60
|
+
"country": "GT",
|
|
61
|
+
"email": "facturacion@empresa.com", # opcional
|
|
62
|
+
},
|
|
63
|
+
items=[{"description": "Producto", "qty": 1, "price": 100.00}]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# FCAM (Factura Cambiaria)
|
|
67
|
+
result = client.invoice(
|
|
68
|
+
buyer="12345678",
|
|
69
|
+
items=[{"description": "Servicio", "qty": 1, "price": 500.00}],
|
|
70
|
+
doc_type="FCAM",
|
|
71
|
+
payment_terms=[{"date": "2026-04-18", "amount": 500.00}]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Nota de crédito (NCRE)
|
|
75
|
+
result = client.credit_note(
|
|
76
|
+
buyer="12345678",
|
|
77
|
+
items=[{"description": "Devolución", "qty": 1, "price": 100.00}],
|
|
78
|
+
origin={
|
|
79
|
+
"auth_number": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
|
80
|
+
"date": "2026-03-18",
|
|
81
|
+
"series": "XXXXXXXX",
|
|
82
|
+
"number": "123456",
|
|
83
|
+
},
|
|
84
|
+
reason="Producto defectuoso"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Nota de débito (NDEB)
|
|
88
|
+
result = client.debit_note(
|
|
89
|
+
buyer="12345678",
|
|
90
|
+
items=[{"description": "Cargo adicional", "qty": 1, "price": 50.00}],
|
|
91
|
+
origin={...},
|
|
92
|
+
reason="Cargo por entrega express"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Anular un DTE
|
|
96
|
+
result = client.cancel(
|
|
97
|
+
auth_number="XXXXXXXX-...",
|
|
98
|
+
receiver_id="CF",
|
|
99
|
+
issue_datetime="2026-03-18 21:40:14",
|
|
100
|
+
reason="Error en monto"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Nota de crédito total
|
|
104
|
+
result = client.credit_note_total(
|
|
105
|
+
auth_number="XXXXXXXX-...",
|
|
106
|
+
issue_datetime="2026-03-18 21:40:14",
|
|
107
|
+
reason="Nota de crédito total"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Consulta de NIT
|
|
111
|
+
info = client.lookup_nit("12345678")
|
|
112
|
+
print(info["name"])
|
|
113
|
+
|
|
114
|
+
# Obtener DTE
|
|
115
|
+
doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Tipos de documento
|
|
119
|
+
|
|
120
|
+
| Tipo | Descripción | IVA |
|
|
121
|
+
|------|-------------|-----|
|
|
122
|
+
| `FACT` | Factura estándar | Sí |
|
|
123
|
+
| `FCAM` | Factura Cambiaria (con abonos) | Sí |
|
|
124
|
+
| `NDEB` | Nota de débito | Sí |
|
|
125
|
+
| `NCRE` | Nota de crédito | Sí |
|
|
126
|
+
| `NABN` | Nota de abono | No |
|
|
127
|
+
| `FESP` | Factura especial | Sí |
|
|
128
|
+
| `RDON` | Recibo por donación | No |
|
|
129
|
+
| `FPEQ` | Factura pequeño contribuyente | No |
|
|
130
|
+
| `RECI` | Recibo universitario | No |
|
|
131
|
+
| `CCA` | Cobro por cuenta ajena | Sí |
|
|
132
|
+
|
|
133
|
+
## Cálculo del IVA
|
|
134
|
+
|
|
135
|
+
Los precios incluyen IVA (es lo que paga el cliente):
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
total_linea = qty × price
|
|
139
|
+
base_imponible = total_linea / 1.12
|
|
140
|
+
monto_iva = total_linea − base_imponible
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Todos los montos se formatean como cadenas con 6 decimales.
|
|
144
|
+
|
|
145
|
+
## Campos del ítem
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
{
|
|
149
|
+
"description": str, # requerido
|
|
150
|
+
"price": float | Decimal, # requerido — precio unitario, incluye IVA
|
|
151
|
+
"qty": float | Decimal, # opcional, por defecto 1
|
|
152
|
+
"type": str, # opcional: "Servicio" (por defecto) | "Bien"
|
|
153
|
+
"unit_of_measure": str, # opcional, por defecto "UNI"
|
|
154
|
+
"discount": float | None, # opcional — descuento de la línea
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Facturas de combustible (FACT Combustible)
|
|
159
|
+
|
|
160
|
+
Las facturas de combustible emiten IVA **y** un impuesto PETROLEO según la
|
|
161
|
+
especificación de SAT. Los ítems sin `petroleo_amount` se tratan como ítems
|
|
162
|
+
regulares (sólo IVA) y pueden coexistir en la misma factura.
|
|
163
|
+
|
|
164
|
+
### Opción A — tarifas fijadas al inicializar el cliente (recomendada para gasolineras)
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# Fijar tarifas PETROLEO una sola vez por tipo de combustible (Q/galón, según MEM o factura del proveedor)
|
|
168
|
+
client = DigifactClient(
|
|
169
|
+
taxid="12345678",
|
|
170
|
+
username="FELUSER",
|
|
171
|
+
password="secret",
|
|
172
|
+
petroleo_rates={"1": 4.70, "2": 4.60, "4": 1.30}, # SUPER / REGULAR / DIESEL
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Sólo hace falta petroleo_code — petroleo_amount se completa automáticamente
|
|
176
|
+
result = client.fuel_invoice(
|
|
177
|
+
buyer="CF",
|
|
178
|
+
items=[
|
|
179
|
+
{"description": "GASOLINA SUPER", "qty": 30, "price": 35.00, "petroleo_code": "1", "type": "Bien"},
|
|
180
|
+
{"description": "GASOLINA REGULAR", "qty": 20, "price": 34.00, "petroleo_code": "2", "type": "Bien"},
|
|
181
|
+
{"description": "GASOLINA DIESEL", "qty": 50, "price": 32.00, "petroleo_code": "4", "type": "Bien"},
|
|
182
|
+
# Ítems regulares (sin petroleo_code): sólo IVA, pueden coexistir
|
|
183
|
+
{"description": "FILTRO DE ACEITE", "qty": 1, "price": 45.00, "type": "Bien"},
|
|
184
|
+
{"description": "SET DE CANDELAS NGK", "qty": 1, "price": 400.00, "type": "Bien"},
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
print(result.auth_number)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Opción B — monto explícito por ítem
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
result = client.fuel_invoice(
|
|
194
|
+
buyer="CF",
|
|
195
|
+
items=[
|
|
196
|
+
{"description": "GASOLINA SUPER", "qty": 1, "price": 35.00, "petroleo_amount": 4.70, "petroleo_code": "1", "type": "Bien"},
|
|
197
|
+
{"description": "GASOLINA DIESEL", "qty": 1, "price": 32.00, "petroleo_amount": 1.30, "petroleo_code": "4", "type": "Bien"},
|
|
198
|
+
],
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Campos del ítem de combustible
|
|
203
|
+
|
|
204
|
+
| Campo | Tipo | Por defecto | Descripción |
|
|
205
|
+
|-----|------|---------|-------------|
|
|
206
|
+
| `description` | `str` | requerido | Descripción de la línea |
|
|
207
|
+
| `price` | `float\|Decimal` | requerido | Precio unitario completo al consumidor (incluye PETROLEO + IVA). Es lo que paga el cliente en la bomba. Si la factura del proveedor muestra un precio unitario *sin* PETROLEO/IDP (p. ej. `37.99`), suma la tarifa IDP por unidad: `price = 37.99 + 4.70 = 42.69`. |
|
|
208
|
+
| `qty` | `float\|Decimal` | `1` | Cantidad |
|
|
209
|
+
| `type` | `str` | `"Servicio"` | `"Bien"` o `"Servicio"` |
|
|
210
|
+
| `unit_of_measure` | `str` | `"UNI"` | Código de unidad de SAT |
|
|
211
|
+
| `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
|
|
212
|
+
| `petroleo_code` | `str` | `"1"` | `"1"`=SUPER, `"2"`=REGULAR, `"4"`=DIESEL. Obligatorio cuando se omite `petroleo_amount` y `petroleo_rates` está configurado; lanza `DigifactValidationError` si el código no está en el diccionario de tarifas. |
|
|
213
|
+
|
|
214
|
+
## Configuración de frases (TipoFrase / CodigoEscenario)
|
|
215
|
+
|
|
216
|
+
Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
|
|
217
|
+
SDK elige valores por defecto adecuados, por lo que **no** hace falta
|
|
218
|
+
configurar nada en el caso común.
|
|
219
|
+
|
|
220
|
+
**Orden de precedencia:** argumentos por llamada → globales del constructor (`tipo_frase` / `escenario`) → tabla de valores por defecto.
|
|
221
|
+
|
|
222
|
+
**Tabla de valores por defecto:**
|
|
223
|
+
|
|
224
|
+
| DTE | Afiliación | TipoFrase | CodigoEscenario | Notas |
|
|
225
|
+
|-------------|-----------:|:---------:|:---------------:|-------|
|
|
226
|
+
| FESP | — | — | — | Sin bloque `AdditionlInfo` |
|
|
227
|
+
| FPEQ | PEQ | `2` | `1` | Pequeño contribuyente |
|
|
228
|
+
| RDON | cualquiera | `4` | `4` | Donaciones |
|
|
229
|
+
| RECI | cualquiera | `4` | `5` | Recibos (universidades) |
|
|
230
|
+
| NABN | cualquiera | `1` | `1` | Abonos |
|
|
231
|
+
| FACT / FCAM / NCRE / NDEB | **GEN** | `1` | `1` | Por defecto: ISR **régimen sobre utilidades trimestrales** |
|
|
232
|
+
| FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
|
|
233
|
+
| FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
|
|
234
|
+
|
|
235
|
+
Tanto `tipo_frase` como `escenario` se pueden sobreescribir de forma
|
|
236
|
+
independiente — por llamada (como argumentos con nombre) o globalmente al
|
|
237
|
+
construir el cliente. Cuando se omiten, cada uno cae al global del
|
|
238
|
+
constructor y luego a la tabla de valores por defecto.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
# Sobreescritura por llamada (uno o ambos)
|
|
242
|
+
client.invoice("CF", items, escenario="1")
|
|
243
|
+
client.invoice("CF", items, tipo_frase="2", escenario="1")
|
|
244
|
+
|
|
245
|
+
# Funciona igual en los demás métodos de DTE
|
|
246
|
+
client.credit_note("12345678", items, origin, "...", tipo_frase="2", escenario="1")
|
|
247
|
+
client.fuel_invoice("CF", items, tipo_frase="2", escenario="1")
|
|
248
|
+
|
|
249
|
+
# O globalmente al construir el cliente (p. ej. GEN + ISR régimen opcional simplificado)
|
|
250
|
+
client = DigifactClient(
|
|
251
|
+
taxid="12345678", username="FELUSER", password="...",
|
|
252
|
+
afiliacion_iva="GEN",
|
|
253
|
+
tipo_frase="1", # opcional — la tabla ya devuelve "1" para GEN
|
|
254
|
+
escenario="2", # ISR régimen opcional simplificado (sobreescribe el "1" por defecto)
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Para descubrir el par correcto en un caso particular, revisa las afiliaciones
|
|
259
|
+
del RTU en el portal de SAT.
|
|
260
|
+
|
|
261
|
+
## Ejecutar las pruebas
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Pruebas unitarias (no requieren credenciales)
|
|
265
|
+
python -m pytest tests/ -v
|
|
266
|
+
|
|
267
|
+
# Pruebas de integración
|
|
268
|
+
export DIGIFACT_TAXID=12345678
|
|
269
|
+
export DIGIFACT_USERNAME=FELUSER
|
|
270
|
+
export DIGIFACT_PASSWORD=tu_contraseña
|
|
271
|
+
python -m pytest tests/ -v
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Variables de entorno
|
|
275
|
+
|
|
276
|
+
| Variable | Descripción |
|
|
277
|
+
|----------|-------------|
|
|
278
|
+
| `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
|
|
279
|
+
| `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
|
|
280
|
+
| `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
|
|
281
|
+
|
|
282
|
+
## Manejo de errores
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from digifact_sdk import (
|
|
286
|
+
DigifactError, # base
|
|
287
|
+
DigifactAuthError, # fallo de autenticación
|
|
288
|
+
DigifactApiError, # error HTTP / de API
|
|
289
|
+
DigifactValidationError, # rechazo de SAT
|
|
290
|
+
DigifactNitNotFoundError, # NIT no encontrado
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
result = client.invoice("CF", [...])
|
|
295
|
+
except DigifactValidationError as exc:
|
|
296
|
+
print(f"SAT rechazó: {exc}")
|
|
297
|
+
print(f"Código: {exc.code}")
|
|
298
|
+
print(f"Respuesta: {exc.raw}")
|
|
299
|
+
except DigifactError as exc:
|
|
300
|
+
print(f"Error del SDK: {exc}")
|
|
301
|
+
```
|