libredte-lib-sdk 0.4.0__tar.gz → 0.5.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.
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/PKG-INFO +4 -11
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/README.rst +3 -10
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/__init__.py +1 -2
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/builder.py +6 -4
- libredte_lib_sdk-0.5.0/libredte_lib_sdk/billing/book/models.py +79 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/common.py +8 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/__init__.py +0 -2
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/builder.py +18 -16
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/dispatcher.py +48 -25
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/loader.py +8 -1
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/models.py +116 -43
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/renderer.py +1 -1
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/validator.py +1 -1
- libredte_lib_sdk-0.5.0/libredte_lib_sdk/billing/identifier/models.py +75 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/sdk.py +1 -3
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/pyproject.toml +1 -1
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/conftest.py +22 -14
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_book.py +8 -6
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_caf.py +6 -2
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_loader.py +9 -8
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_ownership_transfer.py +1 -2
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/test_models.py +124 -18
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/test_services.py +214 -37
- libredte_lib_sdk-0.4.0/libredte_lib_sdk/billing/book/models.py +0 -70
- libredte_lib_sdk-0.4.0/libredte_lib_sdk/billing/identifier/models.py +0 -40
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.claude/scheduled_tasks.lock +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.github/workflows/ci.yml +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.github/workflows/docs.yml +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.gitignore +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.readthedocs.yaml +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/CLAUDE.md +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/LICENSE +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/Makefile +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/docs/Makefile +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/docs/conf.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/docs/index.rst +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/loader.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/validator.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/examples.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/enums.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/document_response.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/models.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_faker.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_loader.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_validator.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/models.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_dte.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_rcv.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_rtc.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/aec.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/models.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/mandatario_manager.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/models.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/client.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/exceptions.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/integration/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/integration/previred_provider.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/calculator.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/renderer.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/py.typed +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/certificate/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/certificate/loader.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/repository/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/repository/catalog.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/conftest.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/__init__.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_certificate_loader.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_rendering.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_validation.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_examples.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_exchange.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_human_resources.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_operation_not_found.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_repository.py +0 -0
- {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: libredte-lib-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: SDK Python para la API de LibreDTE Lib (facturacion electronica de Chile).
|
|
5
5
|
Project-URL: Homepage, https://www.libredte.cl
|
|
6
6
|
Project-URL: Source, https://github.com/libredte/libredte-lib-sdk-python
|
|
@@ -133,16 +133,9 @@ Uso
|
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
# 3. Envío al SII (arma el sobre EnvioDTE y luego lo envía).
|
|
136
|
-
emisor
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'autorizacion_dte': {
|
|
140
|
-
'fecha_resolucion': '2014-08-22', 'numero_resolucion': 80,
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
sobre = b.document.dispatcher.create(
|
|
144
|
-
documento.xml_base64, certificate=certificate, emisor=emisor,
|
|
145
|
-
)
|
|
136
|
+
# `documento` ya trae certificate/emisor incluidos — no hace falta
|
|
137
|
+
# pasarlos aparte.
|
|
138
|
+
sobre = b.document.dispatcher.create(documento)
|
|
146
139
|
envio = b.integration.sii_dte.send(
|
|
147
140
|
sobre.xml_base64, certificate=certificate, company_rut='76192083-9',
|
|
148
141
|
)
|
|
@@ -89,16 +89,9 @@ Uso
|
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
# 3. Envío al SII (arma el sobre EnvioDTE y luego lo envía).
|
|
92
|
-
emisor
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'autorizacion_dte': {
|
|
96
|
-
'fecha_resolucion': '2014-08-22', 'numero_resolucion': 80,
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
sobre = b.document.dispatcher.create(
|
|
100
|
-
documento.xml_base64, certificate=certificate, emisor=emisor,
|
|
101
|
-
)
|
|
92
|
+
# `documento` ya trae certificate/emisor incluidos — no hace falta
|
|
93
|
+
# pasarlos aparte.
|
|
94
|
+
sobre = b.document.dispatcher.create(documento)
|
|
102
95
|
envio = b.integration.sii_dte.send(
|
|
103
96
|
sobre.xml_base64, certificate=certificate, company_rut='76192083-9',
|
|
104
97
|
)
|
|
@@ -8,11 +8,10 @@ from __future__ import annotations
|
|
|
8
8
|
from ...client import ApiClient
|
|
9
9
|
from .builder import BookBuilderService
|
|
10
10
|
from .loader import BookLoaderService
|
|
11
|
-
from .models import
|
|
11
|
+
from .models import BookBag
|
|
12
12
|
from .validator import BookValidatorService
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
|
-
'Book',
|
|
16
15
|
'BookBag',
|
|
17
16
|
'BookBuilderService',
|
|
18
17
|
'BookComponent',
|
|
@@ -9,7 +9,7 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
from ...client import ApiClient
|
|
11
11
|
from ..trading_parties.models import Certificate
|
|
12
|
-
from .models import
|
|
12
|
+
from .models import BookBag
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class BookBuilderService:
|
|
@@ -26,17 +26,19 @@ class BookBuilderService:
|
|
|
26
26
|
bag: dict[str, Any],
|
|
27
27
|
*,
|
|
28
28
|
certificate: Certificate,
|
|
29
|
-
) ->
|
|
29
|
+
) -> BookBag:
|
|
30
30
|
"""
|
|
31
31
|
Construye el libro descrito por `bag`.
|
|
32
32
|
|
|
33
33
|
`bag` trae `tipo` (`'libro_ventas'`, `'libro_compras'`,
|
|
34
34
|
`'libro_boletas'`, `'libro_guias'`, `'resumen_ventas_diarias'`,
|
|
35
35
|
sin tipar como enum cerrado), `caratula`, `detalle` y `emisor`,
|
|
36
|
-
tal cual el formato SII.
|
|
36
|
+
tal cual el formato SII. A diferencia de `BookLoaderService
|
|
37
|
+
.load()`, acá `BookBag.book` viene poblado (el libro construido
|
|
38
|
+
y firmado, con su `xml`).
|
|
37
39
|
"""
|
|
38
40
|
data = self._client.call(
|
|
39
41
|
self._BUILD_OPERATION,
|
|
40
42
|
bag={**bag, 'certificate': certificate.to_payload()},
|
|
41
43
|
)
|
|
42
|
-
return
|
|
44
|
+
return BookBag.from_api(data)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (C) 2026 LibreDTE <https://www.libredte.cl>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""DTO del componente `billing.book`."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class BookBag:
|
|
15
|
+
"""
|
|
16
|
+
Bolsa de un libro tributario (`book.builder::build`/`book.loader::load`).
|
|
17
|
+
|
|
18
|
+
Ambas operaciones devuelven esta misma forma — la diferencia es qué
|
|
19
|
+
tan lejos llegó el procesamiento: `loader.load()` solo normaliza
|
|
20
|
+
`caratula`/`detalle` a partir de los datos de entrada (`book`
|
|
21
|
+
siempre viene `None`, sin importar el input); `builder.build()` sí
|
|
22
|
+
construye y firma el libro, por lo que `book` viene poblado (el
|
|
23
|
+
libro completo — genérico entre los 5 tipos, ej.
|
|
24
|
+
`libro_ventas`/`libro_compras` traen `{'LibroCompraVenta': {...}}`
|
|
25
|
+
junto a su `xml` en base64 — no se tipa más allá de `dict` porque
|
|
26
|
+
la clave raíz varía según el tipo). `book_auth` (la autorización
|
|
27
|
+
del emisor) solo viene poblada si esa autorización se incluyó junto
|
|
28
|
+
con los datos del emisor en la solicitud; si no, viene `None`.
|
|
29
|
+
|
|
30
|
+
`xml_base64`/`xml`/`xml_bytes` dan acceso directo al XML del libro
|
|
31
|
+
construido — solo válidos cuando `book` no es `None` (ver
|
|
32
|
+
`is_construido`); listos para encadenar a `BookValidatorService`.
|
|
33
|
+
No usa `XmlPayloadMixin`: acá `xml_base64` es una `@property` (el
|
|
34
|
+
libro puede no estar construido), no un campo simple.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
book: dict[str, Any] | None
|
|
38
|
+
book_type: dict[str, Any]
|
|
39
|
+
book_auth: dict[str, Any] | None
|
|
40
|
+
caratula: dict[str, Any]
|
|
41
|
+
detalle: list[dict[str, Any]]
|
|
42
|
+
raw: dict[str, Any]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_construido(self) -> bool:
|
|
46
|
+
"""Si `book` viene poblado (`builder.build()`, no `loader.load()`)."""
|
|
47
|
+
return self.book is not None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def xml_base64(self) -> str:
|
|
51
|
+
"""XML del libro construido, en base64 — ver `BookValidatorService`."""
|
|
52
|
+
if self.book is None:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
'Este BookBag no tiene un libro construido — viene de '
|
|
55
|
+
'`loader.load()`, no de `builder.build()`.',
|
|
56
|
+
)
|
|
57
|
+
return cast('str', self.book['xml'])
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def xml_bytes(self) -> bytes:
|
|
61
|
+
"""XML del libro construido, decodificado desde base64."""
|
|
62
|
+
return base64.b64decode(self.xml_base64)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def xml(self) -> str:
|
|
66
|
+
"""XML del libro construido, como texto (ISO-8859-1)."""
|
|
67
|
+
return self.xml_bytes.decode('iso-8859-1')
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_api(cls, data: dict[str, Any]) -> BookBag:
|
|
71
|
+
"""Construye un `BookBag` desde el `data` que devuelve la API."""
|
|
72
|
+
return cls(
|
|
73
|
+
book=data.get('book'),
|
|
74
|
+
book_type=data['book_type'],
|
|
75
|
+
book_auth=data.get('book_auth'),
|
|
76
|
+
caratula=data.get('caratula') or {},
|
|
77
|
+
detalle=data.get('detalle') or [],
|
|
78
|
+
raw=data,
|
|
79
|
+
)
|
|
@@ -15,6 +15,14 @@ class XmlPayloadMixin:
|
|
|
15
15
|
Espera un atributo `xml_base64: str` en la clase concreta (`Document`,
|
|
16
16
|
`DocumentEnvelope`, `Caf`, ...) y expone `xml_bytes`/`xml` sobre él,
|
|
17
17
|
para no repetir esta misma conversión en cada DTO.
|
|
18
|
+
|
|
19
|
+
Solo para clases donde `xml_base64` es un campo real del dataclass
|
|
20
|
+
(siempre presente). `BookBag`/`DocumentBag` NO lo usan — su
|
|
21
|
+
`xml_base64` es una `@property` que puede lanzar (el XML puede no
|
|
22
|
+
existir aún, ej. un borrador sin construir) y un `@property` no
|
|
23
|
+
puede sobrescribir a un atributo simple heredado sin romper el
|
|
24
|
+
orden de campos del dataclass (`xml_base64` seguido de un campo
|
|
25
|
+
obligatorio) — definen `xml_bytes`/`xml` por su cuenta.
|
|
18
26
|
"""
|
|
19
27
|
|
|
20
28
|
__slots__ = ()
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/__init__.py
RENAMED
|
@@ -11,7 +11,6 @@ from .dispatcher import DocumentDispatcherService
|
|
|
11
11
|
from .examples import DocumentExamplesService
|
|
12
12
|
from .loader import DocumentLoaderService
|
|
13
13
|
from .models import (
|
|
14
|
-
Document,
|
|
15
14
|
DocumentBag,
|
|
16
15
|
DocumentEnvelope,
|
|
17
16
|
Example,
|
|
@@ -23,7 +22,6 @@ from .renderer import DocumentRendererService
|
|
|
23
22
|
from .validator import DocumentValidatorService
|
|
24
23
|
|
|
25
24
|
__all__ = [
|
|
26
|
-
'Document',
|
|
27
25
|
'DocumentBag',
|
|
28
26
|
'DocumentBuilderService',
|
|
29
27
|
'DocumentComponent',
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/builder.py
RENAMED
|
@@ -9,7 +9,7 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
from ...client import ApiClient
|
|
11
11
|
from ..trading_parties.models import Certificate
|
|
12
|
-
from .models import
|
|
12
|
+
from .models import DocumentBag
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class DocumentBuilderService:
|
|
@@ -33,27 +33,27 @@ class DocumentBuilderService:
|
|
|
33
33
|
input_data: str | dict[str, Any],
|
|
34
34
|
*,
|
|
35
35
|
options: dict[str, Any] | None = None,
|
|
36
|
-
) ->
|
|
36
|
+
) -> DocumentBag:
|
|
37
37
|
"""
|
|
38
38
|
Emite el borrador de un DTE a partir de datos de entrada.
|
|
39
39
|
|
|
40
40
|
`input_data` es el `Encabezado`/`Detalle` (y demás nodos) del
|
|
41
|
-
formato DTE del SII como `dict
|
|
42
|
-
|
|
43
|
-
quien llama, típicamente porque lleva el correlativo) — o datos
|
|
44
|
-
en otro formato (XML/YAML, un formulario, etc.) junto con
|
|
41
|
+
formato DTE del SII como `dict` — o datos en otro formato
|
|
42
|
+
(XML/YAML, un formulario, etc.) junto con
|
|
45
43
|
`options={'parser': {'strategy': '<estrategia>'}}` para indicar
|
|
46
44
|
cómo parsearlos. Sin `options`, la API asume que `input_data` ya
|
|
47
|
-
viene en el formato DTE del SII (estrategia `default.json`).
|
|
48
|
-
|
|
49
|
-
(`
|
|
45
|
+
viene en el formato DTE del SII (estrategia `default.json`).
|
|
46
|
+
`Encabezado.IdDoc.Folio` es opcional acá — un borrador sin folio
|
|
47
|
+
no falla (queda con folio `0` en `DocumentBag.document_id`, sin
|
|
48
|
+
nodo `Folio` en el XML). Sin CAF ni certificado, el resultado no
|
|
49
|
+
queda timbrado (`DocumentBag.is_timbrado` es `False`).
|
|
50
50
|
"""
|
|
51
51
|
bag: dict[str, Any] = {'inputData': input_data}
|
|
52
52
|
if options is not None:
|
|
53
53
|
bag['options'] = options
|
|
54
54
|
|
|
55
55
|
data = self._client.call(self._BUILD_OPERATION, bag=bag)
|
|
56
|
-
return
|
|
56
|
+
return DocumentBag.from_api(data)
|
|
57
57
|
|
|
58
58
|
def build_signed(
|
|
59
59
|
self,
|
|
@@ -62,14 +62,16 @@ class DocumentBuilderService:
|
|
|
62
62
|
options: dict[str, Any] | None = None,
|
|
63
63
|
caf_xml: str,
|
|
64
64
|
certificate: Certificate,
|
|
65
|
-
) ->
|
|
65
|
+
) -> DocumentBag:
|
|
66
66
|
"""
|
|
67
67
|
Genera el DTE real, timbrado y firmado.
|
|
68
68
|
|
|
69
|
-
`input_data`/`options` funcionan igual que en `build_draft()
|
|
70
|
-
|
|
71
|
-
el
|
|
72
|
-
|
|
69
|
+
`input_data`/`options` funcionan igual que en `build_draft()` —
|
|
70
|
+
acá sí se necesita un `Encabezado.IdDoc.Folio` real, cubierto
|
|
71
|
+
por `caf_xml` (el SDK no asigna folios: eso lo decide quien
|
|
72
|
+
llama, típicamente porque lleva el correlativo). Requiere un CAF
|
|
73
|
+
real (XML tal como lo entrega el SII) y el certificado digital
|
|
74
|
+
del emisor. Para pruebas, ambos se pueden generar con
|
|
73
75
|
`IdentifierComponent.caf_faker` y
|
|
74
76
|
`TradingPartiesComponent.mandatario_manager`.
|
|
75
77
|
"""
|
|
@@ -80,4 +82,4 @@ class DocumentBuilderService:
|
|
|
80
82
|
bag['certificate'] = certificate.to_payload()
|
|
81
83
|
|
|
82
84
|
data = self._client.call(self._BUILD_OPERATION, bag=bag)
|
|
83
|
-
return
|
|
85
|
+
return DocumentBag.from_api(data)
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/dispatcher.py
RENAMED
|
@@ -5,17 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from collections.abc import Sequence
|
|
8
9
|
from typing import Any, cast
|
|
9
10
|
|
|
10
11
|
from ...client import ApiClient
|
|
11
|
-
from
|
|
12
|
-
from .models import DocumentEnvelope
|
|
12
|
+
from .models import DocumentBag, DocumentEnvelope
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class DocumentDispatcherService:
|
|
16
16
|
"""Arma y valida el sobre `EnvioDTE` (`billing.document.dispatcher`)."""
|
|
17
17
|
|
|
18
18
|
_CREATE_OPERATION = 'billing.document.dispatcher::create'
|
|
19
|
+
_CREATE_MANY_OPERATION = 'billing.document.dispatcher::createMany'
|
|
19
20
|
_LOAD_XML_OPERATION = 'billing.document.dispatcher::loadXml'
|
|
20
21
|
_VALIDATE_OPERATION = 'billing.document.dispatcher::validate'
|
|
21
22
|
_VALIDATE_SCHEMA_OPERATION = 'billing.document.dispatcher::validateSchema'
|
|
@@ -27,42 +28,64 @@ class DocumentDispatcherService:
|
|
|
27
28
|
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
28
29
|
self._client = client
|
|
29
30
|
|
|
30
|
-
def create(
|
|
31
|
-
self,
|
|
32
|
-
document_xml_base64: str,
|
|
33
|
-
*,
|
|
34
|
-
certificate: Certificate,
|
|
35
|
-
emisor: dict[str, Any],
|
|
36
|
-
) -> DocumentEnvelope:
|
|
31
|
+
def create(self, bag: DocumentBag) -> DocumentEnvelope:
|
|
37
32
|
"""
|
|
38
33
|
Envuelve y firma un documento ya timbrado en un sobre `EnvioDTE`.
|
|
39
34
|
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`emisor` es un `dict` con `rut`/`razon_social`/
|
|
46
|
-
`autorizacion_dte` (esta última con `fecha_resolucion`/
|
|
47
|
-
`numero_resolucion`), tal como lo espera la API.
|
|
35
|
+
`bag` es el `DocumentBag` ya timbrado y firmado que devuelve
|
|
36
|
+
`DocumentBuilderService.build_signed()` — trae `certificate`/
|
|
37
|
+
`emisor` incluidos, no hace falta pasarlos aparte. El sobre
|
|
38
|
+
resultante es lo que se envía al SII (`SiiDteService.send`).
|
|
48
39
|
"""
|
|
49
40
|
data = self._client.call(
|
|
50
41
|
self._CREATE_OPERATION,
|
|
51
|
-
bag=
|
|
52
|
-
'xmlDocument': document_xml_base64,
|
|
53
|
-
'certificate': certificate.to_payload(),
|
|
54
|
-
'emisor': emisor,
|
|
55
|
-
},
|
|
42
|
+
bag=self._bag_payload(bag),
|
|
56
43
|
)
|
|
57
44
|
return DocumentEnvelope.from_api(data)
|
|
58
45
|
|
|
46
|
+
def create_many(self, bags: Sequence[DocumentBag]) -> DocumentEnvelope:
|
|
47
|
+
"""
|
|
48
|
+
Envuelve y firma 2+ documentos ya timbrados en un sobre `EnvioDTE`.
|
|
49
|
+
|
|
50
|
+
Igual que `create()`, pero para un sobre que agrupa más de un
|
|
51
|
+
documento — cada `DocumentBag` trae su propio `certificate`/
|
|
52
|
+
`emisor`. El sobre resultante queda firmado con el certificado
|
|
53
|
+
del PRIMER `DocumentBag` (el sobre en sí también se firma, no
|
|
54
|
+
solo cada documento).
|
|
55
|
+
"""
|
|
56
|
+
data = self._client.call(
|
|
57
|
+
self._CREATE_MANY_OPERATION,
|
|
58
|
+
bags=[self._bag_payload(bag) for bag in bags],
|
|
59
|
+
)
|
|
60
|
+
return DocumentEnvelope.from_api(data)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _bag_payload(bag: DocumentBag) -> dict[str, Any]:
|
|
64
|
+
if bag.certificate is None:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
'Este DocumentBag no tiene certificado — ¿se firmó con '
|
|
67
|
+
'`builder.build_signed()`?',
|
|
68
|
+
)
|
|
69
|
+
if bag.emisor is None:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
'Este DocumentBag no tiene emisor — ¿se construyó con '
|
|
72
|
+
'`builder.build_draft/build_signed()` o se cargó con '
|
|
73
|
+
'`loader.load_xml()`?',
|
|
74
|
+
)
|
|
75
|
+
return {
|
|
76
|
+
'xmlDocument': bag.xml_base64,
|
|
77
|
+
'certificate': bag.certificate.to_payload(),
|
|
78
|
+
'emisor': bag.emisor,
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
def load_xml(self, xml_base64: str) -> DocumentEnvelope:
|
|
60
82
|
"""
|
|
61
83
|
Carga un sobre `EnvioDTE` ya existente desde su XML (base64).
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
El `DocumentEnvelope` resultante trae en `documents` una bolsa
|
|
86
|
+
por cada documento tributario que el sobre agrupaba — uno o
|
|
87
|
+
más — y en `caratula` los datos de la carátula ya presente en
|
|
88
|
+
ese XML.
|
|
66
89
|
"""
|
|
67
90
|
data = self._client.call(self._LOAD_XML_OPERATION, xml=xml_base64)
|
|
68
91
|
return DocumentEnvelope.from_api(data)
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/loader.py
RENAMED
|
@@ -26,6 +26,13 @@ class DocumentLoaderService:
|
|
|
26
26
|
self._client = client
|
|
27
27
|
|
|
28
28
|
def load_xml(self, xml_base64: str) -> DocumentBag:
|
|
29
|
-
"""
|
|
29
|
+
"""
|
|
30
|
+
Carga y normaliza el documento cuyo XML (base64) es `xml_base64`.
|
|
31
|
+
|
|
32
|
+
Devuelve la misma `DocumentBag` que `DocumentBuilderService` —
|
|
33
|
+
`document_id`/`xml_base64` reconstruyen lo que ya se le pasó
|
|
34
|
+
acá, `document`/`document_type`/`document_stamp_base64`/etc.
|
|
35
|
+
son el valor agregado real: los datos ya normalizados.
|
|
36
|
+
"""
|
|
30
37
|
data = self._client.call(self._LOAD_XML_OPERATION, xml=xml_base64)
|
|
31
38
|
return DocumentBag.from_api(data)
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/models.py
RENAMED
|
@@ -16,84 +16,157 @@ from dataclasses import dataclass
|
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
18
|
from ..common import XmlPayloadMixin
|
|
19
|
+
from ..identifier.models import Caf
|
|
20
|
+
from ..trading_parties.models import Certificate
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
@dataclass(frozen=True, slots=True)
|
|
22
|
-
class
|
|
24
|
+
class DocumentBag:
|
|
23
25
|
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
se
|
|
26
|
+
Bolsa de un documento tributario.
|
|
27
|
+
|
|
28
|
+
Devuelta por `document.builder::build`/`document.loader::loadXml`
|
|
29
|
+
— ambas en esta misma forma. `document`
|
|
30
|
+
(`Encabezado`/`Detalle`, con el merge de `libredte_data.extra.dte`
|
|
31
|
+
si se pasó al renderizar) es la vista "de datos" limpia del
|
|
32
|
+
documento — SIN los campos opcionales no usados (se omiten, no
|
|
33
|
+
quedan en `False`). `document_normalized` es la data cruda tal como
|
|
34
|
+
la usa el builder internamente para generar el XML (con esos
|
|
35
|
+
campos opcionales en `False` en vez de omitidos): es la que hay que
|
|
36
|
+
guardar para poder reconstruir el documento después con
|
|
37
|
+
`options={'normalizer': {'normalize': False}}` sin volver a
|
|
38
|
+
normalizar — usar `document` para eso corrompe la reconstrucción.
|
|
39
|
+
|
|
40
|
+
`document_id`/`xml_base64` dan el documento construido completo —
|
|
41
|
+
puede ser un borrador, un documento timbrado, o timbrado y firmado,
|
|
42
|
+
no una subclase distinta: es el mismo recurso en distintos estados
|
|
43
|
+
según qué datos se le hayan pasado a `DocumentBuilderService`. Un
|
|
44
|
+
borrador sin folio asignado tiene folio `0` en `document_id` (ver
|
|
45
|
+
`AbstractDocument::getId()` en la biblioteca), nunca lanza error.
|
|
46
|
+
|
|
47
|
+
`document_stamp_base64` es el TED — `None` si el documento aún no
|
|
48
|
+
está timbrado (ver `is_timbrado`). `document_extra` son los campos
|
|
49
|
+
que el tipo de documento no admite en el XML (ej. `TermPagoGlosa`
|
|
50
|
+
en una boleta) pero sí se pasaron como `libredte_data` al construir.
|
|
51
|
+
`document_auth` viene poblado solo si el emisor incluyó su
|
|
52
|
+
autorización SII.
|
|
53
|
+
|
|
54
|
+
`options` son las opciones efectivas de la bolsa (`builder`/
|
|
55
|
+
`normalizer`/`parser`/`renderer`/`sanitizer`/`validator`), tal como
|
|
56
|
+
las devuelve la API — vacías (`[]`) para el worker que no recibió
|
|
57
|
+
ninguna al construir/cargar el documento.
|
|
58
|
+
|
|
59
|
+
`certificate`/`caf` son `None` salvo que el documento se haya
|
|
60
|
+
construido con ellos (`build_signed`, o un `loadXml` de un XML ya
|
|
61
|
+
firmado/timbrado) — un borrador (`build_draft`) no los trae.
|
|
62
|
+
`emisor`/`receptor` sí están siempre que haya un documento
|
|
63
|
+
construido (`Encabezado.Emisor`/`Encabezado.Receptor` ya
|
|
64
|
+
resueltos como entidad, con todos sus campos — no solo los que
|
|
65
|
+
trae `document`). `timbre` es el TED ya parseado como `dict`
|
|
66
|
+
(`document_stamp_base64` es el mismo dato, pero como XML) — `None`
|
|
67
|
+
si el documento aún no está timbrado.
|
|
68
|
+
|
|
69
|
+
No usa `XmlPayloadMixin`: acá `xml_base64` es una `@property` (el
|
|
70
|
+
documento puede no estar construido), no un campo simple.
|
|
30
71
|
"""
|
|
31
72
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
document: dict[str, Any]
|
|
74
|
+
document_normalized: dict[str, Any] | None
|
|
75
|
+
document_type: dict[str, Any]
|
|
76
|
+
document_stamp_base64: str | None
|
|
77
|
+
document_extra: dict[str, Any] | None
|
|
78
|
+
document_auth: dict[str, Any] | None
|
|
79
|
+
document_id: str | None
|
|
80
|
+
document_xml_base64: str | None
|
|
81
|
+
options: dict[str, Any]
|
|
82
|
+
certificate: Certificate | None
|
|
83
|
+
emisor: dict[str, Any] | None
|
|
84
|
+
receptor: dict[str, Any] | None
|
|
85
|
+
caf: Caf | None
|
|
86
|
+
timbre: dict[str, Any] | None
|
|
87
|
+
raw: dict[str, Any]
|
|
36
88
|
|
|
37
89
|
@property
|
|
38
90
|
def is_timbrado(self) -> bool:
|
|
39
91
|
"""Si el documento ya tiene Timbre Electrónico (TED)."""
|
|
40
|
-
return self.
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def from_api(cls, data: dict[str, Any]) -> Document:
|
|
44
|
-
"""Construye un `Document` desde el `data` que devuelve la API."""
|
|
45
|
-
return cls(
|
|
46
|
-
id=data['id'],
|
|
47
|
-
datos=data.get('datos') or {},
|
|
48
|
-
ted=data.get('ted'),
|
|
49
|
-
xml_base64=data['xml'],
|
|
50
|
-
)
|
|
92
|
+
return self.document_stamp_base64 is not None
|
|
51
93
|
|
|
94
|
+
@property
|
|
95
|
+
def xml_base64(self) -> str:
|
|
96
|
+
"""XML completo del documento construido, en base64."""
|
|
97
|
+
if self.document_xml_base64 is None:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
'Este DocumentBag no tiene un documento construido — '
|
|
100
|
+
'¿se llamó a `builder.build_draft/build_signed()` o a '
|
|
101
|
+
'`loader.load_xml()`?',
|
|
102
|
+
)
|
|
103
|
+
return self.document_xml_base64
|
|
52
104
|
|
|
53
|
-
@
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
No trae el XML del documento ni `id` — solo la bolsa ya
|
|
59
|
-
normalizada: `document` (`Encabezado`/`Detalle`), `document_type`
|
|
60
|
-
(metadatos del tipo de documento: `codigo`, `nombre`, `categoria`,
|
|
61
|
-
`es_boleta`, etc., sin tipar) y `document_stamp` (el TED, como XML
|
|
62
|
-
plano, no en base64 ni parseado). Sirve para reconstruir los datos
|
|
63
|
-
de un DTE ya emitido a partir de su XML.
|
|
64
|
-
"""
|
|
105
|
+
@property
|
|
106
|
+
def xml_bytes(self) -> bytes:
|
|
107
|
+
"""XML del documento construido, decodificado desde base64."""
|
|
108
|
+
return base64.b64decode(self.xml_base64)
|
|
65
109
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
document_auth: dict[str, Any] | None
|
|
71
|
-
raw: dict[str, Any]
|
|
110
|
+
@property
|
|
111
|
+
def xml(self) -> str:
|
|
112
|
+
"""XML del documento construido, como texto (ISO-8859-1)."""
|
|
113
|
+
return self.xml_bytes.decode('iso-8859-1')
|
|
72
114
|
|
|
73
115
|
@classmethod
|
|
74
116
|
def from_api(cls, data: dict[str, Any]) -> DocumentBag:
|
|
75
117
|
"""Construye un `DocumentBag` desde el `data` de la API."""
|
|
76
118
|
return cls(
|
|
77
119
|
document=data['document'],
|
|
120
|
+
document_normalized=data.get('document_normalized'),
|
|
78
121
|
document_type=data['document_type'],
|
|
79
|
-
|
|
122
|
+
document_stamp_base64=data.get('document_stamp'),
|
|
80
123
|
document_extra=data.get('document_extra'),
|
|
81
124
|
document_auth=data.get('document_auth'),
|
|
125
|
+
document_id=data.get('document_id'),
|
|
126
|
+
document_xml_base64=data.get('document_xml'),
|
|
127
|
+
options=data.get('options') or {},
|
|
128
|
+
certificate=Certificate.from_api(data['certificate'])
|
|
129
|
+
if data.get('certificate')
|
|
130
|
+
else None,
|
|
131
|
+
emisor=data.get('emisor'),
|
|
132
|
+
receptor=data.get('receptor'),
|
|
133
|
+
caf=Caf.from_api(data['caf']) if data.get('caf') else None,
|
|
134
|
+
timbre=data.get('timbre'),
|
|
82
135
|
raw=data,
|
|
83
136
|
)
|
|
84
137
|
|
|
85
138
|
|
|
86
139
|
@dataclass(frozen=True, slots=True)
|
|
87
140
|
class DocumentEnvelope(XmlPayloadMixin):
|
|
88
|
-
"""
|
|
141
|
+
"""
|
|
142
|
+
Sobre `EnvioDTE`, ya sea recién construido o cargado desde un XML.
|
|
143
|
+
|
|
144
|
+
`documents` trae una bolsa (`DocumentBag`) por cada documento
|
|
145
|
+
tributario que el sobre contiene — uno solo si se armó con
|
|
146
|
+
`create()`, uno o más si se armó con `createMany()` o si se cargó
|
|
147
|
+
con `loadXml()` un sobre que agrupaba varios documentos. `caratula`
|
|
148
|
+
es el `dict` con los datos de la carátula del sobre (`RutEmisor`,
|
|
149
|
+
`RutEnvia`, `RutReceptor`, `FchResol`, `NroResol`, `TmstFirmaEnv`,
|
|
150
|
+
`SubTotDTE`).
|
|
151
|
+
"""
|
|
89
152
|
|
|
90
153
|
tag: str
|
|
91
154
|
xml_base64: str
|
|
155
|
+
documents: tuple[DocumentBag, ...]
|
|
156
|
+
caratula: dict[str, Any] | None
|
|
92
157
|
|
|
93
158
|
@classmethod
|
|
94
159
|
def from_api(cls, data: dict[str, Any]) -> DocumentEnvelope:
|
|
95
160
|
"""Construye un `DocumentEnvelope` desde el `data` de la API."""
|
|
96
|
-
return cls(
|
|
161
|
+
return cls(
|
|
162
|
+
tag=data['tag'],
|
|
163
|
+
xml_base64=data['xml'],
|
|
164
|
+
documents=tuple(
|
|
165
|
+
DocumentBag.from_api(item)
|
|
166
|
+
for item in data.get('documents') or []
|
|
167
|
+
),
|
|
168
|
+
caratula=data.get('caratula'),
|
|
169
|
+
)
|
|
97
170
|
|
|
98
171
|
|
|
99
172
|
@dataclass(frozen=True, slots=True)
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/renderer.py
RENAMED
|
@@ -39,7 +39,7 @@ class DocumentRendererService:
|
|
|
39
39
|
|
|
40
40
|
Sirve tanto para un borrador como para un documento ya timbrado y
|
|
41
41
|
firmado: el resultado depende solo del XML que se le pase (ej.
|
|
42
|
-
`
|
|
42
|
+
`DocumentBag.xml_base64`).
|
|
43
43
|
|
|
44
44
|
Sin `renderings`, la API genera una única copia `'tributaria'`
|
|
45
45
|
(comportamiento por defecto). `renderings` pide presentaciones y
|
{libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/validator.py
RENAMED
|
@@ -14,7 +14,7 @@ class DocumentValidatorService:
|
|
|
14
14
|
"""
|
|
15
15
|
Valida un documento tributario ya construido (`billing.document`).
|
|
16
16
|
|
|
17
|
-
`source` es el XML del documento en base64 (ej. `
|
|
17
|
+
`source` es el XML del documento en base64 (ej. `DocumentBag.xml_base64`
|
|
18
18
|
de un documento ya construido) — la API acepta también la bolsa o el
|
|
19
19
|
documento ya cargado, pero el SDK solo necesita pasar el XML.
|
|
20
20
|
"""
|