digifact-sdk 2.2.1__tar.gz → 2.2.3__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.
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.4
2
+ Name: digifact-sdk
3
+ Version: 2.2.3
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
+ ## Configuración del cliente (`DigifactClient(...)`)
42
+
43
+ | Parámetro | Tipo | Por defecto | Descripción |
44
+ |-----------|------|-------------|-------------|
45
+ | `taxid` | `str` | **requerido** | NIT del emisor. Acepta dígitos o con separadores (`"12345678"`, `"1234567-8"`); los no-dígitos se eliminan. Se rellena internamente a 12 caracteres. |
46
+ | `username` | `str` | **requerido** | Usuario corto de Digifact (la parte después de `GT.<NIT>.`, p. ej. `"FELUSER"`). |
47
+ | `password` | `str` | `""` | Contraseña de la cuenta. **Requerido** si no se provee `token`. |
48
+ | `environment` | `str` | `"test"` | `"test"` o `"production"`. |
49
+ | `token` | `str` | `""` | Bearer token preobtenido. Si se provee, se omite el login. |
50
+ | `seller_name` | `str` | `""` | Nombre comercial del emisor. Si está vacío, se consulta en SAT vía `lookup_nit()`. |
51
+ | `seller_address` | `str` | `""` | Dirección del emisor. Si está vacía, se consulta en SAT. |
52
+ | `afiliacion_iva` | `str` | `"GEN"` | Afiliación IVA del RTU: `"GEN"`, `"PEQ"` o `"EXE"`. |
53
+ | `tipo_personeria` | `str` | `"1"` | Código de `TipoPersoneria` del RTU (usado en RDON). |
54
+ | `branch_code` | `str` | `"1"` | **Código del establecimiento** del RTU. Un NIT puede tener varios establecimientos (1, 2, 3…). Se escribe en `Seller.BranchInfo.Code`. |
55
+ | `branch_name` | `str` | `"ESTABLECIMIENTO PRINCIPAL"` | Nombre comercial del establecimiento. Se escribe en `Seller.BranchInfo.Name`. |
56
+ | `tipo_frase` | `str \| None` | `None` | Sobreescritura global de `TipoFrase`. Ver [frases](#configuración-de-frases-tipofrase--codigoescenario). |
57
+ | `escenario` | `str \| None` | `None` | Sobreescritura global de `CodigoEscenario`. |
58
+ | `timeout` | `int` | `120` | Timeout HTTP en segundos. |
59
+ | `session` | `requests.Session \| None` | `None` | Sesión HTTP personalizada (útil para tests). |
60
+ | `petroleo_rates` | `dict[str, float] \| None` | `None` | Mapa código PETROLEO → tarifa por unidad. Usado por `fuel_invoice()`. |
61
+
62
+ ## Inicio rápido
63
+
64
+ ```python
65
+ from digifact_sdk import DigifactClient
66
+
67
+ client = DigifactClient(
68
+ taxid="12345678",
69
+ username="FELUSER",
70
+ password="secret",
71
+ environment="test", # o "production"
72
+ )
73
+
74
+ # FACT CF — consumidor final, el IVA se calcula automáticamente
75
+ result = client.invoice(
76
+ buyer="CF",
77
+ items=[{"description": "Consultoría", "qty": 1, "price": 100.00}]
78
+ )
79
+ print(result.auth_number)
80
+
81
+ # FACT a NIT — el nombre del receptor se consulta automáticamente en SAT
82
+ result = client.invoice(
83
+ buyer="12345678",
84
+ items=[
85
+ {"description": "Laptop", "qty": 1, "price": 5000.00, "type": "Bien"},
86
+ {"description": "Soporte anual", "qty": 1, "price": 500.00},
87
+ ]
88
+ )
89
+
90
+ # FACT a receptor con CUI
91
+ result = client.invoice(
92
+ buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
93
+ items=[{"description": "Producto", "qty": 2, "price": 50.00}]
94
+ )
95
+
96
+ # Receptor NIT con datos explícitos (sin consulta automática)
97
+ result = client.invoice(
98
+ buyer={
99
+ "taxid": "12345678",
100
+ "name": "EMPRESA EJEMPLO S.A.",
101
+ "address": "6 AV 6-48 ZONA 9",
102
+ "city": "01009",
103
+ "district": "GUATEMALA",
104
+ "state": "GUATEMALA",
105
+ "country": "GT",
106
+ "email": "facturacion@empresa.com", # opcional
107
+ },
108
+ items=[{"description": "Producto", "qty": 1, "price": 100.00}]
109
+ )
110
+
111
+ # FCAM (Factura Cambiaria)
112
+ result = client.invoice(
113
+ buyer="12345678",
114
+ items=[{"description": "Servicio", "qty": 1, "price": 500.00}],
115
+ doc_type="FCAM",
116
+ payment_terms=[{"date": "2026-04-18", "amount": 500.00}]
117
+ )
118
+
119
+ # Nota de crédito (NCRE)
120
+ result = client.credit_note(
121
+ buyer="12345678",
122
+ items=[{"description": "Devolución", "qty": 1, "price": 100.00}],
123
+ origin={
124
+ "auth_number": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
125
+ "date": "2026-03-18",
126
+ "series": "XXXXXXXX",
127
+ "number": "123456",
128
+ },
129
+ reason="Producto defectuoso"
130
+ )
131
+
132
+ # Nota de débito (NDEB)
133
+ result = client.debit_note(
134
+ buyer="12345678",
135
+ items=[{"description": "Cargo adicional", "qty": 1, "price": 50.00}],
136
+ origin={...},
137
+ reason="Cargo por entrega express"
138
+ )
139
+
140
+ # Anular un DTE
141
+ result = client.cancel(
142
+ auth_number="XXXXXXXX-...",
143
+ receiver_id="CF",
144
+ issue_datetime="2026-03-18 21:40:14",
145
+ reason="Error en monto"
146
+ )
147
+
148
+ # Nota de crédito total
149
+ result = client.credit_note_total(
150
+ auth_number="XXXXXXXX-...",
151
+ issue_datetime="2026-03-18 21:40:14",
152
+ reason="Nota de crédito total"
153
+ )
154
+
155
+ # Consulta de NIT
156
+ info = client.lookup_nit("12345678")
157
+ print(info["name"])
158
+
159
+ # Obtener DTE
160
+ doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
161
+ ```
162
+
163
+ ## Tipos de documento
164
+
165
+ | Tipo | Descripción | IVA |
166
+ |------|-------------|-----|
167
+ | `FACT` | Factura estándar | Sí |
168
+ | `FCAM` | Factura Cambiaria (con abonos) | Sí |
169
+ | `NDEB` | Nota de débito | Sí |
170
+ | `NCRE` | Nota de crédito | Sí |
171
+ | `NABN` | Nota de abono | No |
172
+ | `FESP` | Factura especial | Sí |
173
+ | `RDON` | Recibo por donación | No |
174
+ | `FPEQ` | Factura pequeño contribuyente | No |
175
+ | `RECI` | Recibo universitario | No |
176
+ | `CCA` | Cobro por cuenta ajena | Sí |
177
+
178
+ ## Cálculo del IVA
179
+
180
+ Los precios incluyen IVA (es lo que paga el cliente):
181
+
182
+ ```
183
+ total_linea = qty × price
184
+ base_imponible = total_linea / 1.12
185
+ monto_iva = total_linea − base_imponible
186
+ ```
187
+
188
+ Todos los montos se formatean como cadenas con 6 decimales.
189
+
190
+ ## Campos del ítem
191
+
192
+ ```python
193
+ {
194
+ "description": str, # requerido
195
+ "price": float | Decimal, # requerido — precio unitario, incluye IVA
196
+ "qty": float | Decimal, # opcional, por defecto 1
197
+ "type": str, # opcional: "Servicio" (por defecto) | "Bien"
198
+ "unit_of_measure": str, # opcional, por defecto "UNI"
199
+ "discount": float | None, # opcional — descuento de la línea
200
+ }
201
+ ```
202
+
203
+ ## Facturas de combustible (FACT Combustible)
204
+
205
+ Las facturas de combustible emiten IVA **y** un impuesto PETROLEO según la
206
+ especificación de SAT. Los ítems sin `petroleo_amount` se tratan como ítems
207
+ regulares (sólo IVA) y pueden coexistir en la misma factura.
208
+
209
+ ### Opción A — tarifas fijadas al inicializar el cliente (recomendada para gasolineras)
210
+
211
+ ```python
212
+ # Fijar tarifas PETROLEO una sola vez por tipo de combustible (Q/galón, según MEM o factura del proveedor)
213
+ client = DigifactClient(
214
+ taxid="12345678",
215
+ username="FELUSER",
216
+ password="secret",
217
+ petroleo_rates={"1": 4.70, "2": 4.60, "4": 1.30}, # SUPER / REGULAR / DIESEL
218
+ )
219
+
220
+ # Sólo hace falta petroleo_code — petroleo_amount se completa automáticamente
221
+ result = client.fuel_invoice(
222
+ buyer="CF",
223
+ items=[
224
+ {"description": "GASOLINA SUPER", "qty": 30, "price": 35.00, "petroleo_code": "1", "type": "Bien"},
225
+ {"description": "GASOLINA REGULAR", "qty": 20, "price": 34.00, "petroleo_code": "2", "type": "Bien"},
226
+ {"description": "GASOLINA DIESEL", "qty": 50, "price": 32.00, "petroleo_code": "4", "type": "Bien"},
227
+ # Ítems regulares (sin petroleo_code): sólo IVA, pueden coexistir
228
+ {"description": "FILTRO DE ACEITE", "qty": 1, "price": 45.00, "type": "Bien"},
229
+ {"description": "SET DE CANDELAS NGK", "qty": 1, "price": 400.00, "type": "Bien"},
230
+ ],
231
+ )
232
+ print(result.auth_number)
233
+ ```
234
+
235
+ ### Opción B — monto explícito por ítem
236
+
237
+ ```python
238
+ result = client.fuel_invoice(
239
+ buyer="CF",
240
+ items=[
241
+ {"description": "GASOLINA SUPER", "qty": 1, "price": 35.00, "petroleo_amount": 4.70, "petroleo_code": "1", "type": "Bien"},
242
+ {"description": "GASOLINA DIESEL", "qty": 1, "price": 32.00, "petroleo_amount": 1.30, "petroleo_code": "4", "type": "Bien"},
243
+ ],
244
+ )
245
+ ```
246
+
247
+ ### Campos del ítem de combustible
248
+
249
+ | Campo | Tipo | Por defecto | Descripción |
250
+ |-----|------|---------|-------------|
251
+ | `description` | `str` | requerido | Descripción de la línea |
252
+ | `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`. |
253
+ | `qty` | `float\|Decimal` | `1` | Cantidad |
254
+ | `type` | `str` | `"Servicio"` | `"Bien"` o `"Servicio"` |
255
+ | `unit_of_measure` | `str` | `"UNI"` | Código de unidad de SAT |
256
+ | `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
257
+ | `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. |
258
+
259
+ ## Configuración de frases (TipoFrase / CodigoEscenario)
260
+
261
+ Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
262
+ SDK elige valores por defecto adecuados, por lo que **no** hace falta
263
+ configurar nada en el caso común.
264
+
265
+ **Orden de precedencia:** argumentos por llamada → globales del constructor (`tipo_frase` / `escenario`) → tabla de valores por defecto.
266
+
267
+ **Tabla de valores por defecto:**
268
+
269
+ | DTE | Afiliación | TipoFrase | CodigoEscenario | Notas |
270
+ |-------------|-----------:|:---------:|:---------------:|-------|
271
+ | FESP | — | — | — | Sin bloque `AdditionlInfo` |
272
+ | FPEQ | PEQ | `2` | `1` | Pequeño contribuyente |
273
+ | RDON | cualquiera | `4` | `4` | Donaciones |
274
+ | RECI | cualquiera | `4` | `5` | Recibos (universidades) |
275
+ | NABN | cualquiera | `1` | `1` | Abonos |
276
+ | FACT / FCAM / NCRE / NDEB | **GEN** | `1` | `1` | Por defecto: ISR **régimen sobre utilidades trimestrales** |
277
+ | FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
278
+ | FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
279
+
280
+ Tanto `tipo_frase` como `escenario` se pueden sobreescribir de forma
281
+ independiente — por llamada (como argumentos con nombre) o globalmente al
282
+ construir el cliente. Cuando se omiten, cada uno cae al global del
283
+ constructor y luego a la tabla de valores por defecto.
284
+
285
+ ```python
286
+ # Sobreescritura por llamada (uno o ambos)
287
+ client.invoice("CF", items, escenario="1")
288
+ client.invoice("CF", items, tipo_frase="2", escenario="1")
289
+
290
+ # Funciona igual en los demás métodos de DTE
291
+ client.credit_note("12345678", items, origin, "...", tipo_frase="2", escenario="1")
292
+ client.fuel_invoice("CF", items, tipo_frase="2", escenario="1")
293
+
294
+ # O globalmente al construir el cliente (p. ej. GEN + ISR régimen opcional simplificado)
295
+ client = DigifactClient(
296
+ taxid="12345678", username="FELUSER", password="...",
297
+ afiliacion_iva="GEN",
298
+ tipo_frase="1", # opcional — la tabla ya devuelve "1" para GEN
299
+ escenario="2", # ISR régimen opcional simplificado (sobreescribe el "1" por defecto)
300
+ )
301
+ ```
302
+
303
+ Para descubrir el par correcto en un caso particular, revisa las afiliaciones
304
+ del RTU en el portal de SAT.
305
+
306
+ ## Referencia de métodos
307
+
308
+ Todos los métodos de emisión devuelven `DteResult` con `result.auth_number`, `series`, `number`, `issue_datetime`, `raw`.
309
+
310
+ | Método | Firma | Descripción |
311
+ |--------|-------|-------------|
312
+ | `invoice()` | `invoice(buyer, items, *, doc_type="FACT", payment_terms=None, amount_str="", observaciones="-", tipo_personeria=None, tipo_frase=None, escenario=None)` | Emite FACT, FCAM, FESP, FPEQ, NABN, RDON o RECI según `doc_type`. |
313
+ | `cca_invoice()` | `cca_invoice(buyer, items, cobros, *, tipo_frase=None, escenario=None)` | FACT con complemento CCA. |
314
+ | `fuel_invoice()` | `fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None)` | FACT con complemento combustible (IVA + PETROLEO). |
315
+ | `credit_note()` | `credit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de crédito (NCRE). |
316
+ | `debit_note()` | `debit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de débito (NDEB). |
317
+ | `credit_note_total()` | `credit_note_total(auth_number, issue_datetime, reason="...", reference="")` | Nota de crédito total. Devuelve `dict`. |
318
+ | `cancel()` | `cancel(auth_number, receiver_id, issue_datetime, reason="Anulación")` | Anula un DTE. Devuelve `dict`. |
319
+ | `lookup_nit()` | `lookup_nit(nit)` | Consulta SAT. Devuelve `{"nit","name","address","city","district","state"}`. |
320
+ | `get_dte()` | `get_dte(auth_number, fmt="JSON")` | Recupera el DTE (`"JSON"`, `"XML"`, `"HTML"`, `"PDF"`). |
321
+ | `get_dte_info()` | `get_dte_info(auth_number)` | Metadatos del DTE. |
322
+
323
+ ### Parámetros comunes
324
+
325
+ - **`buyer`**: `"CF"` (consumidor final), un NIT string (`"12345678"` — se consulta el nombre), un dict CUI (`{"type":"CUI","taxid":...,"name":...}`) o un dict NIT explícito (`{"taxid","name","address","city","district","state","country","email"}`).
326
+ - **`items`**: lista de dicts con `description` (str, req), `price` (float/Decimal, req), `qty` (1), `type` (`"Servicio"`/`"Bien"`), `unit_of_measure` (`"UNI"`), `discount` (opcional).
327
+ - **`origin`** (NCRE/NDEB): `{"auth_number": ..., "date": "YYYY-MM-DD", "series": ..., "number": ...}`.
328
+
329
+ ## Establecimiento (sucursal)
330
+
331
+ Cada NIT puede tener varios establecimientos registrados en el RTU. Configúralos al crear el cliente:
332
+
333
+ ```python
334
+ client = DigifactClient(
335
+ taxid="12345678",
336
+ username="FELUSER",
337
+ password="secret",
338
+ branch_code="2",
339
+ branch_name="SUCURSAL ZONA 10",
340
+ )
341
+ ```
342
+
343
+ Aplican a todos los DTE emitidos por ese cliente. Si se omiten, se usan los defaults `"1"` / `"ESTABLECIMIENTO PRINCIPAL"`.
344
+
345
+ ## Ejecutar las pruebas
346
+
347
+ ```bash
348
+ # Pruebas unitarias (no requieren credenciales)
349
+ python -m pytest tests/ -v
350
+
351
+ # Pruebas de integración
352
+ export DIGIFACT_TAXID=12345678
353
+ export DIGIFACT_USERNAME=FELUSER
354
+ export DIGIFACT_PASSWORD=tu_contraseña
355
+ python -m pytest tests/ -v
356
+ ```
357
+
358
+ ## Variables de entorno
359
+
360
+ | Variable | Descripción |
361
+ |----------|-------------|
362
+ | `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
363
+ | `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
364
+ | `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
365
+
366
+ ## Manejo de errores
367
+
368
+ ```python
369
+ from digifact_sdk import (
370
+ DigifactError, # base
371
+ DigifactAuthError, # fallo de autenticación
372
+ DigifactApiError, # error HTTP / de API
373
+ DigifactValidationError, # rechazo de SAT
374
+ DigifactNitNotFoundError, # NIT no encontrado
375
+ )
376
+
377
+ try:
378
+ result = client.invoice("CF", [...])
379
+ except DigifactValidationError as exc:
380
+ print(f"SAT rechazó: {exc}")
381
+ print(f"Código: {exc.code}")
382
+ print(f"Respuesta: {exc.raw}")
383
+ except DigifactError as exc:
384
+ print(f"Error del SDK: {exc}")
385
+ ```