libredte-lib-sdk 0.1.0__py3-none-any.whl
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/__init__.py +75 -0
- libredte_lib_sdk/billing/__init__.py +31 -0
- libredte_lib_sdk/billing/common.py +29 -0
- libredte_lib_sdk/billing/document/__init__.py +39 -0
- libredte_lib_sdk/billing/document/builder.py +68 -0
- libredte_lib_sdk/billing/document/dispatcher.py +49 -0
- libredte_lib_sdk/billing/document/models.py +169 -0
- libredte_lib_sdk/billing/document/renderer.py +60 -0
- libredte_lib_sdk/billing/enums.py +18 -0
- libredte_lib_sdk/billing/identifier/__init__.py +27 -0
- libredte_lib_sdk/billing/identifier/caf_faker.py +49 -0
- libredte_lib_sdk/billing/identifier/caf_loader.py +31 -0
- libredte_lib_sdk/billing/identifier/caf_validator.py +36 -0
- libredte_lib_sdk/billing/identifier/models.py +37 -0
- libredte_lib_sdk/billing/integration/__init__.py +17 -0
- libredte_lib_sdk/billing/integration/models.py +46 -0
- libredte_lib_sdk/billing/integration/sii_dte.py +83 -0
- libredte_lib_sdk/billing/trading_parties/__init__.py +21 -0
- libredte_lib_sdk/billing/trading_parties/mandatario_manager.py +45 -0
- libredte_lib_sdk/billing/trading_parties/models.py +38 -0
- libredte_lib_sdk/client.py +184 -0
- libredte_lib_sdk/exceptions.py +117 -0
- libredte_lib_sdk/py.typed +0 -0
- libredte_lib_sdk/sdk.py +79 -0
- libredte_lib_sdk-0.1.0.dist-info/METADATA +878 -0
- libredte_lib_sdk-0.1.0.dist-info/RECORD +28 -0
- libredte_lib_sdk-0.1.0.dist-info/WHEEL +4 -0
- libredte_lib_sdk-0.1.0.dist-info/licenses/COPYING +661 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""SDK Python para la API de LibreDTE Lib (facturación electrónica Chile)."""
|
|
2
|
+
|
|
3
|
+
from .billing import BillingPackage
|
|
4
|
+
from .billing.document import (
|
|
5
|
+
AutorizacionDte,
|
|
6
|
+
Document,
|
|
7
|
+
DocumentBuilderService,
|
|
8
|
+
DocumentComponent,
|
|
9
|
+
DocumentDispatcherService,
|
|
10
|
+
DocumentRendererService,
|
|
11
|
+
Emisor,
|
|
12
|
+
Envelope,
|
|
13
|
+
Rendering,
|
|
14
|
+
RenderResult,
|
|
15
|
+
)
|
|
16
|
+
from .billing.enums import SiiEnvironment
|
|
17
|
+
from .billing.identifier import (
|
|
18
|
+
Caf,
|
|
19
|
+
CafFakerService,
|
|
20
|
+
CafLoaderService,
|
|
21
|
+
CafValidatorService,
|
|
22
|
+
IdentifierComponent,
|
|
23
|
+
)
|
|
24
|
+
from .billing.integration import (
|
|
25
|
+
IntegrationComponent,
|
|
26
|
+
SendResult,
|
|
27
|
+
SiiDteService,
|
|
28
|
+
SiiStatus,
|
|
29
|
+
)
|
|
30
|
+
from .billing.trading_parties import (
|
|
31
|
+
Certificate,
|
|
32
|
+
MandatarioManagerService,
|
|
33
|
+
TradingPartiesComponent,
|
|
34
|
+
)
|
|
35
|
+
from .client import ApiClient
|
|
36
|
+
from .exceptions import (
|
|
37
|
+
LibreDteApiError,
|
|
38
|
+
LibreDteConnectionError,
|
|
39
|
+
LibreDteRateLimitError,
|
|
40
|
+
LibreDteSdkError,
|
|
41
|
+
)
|
|
42
|
+
from .sdk import LibreDTE
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
'ApiClient',
|
|
46
|
+
'AutorizacionDte',
|
|
47
|
+
'BillingPackage',
|
|
48
|
+
'Caf',
|
|
49
|
+
'CafFakerService',
|
|
50
|
+
'CafLoaderService',
|
|
51
|
+
'CafValidatorService',
|
|
52
|
+
'Certificate',
|
|
53
|
+
'Document',
|
|
54
|
+
'DocumentBuilderService',
|
|
55
|
+
'DocumentComponent',
|
|
56
|
+
'DocumentDispatcherService',
|
|
57
|
+
'DocumentRendererService',
|
|
58
|
+
'Emisor',
|
|
59
|
+
'Envelope',
|
|
60
|
+
'IdentifierComponent',
|
|
61
|
+
'IntegrationComponent',
|
|
62
|
+
'LibreDTE',
|
|
63
|
+
'LibreDteApiError',
|
|
64
|
+
'LibreDteConnectionError',
|
|
65
|
+
'LibreDteRateLimitError',
|
|
66
|
+
'LibreDteSdkError',
|
|
67
|
+
'MandatarioManagerService',
|
|
68
|
+
'RenderResult',
|
|
69
|
+
'Rendering',
|
|
70
|
+
'SendResult',
|
|
71
|
+
'SiiDteService',
|
|
72
|
+
'SiiEnvironment',
|
|
73
|
+
'SiiStatus',
|
|
74
|
+
'TradingPartiesComponent',
|
|
75
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Paquete `billing`: facturación electrónica de Chile.
|
|
3
|
+
|
|
4
|
+
Organizado igual que la propia API (`paquete.componente.worker::operacion`):
|
|
5
|
+
un subpaquete por componente (`document`, `identifier`, `trading_parties`,
|
|
6
|
+
`integration`), cada uno con un `*Component` que agrupa sus servicios por
|
|
7
|
+
worker. Agregar un worker nuevo (de un componente ya soportado, u otro
|
|
8
|
+
componente/paquete de la API) es agregar un archivo y registrarlo en el
|
|
9
|
+
`__init__.py` de su componente — no toca nada de lo demás.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ..client import ApiClient
|
|
15
|
+
from .document import DocumentComponent
|
|
16
|
+
from .identifier import IdentifierComponent
|
|
17
|
+
from .integration import IntegrationComponent
|
|
18
|
+
from .trading_parties import TradingPartiesComponent
|
|
19
|
+
|
|
20
|
+
__all__ = ['BillingPackage']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BillingPackage:
|
|
24
|
+
"""Agrupa los componentes del paquete `billing` de la API."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client: ApiClient) -> None:
|
|
27
|
+
"""Crea los componentes del paquete sobre el `ApiClient` dado."""
|
|
28
|
+
self.document = DocumentComponent(client)
|
|
29
|
+
self.identifier = IdentifierComponent(client)
|
|
30
|
+
self.trading_parties = TradingPartiesComponent(client)
|
|
31
|
+
self.integration = IntegrationComponent(client)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Utilidades compartidas entre los componentes de `billing`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class XmlPayloadMixin:
|
|
9
|
+
"""
|
|
10
|
+
Mixin para un DTO cuyo dato principal es un XML en base64.
|
|
11
|
+
|
|
12
|
+
Espera un atributo `xml_base64: str` en la clase concreta (`Document`,
|
|
13
|
+
`Envelope`, `Caf`, ...) y expone `xml_bytes`/`xml` sobre él, para no
|
|
14
|
+
repetir esta misma conversión en cada DTO.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__slots__ = ()
|
|
18
|
+
|
|
19
|
+
xml_base64: str
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def xml_bytes(self) -> bytes:
|
|
23
|
+
"""XML decodificado desde base64."""
|
|
24
|
+
return base64.b64decode(self.xml_base64)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def xml(self) -> str:
|
|
28
|
+
"""XML como texto (ISO-8859-1, la codificación que usa el SII)."""
|
|
29
|
+
return self.xml_bytes.decode('iso-8859-1')
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Componente `billing.document`: DTE — construcción, sobre y render."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ...client import ApiClient
|
|
6
|
+
from .builder import DocumentBuilderService
|
|
7
|
+
from .dispatcher import DocumentDispatcherService
|
|
8
|
+
from .models import (
|
|
9
|
+
AutorizacionDte,
|
|
10
|
+
Document,
|
|
11
|
+
Emisor,
|
|
12
|
+
Envelope,
|
|
13
|
+
Rendering,
|
|
14
|
+
RenderResult,
|
|
15
|
+
)
|
|
16
|
+
from .renderer import DocumentRendererService
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'AutorizacionDte',
|
|
20
|
+
'Document',
|
|
21
|
+
'DocumentBuilderService',
|
|
22
|
+
'DocumentComponent',
|
|
23
|
+
'DocumentDispatcherService',
|
|
24
|
+
'DocumentRendererService',
|
|
25
|
+
'Emisor',
|
|
26
|
+
'Envelope',
|
|
27
|
+
'RenderResult',
|
|
28
|
+
'Rendering',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DocumentComponent:
|
|
33
|
+
"""Agrupa los servicios de `billing.document`."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, client: ApiClient) -> None:
|
|
36
|
+
"""Crea los servicios del componente sobre el `ApiClient` dado."""
|
|
37
|
+
self.builder = DocumentBuilderService(client)
|
|
38
|
+
self.dispatcher = DocumentDispatcherService(client)
|
|
39
|
+
self.renderer = DocumentRendererService(client)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Servicio para `billing.document.builder`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...client import ApiClient
|
|
8
|
+
from ..trading_parties.models import Certificate
|
|
9
|
+
from .models import Document
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DocumentBuilderService:
|
|
13
|
+
"""
|
|
14
|
+
Construye documentos tributarios (`billing.document.builder`).
|
|
15
|
+
|
|
16
|
+
El mismo worker de la API sirve tanto para un borrador como para el
|
|
17
|
+
documento timbrado y firmado, según qué datos se le pasen — acá se
|
|
18
|
+
separa en dos métodos explícitos para que la intención de cada
|
|
19
|
+
llamada quede clara en el código que la usa.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_OPERATION = 'billing.document.builder::build'
|
|
23
|
+
|
|
24
|
+
def __init__(self, client: ApiClient) -> None:
|
|
25
|
+
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
26
|
+
self._client = client
|
|
27
|
+
|
|
28
|
+
def build_draft(self, parsed_data: dict[str, Any]) -> Document:
|
|
29
|
+
"""
|
|
30
|
+
Emite el borrador de un DTE a partir de datos ya normalizados.
|
|
31
|
+
|
|
32
|
+
`parsed_data` es el `Encabezado`/`Detalle` (y demás nodos) del
|
|
33
|
+
formato DTE del SII, incluyendo `Encabezado.IdDoc.Folio` (el SDK
|
|
34
|
+
no asigna folios: eso lo decide quien llama, típicamente porque
|
|
35
|
+
lleva el correlativo). Sin CAF ni certificado, el resultado no
|
|
36
|
+
queda timbrado (`Document.is_timbrado` es `False`).
|
|
37
|
+
"""
|
|
38
|
+
data = self._client.call(
|
|
39
|
+
self._OPERATION,
|
|
40
|
+
bag={'parsedData': parsed_data},
|
|
41
|
+
)
|
|
42
|
+
return Document.from_api(data)
|
|
43
|
+
|
|
44
|
+
def build_signed(
|
|
45
|
+
self,
|
|
46
|
+
parsed_data: dict[str, Any],
|
|
47
|
+
*,
|
|
48
|
+
caf_xml: str,
|
|
49
|
+
certificate: Certificate,
|
|
50
|
+
) -> Document:
|
|
51
|
+
"""
|
|
52
|
+
Genera el DTE real, timbrado y firmado.
|
|
53
|
+
|
|
54
|
+
Requiere un CAF real (XML tal como lo entrega el SII, cubriendo
|
|
55
|
+
el folio indicado en `parsed_data`) y el certificado digital del
|
|
56
|
+
emisor. Para pruebas, ambos se pueden generar con
|
|
57
|
+
`IdentifierComponent.caf_faker` y
|
|
58
|
+
`TradingPartiesComponent.mandatario_manager`.
|
|
59
|
+
"""
|
|
60
|
+
data = self._client.call(
|
|
61
|
+
self._OPERATION,
|
|
62
|
+
bag={
|
|
63
|
+
'parsedData': parsed_data,
|
|
64
|
+
'caf': caf_xml,
|
|
65
|
+
'certificate': certificate.to_payload(),
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
return Document.from_api(data)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Servicio para `billing.document.dispatcher`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ...client import ApiClient
|
|
6
|
+
from ..trading_parties.models import Certificate
|
|
7
|
+
from .models import Emisor, Envelope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DocumentDispatcherService:
|
|
11
|
+
"""
|
|
12
|
+
Arma el sobre `EnvioDTE` (`billing.document.dispatcher`).
|
|
13
|
+
|
|
14
|
+
Solo cubre `create`: envolver y firmar un documento ya construido.
|
|
15
|
+
El worker también expone `loadXml`/`validate`/`validateSchema`/
|
|
16
|
+
`validateSignature`, no incluidas acá.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_CREATE_OPERATION = 'billing.document.dispatcher::create'
|
|
20
|
+
|
|
21
|
+
def __init__(self, client: ApiClient) -> None:
|
|
22
|
+
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
23
|
+
self._client = client
|
|
24
|
+
|
|
25
|
+
def create(
|
|
26
|
+
self,
|
|
27
|
+
document_xml_base64: str,
|
|
28
|
+
*,
|
|
29
|
+
certificate: Certificate,
|
|
30
|
+
emisor: Emisor,
|
|
31
|
+
) -> Envelope:
|
|
32
|
+
"""
|
|
33
|
+
Envuelve y firma un documento ya timbrado en un sobre `EnvioDTE`.
|
|
34
|
+
|
|
35
|
+
`document_xml_base64` es el XML en base64 del documento a
|
|
36
|
+
envolver — típicamente `Document.xml_base64` de un documento ya
|
|
37
|
+
timbrado y firmado (`DocumentBuilderService.build_signed`). El
|
|
38
|
+
sobre resultante es lo que se envía al SII
|
|
39
|
+
(`SiiDteService.send`).
|
|
40
|
+
"""
|
|
41
|
+
data = self._client.call(
|
|
42
|
+
self._CREATE_OPERATION,
|
|
43
|
+
bag={
|
|
44
|
+
'xmlDocument': document_xml_base64,
|
|
45
|
+
'certificate': certificate.to_payload(),
|
|
46
|
+
'emisor': emisor.to_payload(),
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
return Envelope.from_api(data)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DTO del componente `billing.document`.
|
|
3
|
+
|
|
4
|
+
No reimplementan el esquema DTE del SII (`Encabezado`/`Detalle`/etc. se
|
|
5
|
+
pasan como `dict` tal cual ese formato, ver `builder.py`): solo tipan lo
|
|
6
|
+
que el propio SDK arma o recibe de la API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ..common import XmlPayloadMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class AutorizacionDte:
|
|
20
|
+
"""Resolución del SII que autoriza al emisor a emitir DTE."""
|
|
21
|
+
|
|
22
|
+
fecha_resolucion: str
|
|
23
|
+
numero_resolucion: int
|
|
24
|
+
|
|
25
|
+
def to_payload(self) -> dict[str, Any]:
|
|
26
|
+
"""Payload esperado por la API para la autorización."""
|
|
27
|
+
return {
|
|
28
|
+
'fecha_resolucion': self.fecha_resolucion,
|
|
29
|
+
'numero_resolucion': self.numero_resolucion,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class Emisor:
|
|
35
|
+
"""Datos del emisor requeridos para armar el sobre `EnvioDTE`."""
|
|
36
|
+
|
|
37
|
+
rut: str
|
|
38
|
+
razon_social: str
|
|
39
|
+
autorizacion_dte: AutorizacionDte
|
|
40
|
+
|
|
41
|
+
def to_payload(self) -> dict[str, Any]:
|
|
42
|
+
"""Payload esperado por la API para el emisor del sobre."""
|
|
43
|
+
return {
|
|
44
|
+
'rut': self.rut,
|
|
45
|
+
'razon_social': self.razon_social,
|
|
46
|
+
'autorizacion_dte': self.autorizacion_dte.to_payload(),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class Document(XmlPayloadMixin):
|
|
52
|
+
"""
|
|
53
|
+
Documento tributario construido.
|
|
54
|
+
|
|
55
|
+
Puede ser un borrador, un documento timbrado, o timbrado y firmado —
|
|
56
|
+
lo distingue `is_timbrado` (`ted is not None`), no una subclase
|
|
57
|
+
distinta: es el mismo recurso en distintos estados, según qué datos
|
|
58
|
+
se le hayan pasado a `DocumentBuilderService`.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
id: str
|
|
62
|
+
datos: dict[str, Any]
|
|
63
|
+
ted: dict[str, Any] | None
|
|
64
|
+
xml_base64: str
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def is_timbrado(self) -> bool:
|
|
68
|
+
"""Si el documento ya tiene Timbre Electrónico (TED)."""
|
|
69
|
+
return self.ted is not None
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_api(cls, data: dict[str, Any]) -> Document:
|
|
73
|
+
"""Construye un `Document` desde el `data` que devuelve la API."""
|
|
74
|
+
return cls(
|
|
75
|
+
id=data['id'],
|
|
76
|
+
datos=data.get('datos') or {},
|
|
77
|
+
ted=data.get('ted'),
|
|
78
|
+
xml_base64=data['xml'],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class Envelope(XmlPayloadMixin):
|
|
84
|
+
"""Sobre `EnvioDTE` construido y firmado, listo para enviar al SII."""
|
|
85
|
+
|
|
86
|
+
tag: str
|
|
87
|
+
xml_base64: str
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_api(cls, data: dict[str, Any]) -> Envelope:
|
|
91
|
+
"""Construye un `Envelope` desde el `data` que devuelve la API."""
|
|
92
|
+
return cls(tag=data['tag'], xml_base64=data['xml'])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class Rendering:
|
|
97
|
+
"""
|
|
98
|
+
Un archivo generado por `DocumentRendererService.render()`.
|
|
99
|
+
|
|
100
|
+
`content_base64` es el archivo completo en base64 (un PDF, un HTML,
|
|
101
|
+
lo que sea — `mime_type` dice qué es). `label` es la presentación
|
|
102
|
+
pedida en `renderings` (ej. `'tributaria'`, `'cedible'`); `copies`/
|
|
103
|
+
`copy_number` identifican esta copia entre las pedidas de ese mismo
|
|
104
|
+
`label` (ej. `copies=2, copy_number=1` es la primera de 2 copias
|
|
105
|
+
tributarias). El nombre de los campos en la API (`content`/
|
|
106
|
+
`mimeType`/`filename`/`copyNumber`) es camelCase; acá quedan en
|
|
107
|
+
snake_case como el resto del SDK.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
content_base64: str
|
|
111
|
+
mime_type: str
|
|
112
|
+
filename: str
|
|
113
|
+
label: str
|
|
114
|
+
copies: int
|
|
115
|
+
copy_number: int
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def content_bytes(self) -> bytes:
|
|
119
|
+
"""Contenido del archivo, decodificado desde base64."""
|
|
120
|
+
return base64.b64decode(self.content_base64)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_api(cls, data: dict[str, Any]) -> Rendering:
|
|
124
|
+
"""Construye un `Rendering` desde un ítem de `data.renderings`."""
|
|
125
|
+
return cls(
|
|
126
|
+
content_base64=data['content'],
|
|
127
|
+
mime_type=data['mimeType'],
|
|
128
|
+
filename=data['filename'],
|
|
129
|
+
label=data['label'],
|
|
130
|
+
copies=data['copies'],
|
|
131
|
+
copy_number=data['copyNumber'],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True, slots=True)
|
|
136
|
+
class RenderResult:
|
|
137
|
+
"""
|
|
138
|
+
Resultado de `DocumentRendererService.render()`.
|
|
139
|
+
|
|
140
|
+
`renderings` trae un ítem por copia generada: por defecto (sin pedir
|
|
141
|
+
`renderings` explícito) es un único `'tributaria'`; pidiendo más de
|
|
142
|
+
una presentación y/o copia (ej. `renderings={'tributaria': 2,
|
|
143
|
+
'cedible': 1}`), trae uno por cada copia efectivamente generada —
|
|
144
|
+
una presentación que la API no pudo generar para ese documento (ej.
|
|
145
|
+
`'cedible'` en un tipo de documento sin acuse de recibo) se omite en
|
|
146
|
+
silencio, no rompe la llamada. `.first` es un atajo para el caso más
|
|
147
|
+
común (un solo archivo); `.by_label()` filtra por presentación
|
|
148
|
+
cuando se pidió más de una.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
renderings: tuple[Rendering, ...]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def first(self) -> Rendering:
|
|
155
|
+
"""El primer archivo generado."""
|
|
156
|
+
return self.renderings[0]
|
|
157
|
+
|
|
158
|
+
def by_label(self, label: str) -> tuple[Rendering, ...]:
|
|
159
|
+
"""Los renderings de una presentación (ej. `'tributaria'`)."""
|
|
160
|
+
return tuple(r for r in self.renderings if r.label == label)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_api(cls, data: dict[str, Any]) -> RenderResult:
|
|
164
|
+
"""Construye un `RenderResult` desde el `data` que devuelve la API."""
|
|
165
|
+
return cls(
|
|
166
|
+
renderings=tuple(
|
|
167
|
+
Rendering.from_api(item) for item in data['renderings']
|
|
168
|
+
),
|
|
169
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Servicio para `billing.document.renderer`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...client import ApiClient
|
|
8
|
+
from .models import RenderResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DocumentRendererService:
|
|
12
|
+
"""
|
|
13
|
+
Renderiza un documento tributario (`billing.document.renderer`).
|
|
14
|
+
|
|
15
|
+
Soporta `format='html'` y `format='pdf'`. La API devuelve siempre
|
|
16
|
+
`data.renderings`, una lista de archivos en base64 — ver
|
|
17
|
+
`RenderResult`/`Rendering` en `models.py`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_OPERATION = 'billing.document.renderer::render'
|
|
21
|
+
|
|
22
|
+
def __init__(self, client: ApiClient) -> None:
|
|
23
|
+
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
def render(
|
|
27
|
+
self,
|
|
28
|
+
document_xml_base64: str,
|
|
29
|
+
*,
|
|
30
|
+
format: str = 'pdf', # noqa: A002
|
|
31
|
+
renderings: dict[str, int] | None = None,
|
|
32
|
+
) -> RenderResult:
|
|
33
|
+
"""
|
|
34
|
+
Genera el PDF (u otro formato soportado por la API) de un documento.
|
|
35
|
+
|
|
36
|
+
Sirve tanto para un borrador como para un documento ya timbrado y
|
|
37
|
+
firmado: el resultado depende solo del XML que se le pase (ej.
|
|
38
|
+
`Document.xml_base64`).
|
|
39
|
+
|
|
40
|
+
Sin `renderings`, la API genera una única copia `'tributaria'`
|
|
41
|
+
(comportamiento por defecto). `renderings` pide presentaciones y
|
|
42
|
+
cantidad de copias de cada una — ej. `{'tributaria': 1,
|
|
43
|
+
'cedible': 1}` — y la API rechaza (`LibreDteApiError`, 500) una
|
|
44
|
+
presentación que no existe, o si ninguna de las pedidas pudo
|
|
45
|
+
generarse (ej. pedir solo `'cedible'` para un tipo de documento
|
|
46
|
+
sin acuse de recibo). Ver `RenderResult` para cómo se identifica
|
|
47
|
+
cada copia en la respuesta.
|
|
48
|
+
"""
|
|
49
|
+
renderer_options: dict[str, Any] = {'format': format}
|
|
50
|
+
if renderings is not None:
|
|
51
|
+
renderer_options['renderings'] = renderings
|
|
52
|
+
|
|
53
|
+
data = self._client.call(
|
|
54
|
+
self._OPERATION,
|
|
55
|
+
bag={
|
|
56
|
+
'xmlDocument': document_xml_base64,
|
|
57
|
+
'options': {'renderer': renderer_options},
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
return RenderResult.from_api(data)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Enumeraciones del paquete `billing`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SiiEnvironment(IntEnum):
|
|
9
|
+
"""
|
|
10
|
+
Ambiente del SII contra el que se opera.
|
|
11
|
+
|
|
12
|
+
Convención estándar del ecosistema LibreDTE. El default es
|
|
13
|
+
`CERTIFICATION` a propósito: nadie debería terminar enviando algo a
|
|
14
|
+
producción por haber omitido este argumento.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
CERTIFICATION = 0
|
|
18
|
+
PRODUCTION = 1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Componente `billing.identifier`: folios de documentos tributarios."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ...client import ApiClient
|
|
6
|
+
from .caf_faker import CafFakerService
|
|
7
|
+
from .caf_loader import CafLoaderService
|
|
8
|
+
from .caf_validator import CafValidatorService
|
|
9
|
+
from .models import Caf
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'Caf',
|
|
13
|
+
'CafFakerService',
|
|
14
|
+
'CafLoaderService',
|
|
15
|
+
'CafValidatorService',
|
|
16
|
+
'IdentifierComponent',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IdentifierComponent:
|
|
21
|
+
"""Agrupa los servicios de `billing.identifier`."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, client: ApiClient) -> None:
|
|
24
|
+
"""Crea los servicios del componente sobre el `ApiClient` dado."""
|
|
25
|
+
self.caf_faker = CafFakerService(client)
|
|
26
|
+
self.caf_loader = CafLoaderService(client)
|
|
27
|
+
self.caf_validator = CafValidatorService(client)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Servicio para `billing.identifier.caf_faker`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...client import ApiClient
|
|
8
|
+
from .models import Caf
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CafFakerService:
|
|
12
|
+
"""
|
|
13
|
+
Genera CAF ficticios (`billing.identifier.caf_faker::create`).
|
|
14
|
+
|
|
15
|
+
Solo para pruebas/desarrollo: el CAF resultante no está autorizado
|
|
16
|
+
de verdad por el SII, pero permite ejercitar el timbrado/firma de un
|
|
17
|
+
documento (y, con eso, el resto del flujo) sin depender de un folio
|
|
18
|
+
real. El SDK lo usa así para poder probarse de punta a punta.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_OPERATION = 'billing.identifier.caf_faker::create'
|
|
22
|
+
|
|
23
|
+
def __init__(self, client: ApiClient) -> None:
|
|
24
|
+
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
25
|
+
self._client = client
|
|
26
|
+
|
|
27
|
+
def create(
|
|
28
|
+
self,
|
|
29
|
+
emisor: dict[str, Any],
|
|
30
|
+
*,
|
|
31
|
+
codigo_documento: int,
|
|
32
|
+
folio_desde: int = 1,
|
|
33
|
+
folio_hasta: int | None = None,
|
|
34
|
+
) -> Caf:
|
|
35
|
+
"""
|
|
36
|
+
Genera un CAF ficticio para `emisor` y `codigo_documento`.
|
|
37
|
+
|
|
38
|
+
`emisor` es un `dict` con `rut`/`razon_social`, tal como lo
|
|
39
|
+
espera la API. `folio_hasta` por defecto cubre solo
|
|
40
|
+
`folio_desde` (un único folio), igual que hace la API.
|
|
41
|
+
"""
|
|
42
|
+
data = self._client.call(
|
|
43
|
+
self._OPERATION,
|
|
44
|
+
emisor=emisor,
|
|
45
|
+
codigoDocumento=codigo_documento,
|
|
46
|
+
folioDesde=folio_desde,
|
|
47
|
+
folioHasta=folio_hasta,
|
|
48
|
+
)
|
|
49
|
+
return Caf.from_api(data)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Servicio para `billing.identifier.caf_loader`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ...client import ApiClient
|
|
6
|
+
from .models import Caf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CafLoaderService:
|
|
10
|
+
"""
|
|
11
|
+
Carga un CAF real (`billing.identifier.caf_loader::load`).
|
|
12
|
+
|
|
13
|
+
A diferencia de `CafFakerService` (que genera un CAF ficticio para
|
|
14
|
+
pruebas), esto carga el XML de un CAF **real** — el que el SII le
|
|
15
|
+
entrega al emisor y que la app que use este SDK deja subir al
|
|
16
|
+
usuario — y lo entrega como la misma entidad `Caf` que devuelve
|
|
17
|
+
`CafFakerService.create`, con su folio/vigencia ya resueltos. Sin
|
|
18
|
+
esto, `DocumentBuilderService.build_signed()` no tiene de dónde
|
|
19
|
+
sacar un CAF real para timbrar en producción.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_OPERATION = 'billing.identifier.caf_loader::load'
|
|
23
|
+
|
|
24
|
+
def __init__(self, client: ApiClient) -> None:
|
|
25
|
+
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
26
|
+
self._client = client
|
|
27
|
+
|
|
28
|
+
def load(self, xml_base64: str) -> Caf:
|
|
29
|
+
"""Carga el CAF cuyo XML (en base64) es `xml_base64`."""
|
|
30
|
+
data = self._client.call(self._OPERATION, xml=xml_base64)
|
|
31
|
+
return Caf.from_api(data)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Servicio para `billing.identifier.caf_validator`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ...client import ApiClient
|
|
6
|
+
from .models import Caf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CafValidatorService:
|
|
10
|
+
"""
|
|
11
|
+
Valida un CAF (`billing.identifier.caf_validator::validate`).
|
|
12
|
+
|
|
13
|
+
Valida la firma y las llaves públicas/privadas asociadas al CAF —
|
|
14
|
+
útil antes de usarlo en `DocumentBuilderService.build_signed()`,
|
|
15
|
+
para poder avisarle al usuario que su CAF no es válido (ej. vencido)
|
|
16
|
+
con un error claro, en vez de que la falla aparezca recién al
|
|
17
|
+
intentar timbrar un documento.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_OPERATION = 'billing.identifier.caf_validator::validate'
|
|
21
|
+
|
|
22
|
+
def __init__(self, client: ApiClient) -> None:
|
|
23
|
+
"""Guarda el `ApiClient` compartido usado para llamar a la API."""
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
def validate(self, caf_xml_base64: str) -> Caf:
|
|
27
|
+
"""
|
|
28
|
+
Valida el CAF cuyo XML (en base64) es `caf_xml_base64`.
|
|
29
|
+
|
|
30
|
+
La API espera el mismo XML en base64 que `CafLoaderService.load`
|
|
31
|
+
(el parámetro de la operación se llama `caf`, no `xml`, pero es
|
|
32
|
+
el mismo dato). Devuelve el `Caf` ya validado si es válido;
|
|
33
|
+
levanta `LibreDteApiError` si no lo es.
|
|
34
|
+
"""
|
|
35
|
+
data = self._client.call(self._OPERATION, caf=caf_xml_base64)
|
|
36
|
+
return Caf.from_api(data)
|