digifact-sdk 2.2.3__tar.gz → 3.0.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.3 → digifact_sdk-3.0.0}/PKG-INFO +83 -18
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/README.md +82 -17
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk/__init__.py +1 -1
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk/builder.py +92 -6
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk/client.py +174 -14
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk.egg-info/PKG-INFO +83 -18
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/pyproject.toml +1 -1
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/tests/test_integration.py +159 -12
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk/exceptions.py +0 -0
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk/tax.py +0 -0
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk.egg-info/SOURCES.txt +0 -0
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk.egg-info/dependency_links.txt +0 -0
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk.egg-info/requires.txt +0 -0
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/digifact_sdk.egg-info/top_level.txt +0 -0
- {digifact_sdk-2.2.3 → digifact_sdk-3.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: digifact-sdk
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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,29 +35,32 @@ 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
41
|
## Configuración del cliente (`DigifactClient(...)`)
|
|
42
42
|
|
|
43
|
+
Ordenados de más usados a menos usados.
|
|
44
|
+
|
|
43
45
|
| Parámetro | Tipo | Por defecto | Descripción |
|
|
44
46
|
|-----------|------|-------------|-------------|
|
|
45
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. |
|
|
46
48
|
| `username` | `str` | **requerido** | Usuario corto de Digifact (la parte después de `GT.<NIT>.`, p. ej. `"FELUSER"`). |
|
|
47
49
|
| `password` | `str` | `""` | Contraseña de la cuenta. **Requerido** si no se provee `token`. |
|
|
48
|
-
| `environment` | `str` | `"test"` | `"test"` o `"production"`. |
|
|
49
50
|
| `token` | `str` | `""` | Bearer token preobtenido. Si se provee, se omite el login. |
|
|
50
|
-
| `
|
|
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()`. |
|
|
51
53
|
| `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
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
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
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
|
+
| `petroleo_rates` | `dict[str, float] \| None` | `None` | Mapa código PETROLEO → tarifa por unidad. Usado sólo por `fuel_invoice()` (gasolineras). |
|
|
58
61
|
| `timeout` | `int` | `120` | Timeout HTTP en segundos. |
|
|
59
62
|
| `session` | `requests.Session \| None` | `None` | Sesión HTTP personalizada (útil para tests). |
|
|
60
|
-
| `
|
|
63
|
+
| `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. |
|
|
61
64
|
|
|
62
65
|
## Inicio rápido
|
|
63
66
|
|
|
@@ -87,7 +90,19 @@ result = client.invoice(
|
|
|
87
90
|
]
|
|
88
91
|
)
|
|
89
92
|
|
|
90
|
-
# FACT a receptor con CUI
|
|
93
|
+
# FACT a receptor con CUI (DPI) — el nombre se consulta en SAT automáticamente
|
|
94
|
+
result = client.invoice(
|
|
95
|
+
buyer={"taxid": "1234567890123", "type": "CUI"},
|
|
96
|
+
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Equivalente: un string de 13 dígitos se resuelve como CUI
|
|
100
|
+
result = client.invoice(
|
|
101
|
+
buyer="1234567890123",
|
|
102
|
+
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Con nombre explícito no se consulta SAT
|
|
91
106
|
result = client.invoice(
|
|
92
107
|
buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
|
|
93
108
|
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
@@ -156,6 +171,10 @@ result = client.credit_note_total(
|
|
|
156
171
|
info = client.lookup_nit("12345678")
|
|
157
172
|
print(info["name"])
|
|
158
173
|
|
|
174
|
+
# Consulta de CUI (DPI) — SAT devuelve el nombre como "APELLIDOS, NOMBRES"
|
|
175
|
+
info = client.lookup_cui("1234567890123")
|
|
176
|
+
print(info["name"]) # "PEREZ LOPEZ, JUAN CARLOS"
|
|
177
|
+
|
|
159
178
|
# Obtener DTE
|
|
160
179
|
doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
|
|
161
180
|
```
|
|
@@ -256,13 +275,37 @@ result = client.fuel_invoice(
|
|
|
256
275
|
| `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
|
|
257
276
|
| `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
277
|
|
|
278
|
+
### Subsidio combustible
|
|
279
|
+
|
|
280
|
+
El subsidio a la gasolina y al diésel **finalizó el jueves 2 de julio de 2026 a las 24:00**, antes de lo
|
|
281
|
+
previsto: el presupuesto de Q2 mil millones (Decreto 11-2026, reglamentado por el Acuerdo Gubernativo
|
|
282
|
+
64-2026) se agotó por la demanda. Por eso el SDK **nunca** envía las frases `TipoFrase=9, Escenario=18` ni
|
|
283
|
+
`TipoFrase=9, Escenario=19` por su cuenta — no hay fecha de corte que valga para todos.
|
|
284
|
+
|
|
285
|
+
Las estaciones con inventario adquirido bajo el subsidio deben mantener el precio rebajado hasta agotar ese
|
|
286
|
+
producto, sujeto a verificación. Mientras te quede ese inventario, manda las frases explícitamente con
|
|
287
|
+
`frases`; al agotarlo, deja de mandarlas:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
result = client.fuel_invoice("CF", items, frases=[
|
|
291
|
+
{"tipo_frase": "1", "escenario": "1"}, # frase base
|
|
292
|
+
{"tipo_frase": "9", "escenario": "18"},
|
|
293
|
+
{"tipo_frase": "9", "escenario": "19"},
|
|
294
|
+
])
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
> **Nota:** la leyenda del subsidio en la representación gráfica la genera Digifact a partir de las frases
|
|
298
|
+
> del XML certificado, no este SDK. Al dejar de mandar las frases, deja de imprimirse sola.
|
|
299
|
+
|
|
300
|
+
> **Nota:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** — pasarlos juntos lanza `DigifactValidationError`.
|
|
301
|
+
|
|
259
302
|
## Configuración de frases (TipoFrase / CodigoEscenario)
|
|
260
303
|
|
|
261
304
|
Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
|
|
262
305
|
SDK elige valores por defecto adecuados, por lo que **no** hace falta
|
|
263
306
|
configurar nada en el caso común.
|
|
264
307
|
|
|
265
|
-
**Orden de precedencia:** argumentos por llamada → globales del constructor
|
|
308
|
+
**Orden de precedencia:** argumentos por llamada → globales del constructor → tabla de valores por defecto.
|
|
266
309
|
|
|
267
310
|
**Tabla de valores por defecto:**
|
|
268
311
|
|
|
@@ -277,10 +320,30 @@ configurar nada en el caso común.
|
|
|
277
320
|
| FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
|
|
278
321
|
| FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
|
|
279
322
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
323
|
+
### Nueva API: `frases[]` (múltiples frases)
|
|
324
|
+
|
|
325
|
+
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`.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# Lista explícita de frases (reemplaza a tipo_frase/escenario)
|
|
329
|
+
client.fuel_invoice("CF", items, frases=[
|
|
330
|
+
{"tipo_frase": "1", "escenario": "1"},
|
|
331
|
+
{"tipo_frase": "9", "escenario": "18"},
|
|
332
|
+
{"tipo_frase": "9", "escenario": "19"},
|
|
333
|
+
])
|
|
334
|
+
|
|
335
|
+
# Globalmente en el constructor
|
|
336
|
+
client = DigifactClient(
|
|
337
|
+
taxid="12345678", username="FELUSER", password="...",
|
|
338
|
+
frases=[{"tipo_frase": "1", "escenario": "2"}], # ISR régimen opcional simplificado
|
|
339
|
+
)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
> **Regla:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** en cada nivel (por llamada y en el constructor). Combinarlos lanza `DigifactValidationError`.
|
|
343
|
+
|
|
344
|
+
### API legacy: `tipo_frase` / `escenario`
|
|
345
|
+
|
|
346
|
+
Ambos parámetros siguen funcionando exactamente como antes para integraciones existentes.
|
|
284
347
|
|
|
285
348
|
```python
|
|
286
349
|
# Sobreescritura por llamada (uno o ambos)
|
|
@@ -311,18 +374,19 @@ Todos los métodos de emisión devuelven `DteResult` con `result.auth_number`, `
|
|
|
311
374
|
|--------|-------|-------------|
|
|
312
375
|
| `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
376
|
| `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). |
|
|
377
|
+
| `fuel_invoice()` | `fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None, frases=None)` | FACT con complemento combustible (IVA + PETROLEO). |
|
|
315
378
|
| `credit_note()` | `credit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de crédito (NCRE). |
|
|
316
379
|
| `debit_note()` | `debit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de débito (NDEB). |
|
|
317
380
|
| `credit_note_total()` | `credit_note_total(auth_number, issue_datetime, reason="...", reference="")` | Nota de crédito total. Devuelve `dict`. |
|
|
318
381
|
| `cancel()` | `cancel(auth_number, receiver_id, issue_datetime, reason="Anulación")` | Anula un DTE. Devuelve `dict`. |
|
|
319
382
|
| `lookup_nit()` | `lookup_nit(nit)` | Consulta SAT. Devuelve `{"nit","name","address","city","district","state"}`. |
|
|
383
|
+
| `lookup_cui()` | `lookup_cui(cui)` | Consulta SAT por CUI (DPI). Devuelve `{"cui","name","status"}`. El nombre viene como `"APELLIDOS, NOMBRES"`, tal como lo registra SAT. |
|
|
320
384
|
| `get_dte()` | `get_dte(auth_number, fmt="JSON")` | Recupera el DTE (`"JSON"`, `"XML"`, `"HTML"`, `"PDF"`). |
|
|
321
385
|
| `get_dte_info()` | `get_dte_info(auth_number)` | Metadatos del DTE. |
|
|
322
386
|
|
|
323
387
|
### Parámetros comunes
|
|
324
388
|
|
|
325
|
-
- **`buyer`**: `"CF"` (consumidor final), un NIT string (`"12345678"` — se consulta el nombre), un dict CUI (`{"type":"CUI","taxid"
|
|
389
|
+
- **`buyer`**: `"CF"` (consumidor final), un NIT string (`"12345678"` — se consulta el nombre), un CUI string de 13 dígitos (`"1234567890123"` — se consulta el nombre), un dict CUI (`{"type":"CUI","taxid":...}` — se consulta el nombre; con `"name"` explícito no se consulta) o un dict NIT explícito (`{"taxid","name","address","city","district","state","country","email"}`).
|
|
326
390
|
- **`items`**: lista de dicts con `description` (str, req), `price` (float/Decimal, req), `qty` (1), `type` (`"Servicio"`/`"Bien"`), `unit_of_measure` (`"UNI"`), `discount` (opcional).
|
|
327
391
|
- **`origin`** (NCRE/NDEB): `{"auth_number": ..., "date": "YYYY-MM-DD", "series": ..., "number": ...}`.
|
|
328
392
|
|
|
@@ -362,6 +426,7 @@ python -m pytest tests/ -v
|
|
|
362
426
|
| `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
|
|
363
427
|
| `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
|
|
364
428
|
| `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
|
|
429
|
+
| `DIGIFACT_ENVIRONMENT` | `test` (default) o `production` |
|
|
365
430
|
|
|
366
431
|
## Manejo de errores
|
|
367
432
|
|
|
@@ -11,29 +11,32 @@ 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
17
|
## Configuración del cliente (`DigifactClient(...)`)
|
|
18
18
|
|
|
19
|
+
Ordenados de más usados a menos usados.
|
|
20
|
+
|
|
19
21
|
| Parámetro | Tipo | Por defecto | Descripción |
|
|
20
22
|
|-----------|------|-------------|-------------|
|
|
21
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. |
|
|
22
24
|
| `username` | `str` | **requerido** | Usuario corto de Digifact (la parte después de `GT.<NIT>.`, p. ej. `"FELUSER"`). |
|
|
23
25
|
| `password` | `str` | `""` | Contraseña de la cuenta. **Requerido** si no se provee `token`. |
|
|
24
|
-
| `environment` | `str` | `"test"` | `"test"` o `"production"`. |
|
|
25
26
|
| `token` | `str` | `""` | Bearer token preobtenido. Si se provee, se omite el login. |
|
|
26
|
-
| `
|
|
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()`. |
|
|
27
29
|
| `seller_address` | `str` | `""` | Dirección del emisor. Si está vacía, se consulta en SAT. |
|
|
28
|
-
| `afiliacion_iva` | `str` | `"GEN"` | Afiliación IVA del RTU: `"GEN"`, `"PEQ"` o `"EXE"`. |
|
|
29
|
-
| `tipo_personeria` | `str` | `"1"` | Código de `TipoPersoneria` del RTU (usado en RDON). |
|
|
30
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
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
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
|
+
| `petroleo_rates` | `dict[str, float] \| None` | `None` | Mapa código PETROLEO → tarifa por unidad. Usado sólo por `fuel_invoice()` (gasolineras). |
|
|
34
37
|
| `timeout` | `int` | `120` | Timeout HTTP en segundos. |
|
|
35
38
|
| `session` | `requests.Session \| None` | `None` | Sesión HTTP personalizada (útil para tests). |
|
|
36
|
-
| `
|
|
39
|
+
| `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. |
|
|
37
40
|
|
|
38
41
|
## Inicio rápido
|
|
39
42
|
|
|
@@ -63,7 +66,19 @@ result = client.invoice(
|
|
|
63
66
|
]
|
|
64
67
|
)
|
|
65
68
|
|
|
66
|
-
# FACT a receptor con CUI
|
|
69
|
+
# FACT a receptor con CUI (DPI) — el nombre se consulta en SAT automáticamente
|
|
70
|
+
result = client.invoice(
|
|
71
|
+
buyer={"taxid": "1234567890123", "type": "CUI"},
|
|
72
|
+
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Equivalente: un string de 13 dígitos se resuelve como CUI
|
|
76
|
+
result = client.invoice(
|
|
77
|
+
buyer="1234567890123",
|
|
78
|
+
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Con nombre explícito no se consulta SAT
|
|
67
82
|
result = client.invoice(
|
|
68
83
|
buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
|
|
69
84
|
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
|
|
@@ -132,6 +147,10 @@ result = client.credit_note_total(
|
|
|
132
147
|
info = client.lookup_nit("12345678")
|
|
133
148
|
print(info["name"])
|
|
134
149
|
|
|
150
|
+
# Consulta de CUI (DPI) — SAT devuelve el nombre como "APELLIDOS, NOMBRES"
|
|
151
|
+
info = client.lookup_cui("1234567890123")
|
|
152
|
+
print(info["name"]) # "PEREZ LOPEZ, JUAN CARLOS"
|
|
153
|
+
|
|
135
154
|
# Obtener DTE
|
|
136
155
|
doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
|
|
137
156
|
```
|
|
@@ -232,13 +251,37 @@ result = client.fuel_invoice(
|
|
|
232
251
|
| `petroleo_amount` | `float\|Decimal` | — | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
|
|
233
252
|
| `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. |
|
|
234
253
|
|
|
254
|
+
### Subsidio combustible
|
|
255
|
+
|
|
256
|
+
El subsidio a la gasolina y al diésel **finalizó el jueves 2 de julio de 2026 a las 24:00**, antes de lo
|
|
257
|
+
previsto: el presupuesto de Q2 mil millones (Decreto 11-2026, reglamentado por el Acuerdo Gubernativo
|
|
258
|
+
64-2026) se agotó por la demanda. Por eso el SDK **nunca** envía las frases `TipoFrase=9, Escenario=18` ni
|
|
259
|
+
`TipoFrase=9, Escenario=19` por su cuenta — no hay fecha de corte que valga para todos.
|
|
260
|
+
|
|
261
|
+
Las estaciones con inventario adquirido bajo el subsidio deben mantener el precio rebajado hasta agotar ese
|
|
262
|
+
producto, sujeto a verificación. Mientras te quede ese inventario, manda las frases explícitamente con
|
|
263
|
+
`frases`; al agotarlo, deja de mandarlas:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
result = client.fuel_invoice("CF", items, frases=[
|
|
267
|
+
{"tipo_frase": "1", "escenario": "1"}, # frase base
|
|
268
|
+
{"tipo_frase": "9", "escenario": "18"},
|
|
269
|
+
{"tipo_frase": "9", "escenario": "19"},
|
|
270
|
+
])
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
> **Nota:** la leyenda del subsidio en la representación gráfica la genera Digifact a partir de las frases
|
|
274
|
+
> del XML certificado, no este SDK. Al dejar de mandar las frases, deja de imprimirse sola.
|
|
275
|
+
|
|
276
|
+
> **Nota:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** — pasarlos juntos lanza `DigifactValidationError`.
|
|
277
|
+
|
|
235
278
|
## Configuración de frases (TipoFrase / CodigoEscenario)
|
|
236
279
|
|
|
237
280
|
Todo DTE (excepto FESP) debe llevar un par `TipoFrase` + `CodigoEscenario`. El
|
|
238
281
|
SDK elige valores por defecto adecuados, por lo que **no** hace falta
|
|
239
282
|
configurar nada en el caso común.
|
|
240
283
|
|
|
241
|
-
**Orden de precedencia:** argumentos por llamada → globales del constructor
|
|
284
|
+
**Orden de precedencia:** argumentos por llamada → globales del constructor → tabla de valores por defecto.
|
|
242
285
|
|
|
243
286
|
**Tabla de valores por defecto:**
|
|
244
287
|
|
|
@@ -253,10 +296,30 @@ configurar nada en el caso común.
|
|
|
253
296
|
| FACT / FCAM / NCRE / NDEB | PEQ | `2` | `1` | |
|
|
254
297
|
| FACT / FCAM / NCRE / NDEB | EXE | `4` | `1` | Exento |
|
|
255
298
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
299
|
+
### Nueva API: `frases[]` (múltiples frases)
|
|
300
|
+
|
|
301
|
+
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`.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
# Lista explícita de frases (reemplaza a tipo_frase/escenario)
|
|
305
|
+
client.fuel_invoice("CF", items, frases=[
|
|
306
|
+
{"tipo_frase": "1", "escenario": "1"},
|
|
307
|
+
{"tipo_frase": "9", "escenario": "18"},
|
|
308
|
+
{"tipo_frase": "9", "escenario": "19"},
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
# Globalmente en el constructor
|
|
312
|
+
client = DigifactClient(
|
|
313
|
+
taxid="12345678", username="FELUSER", password="...",
|
|
314
|
+
frases=[{"tipo_frase": "1", "escenario": "2"}], # ISR régimen opcional simplificado
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
> **Regla:** `frases` y `tipo_frase`/`escenario` son **mutuamente exclusivos** en cada nivel (por llamada y en el constructor). Combinarlos lanza `DigifactValidationError`.
|
|
319
|
+
|
|
320
|
+
### API legacy: `tipo_frase` / `escenario`
|
|
321
|
+
|
|
322
|
+
Ambos parámetros siguen funcionando exactamente como antes para integraciones existentes.
|
|
260
323
|
|
|
261
324
|
```python
|
|
262
325
|
# Sobreescritura por llamada (uno o ambos)
|
|
@@ -287,18 +350,19 @@ Todos los métodos de emisión devuelven `DteResult` con `result.auth_number`, `
|
|
|
287
350
|
|--------|-------|-------------|
|
|
288
351
|
| `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`. |
|
|
289
352
|
| `cca_invoice()` | `cca_invoice(buyer, items, cobros, *, tipo_frase=None, escenario=None)` | FACT con complemento CCA. |
|
|
290
|
-
| `fuel_invoice()` | `fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None)` | FACT con complemento combustible (IVA + PETROLEO). |
|
|
353
|
+
| `fuel_invoice()` | `fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None, frases=None)` | FACT con complemento combustible (IVA + PETROLEO). |
|
|
291
354
|
| `credit_note()` | `credit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de crédito (NCRE). |
|
|
292
355
|
| `debit_note()` | `debit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None)` | Nota de débito (NDEB). |
|
|
293
356
|
| `credit_note_total()` | `credit_note_total(auth_number, issue_datetime, reason="...", reference="")` | Nota de crédito total. Devuelve `dict`. |
|
|
294
357
|
| `cancel()` | `cancel(auth_number, receiver_id, issue_datetime, reason="Anulación")` | Anula un DTE. Devuelve `dict`. |
|
|
295
358
|
| `lookup_nit()` | `lookup_nit(nit)` | Consulta SAT. Devuelve `{"nit","name","address","city","district","state"}`. |
|
|
359
|
+
| `lookup_cui()` | `lookup_cui(cui)` | Consulta SAT por CUI (DPI). Devuelve `{"cui","name","status"}`. El nombre viene como `"APELLIDOS, NOMBRES"`, tal como lo registra SAT. |
|
|
296
360
|
| `get_dte()` | `get_dte(auth_number, fmt="JSON")` | Recupera el DTE (`"JSON"`, `"XML"`, `"HTML"`, `"PDF"`). |
|
|
297
361
|
| `get_dte_info()` | `get_dte_info(auth_number)` | Metadatos del DTE. |
|
|
298
362
|
|
|
299
363
|
### Parámetros comunes
|
|
300
364
|
|
|
301
|
-
- **`buyer`**: `"CF"` (consumidor final), un NIT string (`"12345678"` — se consulta el nombre), un dict CUI (`{"type":"CUI","taxid"
|
|
365
|
+
- **`buyer`**: `"CF"` (consumidor final), un NIT string (`"12345678"` — se consulta el nombre), un CUI string de 13 dígitos (`"1234567890123"` — se consulta el nombre), un dict CUI (`{"type":"CUI","taxid":...}` — se consulta el nombre; con `"name"` explícito no se consulta) o un dict NIT explícito (`{"taxid","name","address","city","district","state","country","email"}`).
|
|
302
366
|
- **`items`**: lista de dicts con `description` (str, req), `price` (float/Decimal, req), `qty` (1), `type` (`"Servicio"`/`"Bien"`), `unit_of_measure` (`"UNI"`), `discount` (opcional).
|
|
303
367
|
- **`origin`** (NCRE/NDEB): `{"auth_number": ..., "date": "YYYY-MM-DD", "series": ..., "number": ...}`.
|
|
304
368
|
|
|
@@ -338,6 +402,7 @@ python -m pytest tests/ -v
|
|
|
338
402
|
| `DIGIFACT_TAXID` | NIT del emisor (p. ej. `12345678`) |
|
|
339
403
|
| `DIGIFACT_USERNAME` | Usuario (p. ej. `FELUSER`) |
|
|
340
404
|
| `DIGIFACT_PASSWORD` | Contraseña de la cuenta |
|
|
405
|
+
| `DIGIFACT_ENVIRONMENT` | `test` (default) o `production` |
|
|
341
406
|
|
|
342
407
|
## Manejo de errores
|
|
343
408
|
|
|
@@ -27,6 +27,54 @@ def _adenda_code(doc_type: str) -> str:
|
|
|
27
27
|
return ADENDA_CODE_ALT if doc_type in _ALT_ADENDA_TYPES else ADENDA_CODE_STD
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
# ── Frases ───────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _uniq_frases(frases: list[dict]) -> list[dict]:
|
|
34
|
+
seen: set[str] = set()
|
|
35
|
+
out: list[dict] = []
|
|
36
|
+
for f in frases:
|
|
37
|
+
key = f"{f['tipo_frase']}:{f['escenario']}"
|
|
38
|
+
if key not in seen:
|
|
39
|
+
seen.add(key)
|
|
40
|
+
out.append(f)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _build_additionl_info_from_frases(frases: list[dict]) -> list[dict]:
|
|
45
|
+
"""Build Seller.AdditionlInfo entries from a frases list.
|
|
46
|
+
|
|
47
|
+
Each frase gets a 1-based Data index so Digifact's NUC XSLT groups them into
|
|
48
|
+
separate <dte:Frase> elements. Confirmed via live API: Digifact counts the frases
|
|
49
|
+
correctly with this format (error becomes a NIT-authorization check, not XSLT failure).
|
|
50
|
+
"""
|
|
51
|
+
out: list[dict] = []
|
|
52
|
+
for i, f in enumerate(frases, start=1):
|
|
53
|
+
out.append({"Name": "TipoFrase", "Data": str(i), "Value": str(f["tipo_frase"])})
|
|
54
|
+
out.append({"Name": "Escenario", "Data": str(i), "Value": str(f["escenario"])})
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_fuel_frases(
|
|
59
|
+
frases: list[dict] | None,
|
|
60
|
+
tipo_frase: str | None,
|
|
61
|
+
escenario: str | None,
|
|
62
|
+
) -> list[dict]:
|
|
63
|
+
"""Return the final deduplicated frases list for a fuel invoice.
|
|
64
|
+
|
|
65
|
+
Called only after mutual-exclusivity validation (frases vs tipo_frase/escenario).
|
|
66
|
+
"""
|
|
67
|
+
if frases is not None:
|
|
68
|
+
if not frases:
|
|
69
|
+
from .exceptions import DigifactValidationError
|
|
70
|
+
raise DigifactValidationError("frases must contain at least one TipoFrase/Escenario pair")
|
|
71
|
+
return _uniq_frases(frases)
|
|
72
|
+
result: list[dict] = []
|
|
73
|
+
if tipo_frase is not None and escenario is not None:
|
|
74
|
+
result.append({"tipo_frase": tipo_frase, "escenario": escenario})
|
|
75
|
+
return _uniq_frases(result)
|
|
76
|
+
|
|
77
|
+
|
|
30
78
|
def default_frase(doc_type: str, afiliacion: str = "GEN") -> tuple[str, str] | None:
|
|
31
79
|
"""Return default (TipoFrase, CodigoEscenario) for a (doc_type, afiliacion) combo.
|
|
32
80
|
|
|
@@ -68,6 +116,7 @@ def _build_seller(
|
|
|
68
116
|
afiliacion: str = "GEN",
|
|
69
117
|
tipo_frase: str | None = "1",
|
|
70
118
|
escenario: str | None = "1",
|
|
119
|
+
frases: list[dict] | None = None,
|
|
71
120
|
branch_code: str = "1",
|
|
72
121
|
branch_name: str = "ESTABLECIMIENTO PRINCIPAL",
|
|
73
122
|
city: str = "01010",
|
|
@@ -96,8 +145,12 @@ def _build_seller(
|
|
|
96
145
|
}
|
|
97
146
|
if email:
|
|
98
147
|
seller["Contact"] = {"EmailList": {"Email": [email]}}
|
|
99
|
-
# AdditionlInfo — intentional typo in API spec
|
|
100
|
-
|
|
148
|
+
# AdditionlInfo — intentional typo in API spec.
|
|
149
|
+
# Digifact's XSLT groups entries by 1-based Data index into separate <dte:Frase> elements.
|
|
150
|
+
if frases is not None:
|
|
151
|
+
if frases:
|
|
152
|
+
seller["AdditionlInfo"] = _build_additionl_info_from_frases(frases)
|
|
153
|
+
elif tipo_frase is not None and escenario is not None:
|
|
101
154
|
seller["AdditionlInfo"] = [
|
|
102
155
|
{"Name": "TipoFrase", "Data": "1", "Value": tipo_frase},
|
|
103
156
|
{"Name": "Escenario", "Data": "1", "Value": escenario},
|
|
@@ -306,6 +359,7 @@ def build_fact(
|
|
|
306
359
|
afiliacion: str = "GEN",
|
|
307
360
|
tipo_frase: str | None = None,
|
|
308
361
|
escenario: str | None = None,
|
|
362
|
+
frases: list[dict] | None = None,
|
|
309
363
|
amount_str: str = "",
|
|
310
364
|
observaciones: str = "-",
|
|
311
365
|
extra_header: dict | None = None,
|
|
@@ -329,6 +383,7 @@ def build_fact(
|
|
|
329
383
|
afiliacion=afiliacion,
|
|
330
384
|
tipo_frase=tf,
|
|
331
385
|
escenario=es,
|
|
386
|
+
frases=frases,
|
|
332
387
|
email=seller_email,
|
|
333
388
|
)
|
|
334
389
|
|
|
@@ -371,6 +426,7 @@ def build_fcam(
|
|
|
371
426
|
seller_email: str | None = None,
|
|
372
427
|
tipo_frase: str | None = None,
|
|
373
428
|
escenario: str | None = None,
|
|
429
|
+
frases: list[dict] | None = None,
|
|
374
430
|
) -> dict:
|
|
375
431
|
"""Build a FCAM (Factura Cambiaria) DTE payload.
|
|
376
432
|
|
|
@@ -391,6 +447,7 @@ def build_fcam(
|
|
|
391
447
|
afiliacion=afiliacion,
|
|
392
448
|
tipo_frase=tf,
|
|
393
449
|
escenario=es,
|
|
450
|
+
frases=frases,
|
|
394
451
|
email=seller_email,
|
|
395
452
|
)
|
|
396
453
|
|
|
@@ -443,6 +500,7 @@ def build_ndeb(
|
|
|
443
500
|
seller_email: str | None = None,
|
|
444
501
|
tipo_frase: str | None = None,
|
|
445
502
|
escenario: str | None = None,
|
|
503
|
+
frases: list[dict] | None = None,
|
|
446
504
|
) -> dict:
|
|
447
505
|
"""Build a NDEB (Nota de Débito) DTE payload.
|
|
448
506
|
|
|
@@ -462,6 +520,7 @@ def build_ndeb(
|
|
|
462
520
|
afiliacion=afiliacion,
|
|
463
521
|
tipo_frase=tf,
|
|
464
522
|
escenario=es,
|
|
523
|
+
frases=frases,
|
|
465
524
|
email=seller_email,
|
|
466
525
|
)
|
|
467
526
|
|
|
@@ -527,6 +586,7 @@ def build_ncre(
|
|
|
527
586
|
seller_email: str | None = None,
|
|
528
587
|
tipo_frase: str | None = None,
|
|
529
588
|
escenario: str | None = None,
|
|
589
|
+
frases: list[dict] | None = None,
|
|
530
590
|
) -> dict:
|
|
531
591
|
"""Build a NCRE (Nota de Crédito) DTE payload.
|
|
532
592
|
|
|
@@ -546,6 +606,7 @@ def build_ncre(
|
|
|
546
606
|
afiliacion=afiliacion,
|
|
547
607
|
tipo_frase=tf,
|
|
548
608
|
escenario=es,
|
|
609
|
+
frases=frases,
|
|
549
610
|
email=seller_email,
|
|
550
611
|
)
|
|
551
612
|
|
|
@@ -609,6 +670,7 @@ def build_nabn(
|
|
|
609
670
|
amount_str: str = "",
|
|
610
671
|
observaciones: str = "-",
|
|
611
672
|
seller_email: str | None = None,
|
|
673
|
+
frases: list[dict] | None = None,
|
|
612
674
|
) -> dict:
|
|
613
675
|
"""Build a NABN (Nota de Abono) DTE payload — no IVA."""
|
|
614
676
|
issue_dt, _, _ = gt_now()
|
|
@@ -622,6 +684,7 @@ def build_nabn(
|
|
|
622
684
|
afiliacion=afiliacion,
|
|
623
685
|
tipo_frase="1",
|
|
624
686
|
escenario="1",
|
|
687
|
+
frases=frases,
|
|
625
688
|
email=seller_email,
|
|
626
689
|
)
|
|
627
690
|
|
|
@@ -718,6 +781,7 @@ def build_rdon(
|
|
|
718
781
|
amount_str: str = "",
|
|
719
782
|
observaciones: str = "-",
|
|
720
783
|
seller_email: str | None = None,
|
|
784
|
+
frases: list[dict] | None = None,
|
|
721
785
|
) -> dict:
|
|
722
786
|
"""Build a RDON (Recibo por Donación) DTE payload — no IVA."""
|
|
723
787
|
issue_dt, _, _ = gt_now()
|
|
@@ -730,6 +794,7 @@ def build_rdon(
|
|
|
730
794
|
afiliacion=afiliacion,
|
|
731
795
|
tipo_frase="4",
|
|
732
796
|
escenario="4",
|
|
797
|
+
frases=frases,
|
|
733
798
|
email=seller_email,
|
|
734
799
|
)
|
|
735
800
|
|
|
@@ -769,6 +834,7 @@ def build_fpeq(
|
|
|
769
834
|
seller_email: str | None = None,
|
|
770
835
|
tipo_frase: str | None = None,
|
|
771
836
|
escenario: str | None = None,
|
|
837
|
+
frases: list[dict] | None = None,
|
|
772
838
|
) -> dict:
|
|
773
839
|
"""Build a FPEQ (Factura Pequeño Contribuyente) DTE payload — no IVA, AfiliacionIVA=PEQ."""
|
|
774
840
|
issue_dt, _, _ = gt_now()
|
|
@@ -785,6 +851,7 @@ def build_fpeq(
|
|
|
785
851
|
afiliacion="PEQ",
|
|
786
852
|
tipo_frase=tf,
|
|
787
853
|
escenario=es,
|
|
854
|
+
frases=frases,
|
|
788
855
|
email=seller_email,
|
|
789
856
|
)
|
|
790
857
|
|
|
@@ -819,6 +886,7 @@ def build_reci(
|
|
|
819
886
|
student_id: str = "000000000",
|
|
820
887
|
academic_unit: str = "01",
|
|
821
888
|
seller_email: str | None = None,
|
|
889
|
+
frases: list[dict] | None = None,
|
|
822
890
|
) -> dict:
|
|
823
891
|
"""Build a RECI (Recibo) DTE payload — no IVA, universidad ADENDA."""
|
|
824
892
|
issue_dt, _, _ = gt_now()
|
|
@@ -831,6 +899,7 @@ def build_reci(
|
|
|
831
899
|
afiliacion=afiliacion,
|
|
832
900
|
tipo_frase="4",
|
|
833
901
|
escenario="5",
|
|
902
|
+
frases=frases,
|
|
834
903
|
email=seller_email,
|
|
835
904
|
)
|
|
836
905
|
|
|
@@ -873,6 +942,7 @@ def build_cca(
|
|
|
873
942
|
seller_email: str | None = None,
|
|
874
943
|
tipo_frase: str | None = None,
|
|
875
944
|
escenario: str | None = None,
|
|
945
|
+
frases: list[dict] | None = None,
|
|
876
946
|
) -> dict:
|
|
877
947
|
"""Build a CCA (Cobro por Cuenta Ajena) FACT+CCA complemento payload.
|
|
878
948
|
|
|
@@ -902,6 +972,7 @@ def build_cca(
|
|
|
902
972
|
afiliacion=afiliacion,
|
|
903
973
|
tipo_frase=tf,
|
|
904
974
|
escenario=es,
|
|
975
|
+
frases=frases,
|
|
905
976
|
email=seller_email,
|
|
906
977
|
)
|
|
907
978
|
|
|
@@ -1095,6 +1166,7 @@ def build_fact_combustible(
|
|
|
1095
1166
|
afiliacion: str = "GEN",
|
|
1096
1167
|
tipo_frase: str | None = None,
|
|
1097
1168
|
escenario: str | None = None,
|
|
1169
|
+
frases: list[dict] | None = None,
|
|
1098
1170
|
seller_email: str | None = None,
|
|
1099
1171
|
) -> dict:
|
|
1100
1172
|
"""Build a FACT payload for combustible (fuel) invoices.
|
|
@@ -1104,6 +1176,8 @@ def build_fact_combustible(
|
|
|
1104
1176
|
"4"=DIESEL; default "1"). Items without ``petroleo_amount`` are treated as
|
|
1105
1177
|
regular IVA-only items and may coexist in the same invoice.
|
|
1106
1178
|
|
|
1179
|
+
``frases`` and ``tipo_frase``/``escenario`` are mutually exclusive.
|
|
1180
|
+
|
|
1107
1181
|
Example fuel item::
|
|
1108
1182
|
|
|
1109
1183
|
{"description": "GASOLINA SUPER", "qty": 1, "price": 30.30,
|
|
@@ -1113,6 +1187,12 @@ def build_fact_combustible(
|
|
|
1113
1187
|
|
|
1114
1188
|
{"description": "FILTRO DE ACEITE", "qty": 1, "price": 45.00, "type": "Bien"}
|
|
1115
1189
|
"""
|
|
1190
|
+
from .exceptions import DigifactValidationError
|
|
1191
|
+
if frases is not None and (tipo_frase is not None or escenario is not None):
|
|
1192
|
+
raise DigifactValidationError(
|
|
1193
|
+
"frases and tipo_frase/escenario are mutually exclusive; use one or the other"
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1116
1196
|
issue_dt, _, _ = gt_now()
|
|
1117
1197
|
|
|
1118
1198
|
line_items, grand_total, total_iva, total_petroleo = _build_fuel_items(items)
|
|
@@ -1121,13 +1201,16 @@ def build_fact_combustible(
|
|
|
1121
1201
|
tf = tipo_frase if tipo_frase is not None else def_tf
|
|
1122
1202
|
es = escenario if escenario is not None else def_es
|
|
1123
1203
|
|
|
1204
|
+
resolved_frases = resolve_fuel_frases(frases, tf, es)
|
|
1205
|
+
|
|
1206
|
+
# All frases go into Seller.AdditionlInfo as repeated pairs —
|
|
1207
|
+
# that is the field the API reads to generate <dte:Frases> in the certified XML.
|
|
1124
1208
|
seller = _build_seller(
|
|
1125
1209
|
taxid,
|
|
1126
1210
|
seller_name,
|
|
1127
1211
|
seller_address,
|
|
1128
1212
|
afiliacion=afiliacion,
|
|
1129
|
-
|
|
1130
|
-
escenario=es,
|
|
1213
|
+
frases=resolved_frases,
|
|
1131
1214
|
email=seller_email,
|
|
1132
1215
|
)
|
|
1133
1216
|
|
|
@@ -1135,12 +1218,14 @@ def build_fact_combustible(
|
|
|
1135
1218
|
if float(total_petroleo) > 0.0:
|
|
1136
1219
|
total_tax.append({"Description": "PETROLEO", "Amount": total_petroleo})
|
|
1137
1220
|
|
|
1138
|
-
|
|
1221
|
+
combustible_payload: dict[str, Any] = {
|
|
1139
1222
|
"Version": "1.00",
|
|
1140
1223
|
"CountryCode": "GT",
|
|
1141
1224
|
"Header": {"DocType": "FACT", "IssuedDateTime": issue_dt, "Currency": "GTQ"},
|
|
1142
1225
|
"Seller": seller,
|
|
1143
1226
|
"Buyer": buyer,
|
|
1227
|
+
}
|
|
1228
|
+
combustible_payload.update({
|
|
1144
1229
|
"ThirdParties": None,
|
|
1145
1230
|
"Items": line_items,
|
|
1146
1231
|
"Totals": {
|
|
@@ -1148,4 +1233,5 @@ def build_fact_combustible(
|
|
|
1148
1233
|
"GrandTotal": {"InvoiceTotal": grand_total},
|
|
1149
1234
|
},
|
|
1150
1235
|
"AdditionalDocumentInfo": _build_fuel_adenda(),
|
|
1151
|
-
}
|
|
1236
|
+
})
|
|
1237
|
+
return combustible_payload
|