digifact-sdk 2.2.1__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.
@@ -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
+ ```