digifact-sdk 2.2.2__tar.gz → 2.3.0__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 → digifact_sdk-2.3.0}/PKG-INFO +116 -7
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/README.md +115 -6
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk/builder.py +109 -6
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk/client.py +148 -31
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk.egg-info/PKG-INFO +116 -7
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/pyproject.toml +1 -1
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/tests/test_integration.py +140 -1
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk/__init__.py +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk/exceptions.py +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk/tax.py +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk.egg-info/SOURCES.txt +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk.egg-info/dependency_links.txt +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk.egg-info/requires.txt +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/digifact_sdk.egg-info/top_level.txt +0 -0
- {digifact_sdk-2.2.2 → digifact_sdk-2.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: digifact-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Python SDK for Digifact FEL Guatemala e-invoicing API
|
|
5
5
|
Author-email: AALONZOLU <noreply@github.com>
|
|
6
6
|
License: MIT
|
|
@@ -35,9 +35,34 @@ pip install digifact-sdk
|
|
|
35
35
|
O desde el código fuente:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pip install -e
|
|
38
|
+
pip install -e python/
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Configuración del cliente (`DigifactClient(...)`)
|
|
42
|
+
|
|
43
|
+
Ordenados de más usados a menos usados.
|
|
44
|
+
|
|
45
|
+
| Parámetro | Tipo | Por defecto | Descripción |
|
|
46
|
+
|-----------|------|-------------|-------------|
|
|
47
|
+
| `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. |
|
|
48
|
+
| `username` | `str` | **requerido** | Usuario corto de Digifact (la parte después de `GT.<NIT>.`, p. ej. `"FELUSER"`). |
|
|
49
|
+
| `password` | `str` | `""` | Contraseña de la cuenta. **Requerido** si no se provee `token`. |
|
|
50
|
+
| `token` | `str` | `""` | Bearer token preobtenido. Si se provee, se omite el login. |
|
|
51
|
+
| `environment` | `str` | `"test"` | `"test"` o `"production"`. |
|
|
52
|
+
| `seller_name` | `str` | `""` | Nombre del emisor. Para NIT individual es el nombre de la persona; para S.A. / S.E. es la razón social de la entidad. Si está vacío, se consulta en SAT vía `lookup_nit()`. |
|
|
53
|
+
| `seller_address` | `str` | `""` | Dirección del emisor. Si está vacía, se consulta en SAT. |
|
|
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** de la sucursal — el mismo que aparece en la patente de comercio. Se escribe en `Seller.BranchInfo.Name`. |
|
|
56
|
+
| `afiliacion_iva` | `str` | `"GEN"` | Afiliación IVA del RTU: `"GEN"`, `"PEQ"` o `"EXE"`. |
|
|
57
|
+
| `tipo_frase` | `str \| None` | `None` | Sobreescritura global de `TipoFrase` (legacy). **Mutuamente exclusivo con `frases`**. Ver [frases](#configuración-de-frases-tipofrase--codigoescenario). |
|
|
58
|
+
| `escenario` | `str \| None` | `None` | Sobreescritura global de `CodigoEscenario` (legacy). **Mutuamente exclusivo con `frases`**. |
|
|
59
|
+
| `frases` | `list[dict] \| None` | `None` | Lista de frases `{"tipo_frase": ..., "escenario": ...}`. Reemplaza a `tipo_frase`/`escenario`. **Mutuamente exclusivo** con ellos. Ver [frases](#configuración-de-frases-tipofrase--codigoescenario). |
|
|
60
|
+
| `auto_fuel_subsidy_frases` | `bool \| None` | `None` | Controla la auto-inyección de frases 9/18 y 9/19 en facturas de combustible durante el periodo de subsidio. `None` = usar default (`True`). Ver [subsidio combustible](#subsidio-combustible-frases-automaticas). |
|
|
61
|
+
| `petroleo_rates` | `dict[str, float] \| None` | `None` | Mapa código PETROLEO → tarifa por unidad. Usado sólo por `fuel_invoice()` (gasolineras). |
|
|
62
|
+
| `timeout` | `int` | `120` | Timeout HTTP en segundos. |
|
|
63
|
+
| `session` | `requests.Session \| None` | `None` | Sesión HTTP personalizada (útil para tests). |
|
|
64
|
+
| `tipo_personeria` | `str` | `"1"` | Código de `TipoPersoneria` del RTU. **Sólo aplica a RDON** (Recibo por Donación); ignóralo en los demás documentos. |
|
|
65
|
+
|
|
41
66
|
## Inicio rápido
|
|
42
67
|
|
|
43
68
|
```python
|
|
@@ -235,13 +260,36 @@ result = client.fuel_invoice(
|
|
|
235
260
|
| `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
|
|
236
261
|
| `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
262
|
|
|
263
|
+
### Subsidio combustible — frases automáticas
|
|
264
|
+
|
|
265
|
+
Durante el **periodo de subsidio de combustibles** (2026-04-27 (incl.) a 2026-07-27 (excl.)), SAT exige incluir frases especiales `TipoFrase=9, Escenario=18` y `TipoFrase=9, Escenario=19` en las facturas de combustible. **El SDK las agrega automáticamente** cuando la fecha de emisión cae dentro de la ventana — no se necesita cambiar ningún código existente.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
# Sin cambios — el SDK agrega 9/18 y 9/19 automáticamente durante el subsidio
|
|
269
|
+
result = client.fuel_invoice("CF", items)
|
|
270
|
+
|
|
271
|
+
# Deshabilitarlo por llamada
|
|
272
|
+
result = client.fuel_invoice("CF", items, auto_fuel_subsidy_frases=False)
|
|
273
|
+
|
|
274
|
+
# Deshabilitarlo globalmente al construir el cliente
|
|
275
|
+
client = DigifactClient(..., auto_fuel_subsidy_frases=False)
|
|
276
|
+
|
|
277
|
+
# Deshabilitarlo sin tocar código (útil si el presupuesto se agota antes de la fecha)
|
|
278
|
+
# DIGIFACT_DISABLE_AUTO_FUEL_SUBSIDY_FRASES=1 (variable de entorno)
|
|
279
|
+
|
|
280
|
+
# Frases completamente personalizadas (deshabilita auto-inyección)
|
|
281
|
+
result = client.fuel_invoice("CF", items, frases=[{"tipo_frase": "1", "escenario": "1"}])
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
> **Nota:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** — pasarlos juntos lanza `DigifactValidationError`.
|
|
285
|
+
|
|
238
286
|
## Configuración de frases (TipoFrase / CodigoEscenario)
|
|
239
287
|
|
|
240
288
|
Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
|
|
241
289
|
SDK elige valores por defecto adecuados, por lo que **no** hace falta
|
|
242
290
|
configurar nada en el caso común.
|
|
243
291
|
|
|
244
|
-
**Orden de precedencia:** argumentos por llamada → globales del constructor
|
|
292
|
+
**Orden de precedencia:** argumentos por llamada → globales del constructor → tabla de valores por defecto.
|
|
245
293
|
|
|
246
294
|
**Tabla de valores por defecto:**
|
|
247
295
|
|
|
@@ -256,10 +304,30 @@ configurar nada en el caso común.
|
|
|
256
304
|
| FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
|
|
257
305
|
| FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
|
|
258
306
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
307
|
+
### Nueva API: `frases[]` (múltiples frases)
|
|
308
|
+
|
|
309
|
+
Usa `frases` cuando necesitas enviar **más de un par** TipoFrase/Escenario (p. ej. listados en resoluciones SAT). Es **mutuamente exclusivo** con `tipo_frase`/`escenario`.
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
# Lista explícita de frases (deshabilita tipo_frase/escenario y auto-inyección)
|
|
313
|
+
client.fuel_invoice("CF", items, frases=[
|
|
314
|
+
{"tipo_frase": "1", "escenario": "1"},
|
|
315
|
+
{"tipo_frase": "9", "escenario": "18"},
|
|
316
|
+
{"tipo_frase": "9", "escenario": "19"},
|
|
317
|
+
])
|
|
318
|
+
|
|
319
|
+
# Globalmente en el constructor
|
|
320
|
+
client = DigifactClient(
|
|
321
|
+
taxid="12345678", username="FELUSER", password="...",
|
|
322
|
+
frases=[{"tipo_frase": "1", "escenario": "2"}], # ISR régimen opcional simplificado
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> **Regla:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** en cada nivel (por llamada y en el constructor). Combinarlos lanza `DigifactValidationError`.
|
|
327
|
+
|
|
328
|
+
### API legacy: `tipo_frase` / `escenario`
|
|
329
|
+
|
|
330
|
+
Ambos parámetros siguen funcionando exactamente como antes para integraciones existentes.
|
|
263
331
|
|
|
264
332
|
```python
|
|
265
333
|
# Sobreescritura por llamada (uno o ambos)
|
|
@@ -282,6 +350,45 @@ client = DigifactClient(
|
|
|
282
350
|
Para descubrir el par correcto en un caso particular, revisa las afiliaciones
|
|
283
351
|
del RTU en el portal de SAT.
|
|
284
352
|
|
|
353
|
+
## Referencia de métodos
|
|
354
|
+
|
|
355
|
+
Todos los métodos de emisión devuelven `DteResult` con `result.auth_number`, `series`, `number`, `issue_datetime`, `raw`.
|
|
356
|
+
|
|
357
|
+
| Método | Firma | Descripción |
|
|
358
|
+
|--------|-------|-------------|
|
|
359
|
+
| `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`. |
|
|
360
|
+
| `cca_invoice()` | `cca_invoice(buyer, items, cobros, *, tipo_frase=None, escenario=None)` | FACT con complemento CCA. |
|
|
361
|
+
| `fuel_invoice()` | `fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None, frases=None, auto_fuel_subsidy_frases=None)` | FACT con complemento combustible (IVA + PETROLEO). Auto-inyecta frases 9/18 y 9/19 durante el subsidio. |
|
|
362
|
+
| `credit_note()` | `credit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de crédito (NCRE). |
|
|
363
|
+
| `debit_note()` | `debit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de débito (NDEB). |
|
|
364
|
+
| `credit_note_total()` | `credit_note_total(auth_number, issue_datetime, reason="...", reference="")` | Nota de crédito total. Devuelve `dict`. |
|
|
365
|
+
| `cancel()` | `cancel(auth_number, receiver_id, issue_datetime, reason="Anulación")` | Anula un DTE. Devuelve `dict`. |
|
|
366
|
+
| `lookup_nit()` | `lookup_nit(nit)` | Consulta SAT. Devuelve `{"nit","name","address","city","district","state"}`. |
|
|
367
|
+
| `get_dte()` | `get_dte(auth_number, fmt="JSON")` | Recupera el DTE (`"JSON"`, `"XML"`, `"HTML"`, `"PDF"`). |
|
|
368
|
+
| `get_dte_info()` | `get_dte_info(auth_number)` | Metadatos del DTE. |
|
|
369
|
+
|
|
370
|
+
### Parámetros comunes
|
|
371
|
+
|
|
372
|
+
- **`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"}`).
|
|
373
|
+
- **`items`**: lista de dicts con `description` (str, req), `price` (float/Decimal, req), `qty` (1), `type` (`"Servicio"`/`"Bien"`), `unit_of_measure` (`"UNI"`), `discount` (opcional).
|
|
374
|
+
- **`origin`** (NCRE/NDEB): `{"auth_number": ..., "date": "YYYY-MM-DD", "series": ..., "number": ...}`.
|
|
375
|
+
|
|
376
|
+
## Establecimiento (sucursal)
|
|
377
|
+
|
|
378
|
+
Cada NIT puede tener varios establecimientos registrados en el RTU. Configúralos al crear el cliente:
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
client = DigifactClient(
|
|
382
|
+
taxid="12345678",
|
|
383
|
+
username="FELUSER",
|
|
384
|
+
password="secret",
|
|
385
|
+
branch_code="2",
|
|
386
|
+
branch_name="SUCURSAL ZONA 10",
|
|
387
|
+
)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Aplican a todos los DTE emitidos por ese cliente. Si se omiten, se usan los defaults `"1"` / `"ESTABLECIMIENTO PRINCIPAL"`.
|
|
391
|
+
|
|
285
392
|
## Ejecutar las pruebas
|
|
286
393
|
|
|
287
394
|
```bash
|
|
@@ -302,6 +409,8 @@ python -m pytest tests/ -v
|
|
|
302
409
|
| `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
|
|
303
410
|
| `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
|
|
304
411
|
| `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
|
|
412
|
+
| `DIGIFACT_ENVIRONMENT` | `test` (default) o `production` |
|
|
413
|
+
| `DIGIFACT_DISABLE_AUTO_FUEL_SUBSIDY_FRASES` | `1` para deshabilitar la auto-inyección de frases 9/18 y 9/19 sin tocar código |
|
|
305
414
|
|
|
306
415
|
## Manejo de errores
|
|
307
416
|
|
|
@@ -11,9 +11,34 @@ pip install digifact-sdk
|
|
|
11
11
|
O desde el código fuente:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
pip install -e
|
|
14
|
+
pip install -e python/
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Configuración del cliente (`DigifactClient(...)`)
|
|
18
|
+
|
|
19
|
+
Ordenados de más usados a menos usados.
|
|
20
|
+
|
|
21
|
+
| Parámetro | Tipo | Por defecto | Descripción |
|
|
22
|
+
|-----------|------|-------------|-------------|
|
|
23
|
+
| `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. |
|
|
24
|
+
| `username` | `str` | **requerido** | Usuario corto de Digifact (la parte después de `GT.<NIT>.`, p. ej. `"FELUSER"`). |
|
|
25
|
+
| `password` | `str` | `""` | Contraseña de la cuenta. **Requerido** si no se provee `token`. |
|
|
26
|
+
| `token` | `str` | `""` | Bearer token preobtenido. Si se provee, se omite el login. |
|
|
27
|
+
| `environment` | `str` | `"test"` | `"test"` o `"production"`. |
|
|
28
|
+
| `seller_name` | `str` | `""` | Nombre del emisor. Para NIT individual es el nombre de la persona; para S.A. / S.E. es la razón social de la entidad. Si está vacío, se consulta en SAT vía `lookup_nit()`. |
|
|
29
|
+
| `seller_address` | `str` | `""` | Dirección del emisor. Si está vacía, se consulta en SAT. |
|
|
30
|
+
| `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`. |
|
|
31
|
+
| `branch_name` | `str` | `"ESTABLECIMIENTO PRINCIPAL"` | **Nombre comercial** de la sucursal — el mismo que aparece en la patente de comercio. Se escribe en `Seller.BranchInfo.Name`. |
|
|
32
|
+
| `afiliacion_iva` | `str` | `"GEN"` | Afiliación IVA del RTU: `"GEN"`, `"PEQ"` o `"EXE"`. |
|
|
33
|
+
| `tipo_frase` | `str \| None` | `None` | Sobreescritura global de `TipoFrase` (legacy). **Mutuamente exclusivo con `frases`**. Ver [frases](#configuración-de-frases-tipofrase--codigoescenario). |
|
|
34
|
+
| `escenario` | `str \| None` | `None` | Sobreescritura global de `CodigoEscenario` (legacy). **Mutuamente exclusivo con `frases`**. |
|
|
35
|
+
| `frases` | `list[dict] \| None` | `None` | Lista de frases `{"tipo_frase": ..., "escenario": ...}`. Reemplaza a `tipo_frase`/`escenario`. **Mutuamente exclusivo** con ellos. Ver [frases](#configuración-de-frases-tipofrase--codigoescenario). |
|
|
36
|
+
| `auto_fuel_subsidy_frases` | `bool \| None` | `None` | Controla la auto-inyección de frases 9/18 y 9/19 en facturas de combustible durante el periodo de subsidio. `None` = usar default (`True`). Ver [subsidio combustible](#subsidio-combustible-frases-automaticas). |
|
|
37
|
+
| `petroleo_rates` | `dict[str, float] \| None` | `None` | Mapa código PETROLEO → tarifa por unidad. Usado sólo por `fuel_invoice()` (gasolineras). |
|
|
38
|
+
| `timeout` | `int` | `120` | Timeout HTTP en segundos. |
|
|
39
|
+
| `session` | `requests.Session \| None` | `None` | Sesión HTTP personalizada (útil para tests). |
|
|
40
|
+
| `tipo_personeria` | `str` | `"1"` | Código de `TipoPersoneria` del RTU. **Sólo aplica a RDON** (Recibo por Donación); ignóralo en los demás documentos. |
|
|
41
|
+
|
|
17
42
|
## Inicio rápido
|
|
18
43
|
|
|
19
44
|
```python
|
|
@@ -211,13 +236,36 @@ result = client.fuel_invoice(
|
|
|
211
236
|
| `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
|
|
212
237
|
| `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
238
|
|
|
239
|
+
### Subsidio combustible — frases automáticas
|
|
240
|
+
|
|
241
|
+
Durante el **periodo de subsidio de combustibles** (2026-04-27 (incl.) a 2026-07-27 (excl.)), SAT exige incluir frases especiales `TipoFrase=9, Escenario=18` y `TipoFrase=9, Escenario=19` en las facturas de combustible. **El SDK las agrega automáticamente** cuando la fecha de emisión cae dentro de la ventana — no se necesita cambiar ningún código existente.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
# Sin cambios — el SDK agrega 9/18 y 9/19 automáticamente durante el subsidio
|
|
245
|
+
result = client.fuel_invoice("CF", items)
|
|
246
|
+
|
|
247
|
+
# Deshabilitarlo por llamada
|
|
248
|
+
result = client.fuel_invoice("CF", items, auto_fuel_subsidy_frases=False)
|
|
249
|
+
|
|
250
|
+
# Deshabilitarlo globalmente al construir el cliente
|
|
251
|
+
client = DigifactClient(..., auto_fuel_subsidy_frases=False)
|
|
252
|
+
|
|
253
|
+
# Deshabilitarlo sin tocar código (útil si el presupuesto se agota antes de la fecha)
|
|
254
|
+
# DIGIFACT_DISABLE_AUTO_FUEL_SUBSIDY_FRASES=1 (variable de entorno)
|
|
255
|
+
|
|
256
|
+
# Frases completamente personalizadas (deshabilita auto-inyección)
|
|
257
|
+
result = client.fuel_invoice("CF", items, frases=[{"tipo_frase": "1", "escenario": "1"}])
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
> **Nota:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** — pasarlos juntos lanza `DigifactValidationError`.
|
|
261
|
+
|
|
214
262
|
## Configuración de frases (TipoFrase / CodigoEscenario)
|
|
215
263
|
|
|
216
264
|
Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
|
|
217
265
|
SDK elige valores por defecto adecuados, por lo que **no** hace falta
|
|
218
266
|
configurar nada en el caso común.
|
|
219
267
|
|
|
220
|
-
**Orden de precedencia:** argumentos por llamada → globales del constructor
|
|
268
|
+
**Orden de precedencia:** argumentos por llamada → globales del constructor → tabla de valores por defecto.
|
|
221
269
|
|
|
222
270
|
**Tabla de valores por defecto:**
|
|
223
271
|
|
|
@@ -232,10 +280,30 @@ configurar nada en el caso común.
|
|
|
232
280
|
| FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
|
|
233
281
|
| FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
|
|
234
282
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
283
|
+
### Nueva API: `frases[]` (múltiples frases)
|
|
284
|
+
|
|
285
|
+
Usa `frases` cuando necesitas enviar **más de un par** TipoFrase/Escenario (p. ej. listados en resoluciones SAT). Es **mutuamente exclusivo** con `tipo_frase`/`escenario`.
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
# Lista explícita de frases (deshabilita tipo_frase/escenario y auto-inyección)
|
|
289
|
+
client.fuel_invoice("CF", items, frases=[
|
|
290
|
+
{"tipo_frase": "1", "escenario": "1"},
|
|
291
|
+
{"tipo_frase": "9", "escenario": "18"},
|
|
292
|
+
{"tipo_frase": "9", "escenario": "19"},
|
|
293
|
+
])
|
|
294
|
+
|
|
295
|
+
# Globalmente en el constructor
|
|
296
|
+
client = DigifactClient(
|
|
297
|
+
taxid="12345678", username="FELUSER", password="...",
|
|
298
|
+
frases=[{"tipo_frase": "1", "escenario": "2"}], # ISR régimen opcional simplificado
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
> **Regla:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** en cada nivel (por llamada y en el constructor). Combinarlos lanza `DigifactValidationError`.
|
|
303
|
+
|
|
304
|
+
### API legacy: `tipo_frase` / `escenario`
|
|
305
|
+
|
|
306
|
+
Ambos parámetros siguen funcionando exactamente como antes para integraciones existentes.
|
|
239
307
|
|
|
240
308
|
```python
|
|
241
309
|
# Sobreescritura por llamada (uno o ambos)
|
|
@@ -258,6 +326,45 @@ client = DigifactClient(
|
|
|
258
326
|
Para descubrir el par correcto en un caso particular, revisa las afiliaciones
|
|
259
327
|
del RTU en el portal de SAT.
|
|
260
328
|
|
|
329
|
+
## Referencia de métodos
|
|
330
|
+
|
|
331
|
+
Todos los métodos de emisión devuelven `DteResult` con `result.auth_number`, `series`, `number`, `issue_datetime`, `raw`.
|
|
332
|
+
|
|
333
|
+
| Método | Firma | Descripción |
|
|
334
|
+
|--------|-------|-------------|
|
|
335
|
+
| `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`. |
|
|
336
|
+
| `cca_invoice()` | `cca_invoice(buyer, items, cobros, *, tipo_frase=None, escenario=None)` | FACT con complemento CCA. |
|
|
337
|
+
| `fuel_invoice()` | `fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None, frases=None, auto_fuel_subsidy_frases=None)` | FACT con complemento combustible (IVA + PETROLEO). Auto-inyecta frases 9/18 y 9/19 durante el subsidio. |
|
|
338
|
+
| `credit_note()` | `credit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de crédito (NCRE). |
|
|
339
|
+
| `debit_note()` | `debit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de débito (NDEB). |
|
|
340
|
+
| `credit_note_total()` | `credit_note_total(auth_number, issue_datetime, reason="...", reference="")` | Nota de crédito total. Devuelve `dict`. |
|
|
341
|
+
| `cancel()` | `cancel(auth_number, receiver_id, issue_datetime, reason="Anulación")` | Anula un DTE. Devuelve `dict`. |
|
|
342
|
+
| `lookup_nit()` | `lookup_nit(nit)` | Consulta SAT. Devuelve `{"nit","name","address","city","district","state"}`. |
|
|
343
|
+
| `get_dte()` | `get_dte(auth_number, fmt="JSON")` | Recupera el DTE (`"JSON"`, `"XML"`, `"HTML"`, `"PDF"`). |
|
|
344
|
+
| `get_dte_info()` | `get_dte_info(auth_number)` | Metadatos del DTE. |
|
|
345
|
+
|
|
346
|
+
### Parámetros comunes
|
|
347
|
+
|
|
348
|
+
- **`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"}`).
|
|
349
|
+
- **`items`**: lista de dicts con `description` (str, req), `price` (float/Decimal, req), `qty` (1), `type` (`"Servicio"`/`"Bien"`), `unit_of_measure` (`"UNI"`), `discount` (opcional).
|
|
350
|
+
- **`origin`** (NCRE/NDEB): `{"auth_number": ..., "date": "YYYY-MM-DD", "series": ..., "number": ...}`.
|
|
351
|
+
|
|
352
|
+
## Establecimiento (sucursal)
|
|
353
|
+
|
|
354
|
+
Cada NIT puede tener varios establecimientos registrados en el RTU. Configúralos al crear el cliente:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
client = DigifactClient(
|
|
358
|
+
taxid="12345678",
|
|
359
|
+
username="FELUSER",
|
|
360
|
+
password="secret",
|
|
361
|
+
branch_code="2",
|
|
362
|
+
branch_name="SUCURSAL ZONA 10",
|
|
363
|
+
)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Aplican a todos los DTE emitidos por ese cliente. Si se omiten, se usan los defaults `"1"` / `"ESTABLECIMIENTO PRINCIPAL"`.
|
|
367
|
+
|
|
261
368
|
## Ejecutar las pruebas
|
|
262
369
|
|
|
263
370
|
```bash
|
|
@@ -278,6 +385,8 @@ python -m pytest tests/ -v
|
|
|
278
385
|
| `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
|
|
279
386
|
| `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
|
|
280
387
|
| `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
|
|
388
|
+
| `DIGIFACT_ENVIRONMENT` | `test` (default) o `production` |
|
|
389
|
+
| `DIGIFACT_DISABLE_AUTO_FUEL_SUBSIDY_FRASES` | `1` para deshabilitar la auto-inyección de frases 9/18 y 9/19 sin tocar código |
|
|
281
390
|
|
|
282
391
|
## Manejo de errores
|
|
283
392
|
|
|
@@ -7,6 +7,7 @@ Key intentional API typos preserved from the SAT/Digifact spec:
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import datetime as _datetime
|
|
10
11
|
from decimal import Decimal
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -27,6 +28,69 @@ def _adenda_code(doc_type: str) -> str:
|
|
|
27
28
|
return ADENDA_CODE_ALT if doc_type in _ALT_ADENDA_TYPES else ADENDA_CODE_STD
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
# ── Fuel subsidy frases (Tipo 9, Escenarios 18 / 19) ─────────────────────────
|
|
32
|
+
FUEL_SUBSIDY_START = _datetime.date(2026, 4, 27)
|
|
33
|
+
FUEL_SUBSIDY_END = _datetime.date(2026, 7, 27) # exclusive
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _uniq_frases(frases: list[dict]) -> list[dict]:
|
|
37
|
+
seen: set[str] = set()
|
|
38
|
+
out: list[dict] = []
|
|
39
|
+
for f in frases:
|
|
40
|
+
key = f"{f['tipo_frase']}:{f['escenario']}"
|
|
41
|
+
if key not in seen:
|
|
42
|
+
seen.add(key)
|
|
43
|
+
out.append(f)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_additionl_info_from_frases(frases: list[dict]) -> list[dict]:
|
|
48
|
+
"""Build Seller.AdditionlInfo entries from a frases list.
|
|
49
|
+
|
|
50
|
+
Each frase gets a 1-based Data index so Digifact's NUC XSLT groups them into
|
|
51
|
+
separate <dte:Frase> elements. Confirmed via live API: Digifact counts the frases
|
|
52
|
+
correctly with this format (error becomes a NIT-authorization check, not XSLT failure).
|
|
53
|
+
"""
|
|
54
|
+
out: list[dict] = []
|
|
55
|
+
for i, f in enumerate(frases, start=1):
|
|
56
|
+
out.append({"Name": "TipoFrase", "Data": str(i), "Value": str(f["tipo_frase"])})
|
|
57
|
+
out.append({"Name": "Escenario", "Data": str(i), "Value": str(f["escenario"])})
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _within_subsidy_window(issue_dt_iso: str) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
d = _datetime.date.fromisoformat(issue_dt_iso[:10])
|
|
64
|
+
return FUEL_SUBSIDY_START <= d < FUEL_SUBSIDY_END
|
|
65
|
+
except (ValueError, TypeError):
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resolve_fuel_frases(
|
|
70
|
+
frases: list[dict] | None,
|
|
71
|
+
tipo_frase: str | None,
|
|
72
|
+
escenario: str | None,
|
|
73
|
+
issue_dt_iso: str,
|
|
74
|
+
auto_enabled: bool = True,
|
|
75
|
+
) -> list[dict]:
|
|
76
|
+
"""Return the final deduplicated frases list for a fuel invoice.
|
|
77
|
+
|
|
78
|
+
Called only after mutual-exclusivity validation (frases vs tipo_frase/escenario).
|
|
79
|
+
"""
|
|
80
|
+
if frases is not None:
|
|
81
|
+
if not frases:
|
|
82
|
+
from .exceptions import DigifactValidationError
|
|
83
|
+
raise DigifactValidationError("frases must contain at least one TipoFrase/Escenario pair")
|
|
84
|
+
return _uniq_frases(frases)
|
|
85
|
+
result: list[dict] = []
|
|
86
|
+
if tipo_frase is not None and escenario is not None:
|
|
87
|
+
result.append({"tipo_frase": tipo_frase, "escenario": escenario})
|
|
88
|
+
if auto_enabled and _within_subsidy_window(issue_dt_iso):
|
|
89
|
+
result.append({"tipo_frase": "9", "escenario": "18"})
|
|
90
|
+
result.append({"tipo_frase": "9", "escenario": "19"})
|
|
91
|
+
return _uniq_frases(result)
|
|
92
|
+
|
|
93
|
+
|
|
30
94
|
def default_frase(doc_type: str, afiliacion: str = "GEN") -> tuple[str, str] | None:
|
|
31
95
|
"""Return default (TipoFrase, CodigoEscenario) for a (doc_type, afiliacion) combo.
|
|
32
96
|
|
|
@@ -68,6 +132,7 @@ def _build_seller(
|
|
|
68
132
|
afiliacion: str = "GEN",
|
|
69
133
|
tipo_frase: str | None = "1",
|
|
70
134
|
escenario: str | None = "1",
|
|
135
|
+
frases: list[dict] | None = None,
|
|
71
136
|
branch_code: str = "1",
|
|
72
137
|
branch_name: str = "ESTABLECIMIENTO PRINCIPAL",
|
|
73
138
|
city: str = "01010",
|
|
@@ -96,8 +161,12 @@ def _build_seller(
|
|
|
96
161
|
}
|
|
97
162
|
if email:
|
|
98
163
|
seller["Contact"] = {"EmailList": {"Email": [email]}}
|
|
99
|
-
# AdditionlInfo — intentional typo in API spec
|
|
100
|
-
|
|
164
|
+
# AdditionlInfo — intentional typo in API spec.
|
|
165
|
+
# Digifact's XSLT groups entries by 1-based Data index into separate <dte:Frase> elements.
|
|
166
|
+
if frases is not None:
|
|
167
|
+
if frases:
|
|
168
|
+
seller["AdditionlInfo"] = _build_additionl_info_from_frases(frases)
|
|
169
|
+
elif tipo_frase is not None and escenario is not None:
|
|
101
170
|
seller["AdditionlInfo"] = [
|
|
102
171
|
{"Name": "TipoFrase", "Data": "1", "Value": tipo_frase},
|
|
103
172
|
{"Name": "Escenario", "Data": "1", "Value": escenario},
|
|
@@ -306,6 +375,7 @@ def build_fact(
|
|
|
306
375
|
afiliacion: str = "GEN",
|
|
307
376
|
tipo_frase: str | None = None,
|
|
308
377
|
escenario: str | None = None,
|
|
378
|
+
frases: list[dict] | None = None,
|
|
309
379
|
amount_str: str = "",
|
|
310
380
|
observaciones: str = "-",
|
|
311
381
|
extra_header: dict | None = None,
|
|
@@ -329,6 +399,7 @@ def build_fact(
|
|
|
329
399
|
afiliacion=afiliacion,
|
|
330
400
|
tipo_frase=tf,
|
|
331
401
|
escenario=es,
|
|
402
|
+
frases=frases,
|
|
332
403
|
email=seller_email,
|
|
333
404
|
)
|
|
334
405
|
|
|
@@ -371,6 +442,7 @@ def build_fcam(
|
|
|
371
442
|
seller_email: str | None = None,
|
|
372
443
|
tipo_frase: str | None = None,
|
|
373
444
|
escenario: str | None = None,
|
|
445
|
+
frases: list[dict] | None = None,
|
|
374
446
|
) -> dict:
|
|
375
447
|
"""Build a FCAM (Factura Cambiaria) DTE payload.
|
|
376
448
|
|
|
@@ -391,6 +463,7 @@ def build_fcam(
|
|
|
391
463
|
afiliacion=afiliacion,
|
|
392
464
|
tipo_frase=tf,
|
|
393
465
|
escenario=es,
|
|
466
|
+
frases=frases,
|
|
394
467
|
email=seller_email,
|
|
395
468
|
)
|
|
396
469
|
|
|
@@ -443,6 +516,7 @@ def build_ndeb(
|
|
|
443
516
|
seller_email: str | None = None,
|
|
444
517
|
tipo_frase: str | None = None,
|
|
445
518
|
escenario: str | None = None,
|
|
519
|
+
frases: list[dict] | None = None,
|
|
446
520
|
) -> dict:
|
|
447
521
|
"""Build a NDEB (Nota de Débito) DTE payload.
|
|
448
522
|
|
|
@@ -462,6 +536,7 @@ def build_ndeb(
|
|
|
462
536
|
afiliacion=afiliacion,
|
|
463
537
|
tipo_frase=tf,
|
|
464
538
|
escenario=es,
|
|
539
|
+
frases=frases,
|
|
465
540
|
email=seller_email,
|
|
466
541
|
)
|
|
467
542
|
|
|
@@ -527,6 +602,7 @@ def build_ncre(
|
|
|
527
602
|
seller_email: str | None = None,
|
|
528
603
|
tipo_frase: str | None = None,
|
|
529
604
|
escenario: str | None = None,
|
|
605
|
+
frases: list[dict] | None = None,
|
|
530
606
|
) -> dict:
|
|
531
607
|
"""Build a NCRE (Nota de Crédito) DTE payload.
|
|
532
608
|
|
|
@@ -546,6 +622,7 @@ def build_ncre(
|
|
|
546
622
|
afiliacion=afiliacion,
|
|
547
623
|
tipo_frase=tf,
|
|
548
624
|
escenario=es,
|
|
625
|
+
frases=frases,
|
|
549
626
|
email=seller_email,
|
|
550
627
|
)
|
|
551
628
|
|
|
@@ -609,6 +686,7 @@ def build_nabn(
|
|
|
609
686
|
amount_str: str = "",
|
|
610
687
|
observaciones: str = "-",
|
|
611
688
|
seller_email: str | None = None,
|
|
689
|
+
frases: list[dict] | None = None,
|
|
612
690
|
) -> dict:
|
|
613
691
|
"""Build a NABN (Nota de Abono) DTE payload — no IVA."""
|
|
614
692
|
issue_dt, _, _ = gt_now()
|
|
@@ -622,6 +700,7 @@ def build_nabn(
|
|
|
622
700
|
afiliacion=afiliacion,
|
|
623
701
|
tipo_frase="1",
|
|
624
702
|
escenario="1",
|
|
703
|
+
frases=frases,
|
|
625
704
|
email=seller_email,
|
|
626
705
|
)
|
|
627
706
|
|
|
@@ -718,6 +797,7 @@ def build_rdon(
|
|
|
718
797
|
amount_str: str = "",
|
|
719
798
|
observaciones: str = "-",
|
|
720
799
|
seller_email: str | None = None,
|
|
800
|
+
frases: list[dict] | None = None,
|
|
721
801
|
) -> dict:
|
|
722
802
|
"""Build a RDON (Recibo por Donación) DTE payload — no IVA."""
|
|
723
803
|
issue_dt, _, _ = gt_now()
|
|
@@ -730,6 +810,7 @@ def build_rdon(
|
|
|
730
810
|
afiliacion=afiliacion,
|
|
731
811
|
tipo_frase="4",
|
|
732
812
|
escenario="4",
|
|
813
|
+
frases=frases,
|
|
733
814
|
email=seller_email,
|
|
734
815
|
)
|
|
735
816
|
|
|
@@ -769,6 +850,7 @@ def build_fpeq(
|
|
|
769
850
|
seller_email: str | None = None,
|
|
770
851
|
tipo_frase: str | None = None,
|
|
771
852
|
escenario: str | None = None,
|
|
853
|
+
frases: list[dict] | None = None,
|
|
772
854
|
) -> dict:
|
|
773
855
|
"""Build a FPEQ (Factura Pequeño Contribuyente) DTE payload — no IVA, AfiliacionIVA=PEQ."""
|
|
774
856
|
issue_dt, _, _ = gt_now()
|
|
@@ -785,6 +867,7 @@ def build_fpeq(
|
|
|
785
867
|
afiliacion="PEQ",
|
|
786
868
|
tipo_frase=tf,
|
|
787
869
|
escenario=es,
|
|
870
|
+
frases=frases,
|
|
788
871
|
email=seller_email,
|
|
789
872
|
)
|
|
790
873
|
|
|
@@ -819,6 +902,7 @@ def build_reci(
|
|
|
819
902
|
student_id: str = "000000000",
|
|
820
903
|
academic_unit: str = "01",
|
|
821
904
|
seller_email: str | None = None,
|
|
905
|
+
frases: list[dict] | None = None,
|
|
822
906
|
) -> dict:
|
|
823
907
|
"""Build a RECI (Recibo) DTE payload — no IVA, universidad ADENDA."""
|
|
824
908
|
issue_dt, _, _ = gt_now()
|
|
@@ -831,6 +915,7 @@ def build_reci(
|
|
|
831
915
|
afiliacion=afiliacion,
|
|
832
916
|
tipo_frase="4",
|
|
833
917
|
escenario="5",
|
|
918
|
+
frases=frases,
|
|
834
919
|
email=seller_email,
|
|
835
920
|
)
|
|
836
921
|
|
|
@@ -873,6 +958,7 @@ def build_cca(
|
|
|
873
958
|
seller_email: str | None = None,
|
|
874
959
|
tipo_frase: str | None = None,
|
|
875
960
|
escenario: str | None = None,
|
|
961
|
+
frases: list[dict] | None = None,
|
|
876
962
|
) -> dict:
|
|
877
963
|
"""Build a CCA (Cobro por Cuenta Ajena) FACT+CCA complemento payload.
|
|
878
964
|
|
|
@@ -902,6 +988,7 @@ def build_cca(
|
|
|
902
988
|
afiliacion=afiliacion,
|
|
903
989
|
tipo_frase=tf,
|
|
904
990
|
escenario=es,
|
|
991
|
+
frases=frases,
|
|
905
992
|
email=seller_email,
|
|
906
993
|
)
|
|
907
994
|
|
|
@@ -1095,6 +1182,8 @@ def build_fact_combustible(
|
|
|
1095
1182
|
afiliacion: str = "GEN",
|
|
1096
1183
|
tipo_frase: str | None = None,
|
|
1097
1184
|
escenario: str | None = None,
|
|
1185
|
+
frases: list[dict] | None = None,
|
|
1186
|
+
auto_fuel_subsidy_frases: bool = True,
|
|
1098
1187
|
seller_email: str | None = None,
|
|
1099
1188
|
) -> dict:
|
|
1100
1189
|
"""Build a FACT payload for combustible (fuel) invoices.
|
|
@@ -1104,6 +1193,8 @@ def build_fact_combustible(
|
|
|
1104
1193
|
"4"=DIESEL; default "1"). Items without ``petroleo_amount`` are treated as
|
|
1105
1194
|
regular IVA-only items and may coexist in the same invoice.
|
|
1106
1195
|
|
|
1196
|
+
``frases`` and ``tipo_frase``/``escenario`` are mutually exclusive.
|
|
1197
|
+
|
|
1107
1198
|
Example fuel item::
|
|
1108
1199
|
|
|
1109
1200
|
{"description": "GASOLINA SUPER", "qty": 1, "price": 30.30,
|
|
@@ -1113,6 +1204,12 @@ def build_fact_combustible(
|
|
|
1113
1204
|
|
|
1114
1205
|
{"description": "FILTRO DE ACEITE", "qty": 1, "price": 45.00, "type": "Bien"}
|
|
1115
1206
|
"""
|
|
1207
|
+
from .exceptions import DigifactValidationError
|
|
1208
|
+
if frases is not None and (tipo_frase is not None or escenario is not None):
|
|
1209
|
+
raise DigifactValidationError(
|
|
1210
|
+
"frases and tipo_frase/escenario are mutually exclusive; use one or the other"
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1116
1213
|
issue_dt, _, _ = gt_now()
|
|
1117
1214
|
|
|
1118
1215
|
line_items, grand_total, total_iva, total_petroleo = _build_fuel_items(items)
|
|
@@ -1121,13 +1218,16 @@ def build_fact_combustible(
|
|
|
1121
1218
|
tf = tipo_frase if tipo_frase is not None else def_tf
|
|
1122
1219
|
es = escenario if escenario is not None else def_es
|
|
1123
1220
|
|
|
1221
|
+
resolved_frases = resolve_fuel_frases(frases, tf, es, issue_dt, auto_fuel_subsidy_frases)
|
|
1222
|
+
|
|
1223
|
+
# All frases (base + any subsidy) go into Seller.AdditionlInfo as repeated pairs —
|
|
1224
|
+
# that is the field the API reads to generate <dte:Frases> in the certified XML.
|
|
1124
1225
|
seller = _build_seller(
|
|
1125
1226
|
taxid,
|
|
1126
1227
|
seller_name,
|
|
1127
1228
|
seller_address,
|
|
1128
1229
|
afiliacion=afiliacion,
|
|
1129
|
-
|
|
1130
|
-
escenario=es,
|
|
1230
|
+
frases=resolved_frases,
|
|
1131
1231
|
email=seller_email,
|
|
1132
1232
|
)
|
|
1133
1233
|
|
|
@@ -1135,12 +1235,14 @@ def build_fact_combustible(
|
|
|
1135
1235
|
if float(total_petroleo) > 0.0:
|
|
1136
1236
|
total_tax.append({"Description": "PETROLEO", "Amount": total_petroleo})
|
|
1137
1237
|
|
|
1138
|
-
|
|
1238
|
+
combustible_payload: dict[str, Any] = {
|
|
1139
1239
|
"Version": "1.00",
|
|
1140
1240
|
"CountryCode": "GT",
|
|
1141
1241
|
"Header": {"DocType": "FACT", "IssuedDateTime": issue_dt, "Currency": "GTQ"},
|
|
1142
1242
|
"Seller": seller,
|
|
1143
1243
|
"Buyer": buyer,
|
|
1244
|
+
}
|
|
1245
|
+
combustible_payload.update({
|
|
1144
1246
|
"ThirdParties": None,
|
|
1145
1247
|
"Items": line_items,
|
|
1146
1248
|
"Totals": {
|
|
@@ -1148,4 +1250,5 @@ def build_fact_combustible(
|
|
|
1148
1250
|
"GrandTotal": {"InvoiceTotal": grand_total},
|
|
1149
1251
|
},
|
|
1150
1252
|
"AdditionalDocumentInfo": _build_fuel_adenda(),
|
|
1151
|
-
}
|
|
1253
|
+
})
|
|
1254
|
+
return combustible_payload
|