libredte-lib-sdk 0.3.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.
Files changed (86) hide show
  1. libredte_lib_sdk-0.5.0/.claude/scheduled_tasks.lock +1 -0
  2. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/PKG-INFO +6 -13
  3. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/README.rst +5 -12
  4. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/__init__.py +1 -2
  5. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/builder.py +6 -4
  6. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/loader.py +2 -2
  7. libredte_lib_sdk-0.5.0/libredte_lib_sdk/billing/book/models.py +79 -0
  8. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/common.py +8 -0
  9. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/__init__.py +0 -2
  10. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/builder.py +18 -16
  11. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/dispatcher.py +48 -25
  12. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/examples.py +1 -1
  13. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/loader.py +8 -1
  14. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/models.py +122 -49
  15. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/renderer.py +1 -1
  16. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/validator.py +1 -1
  17. libredte_lib_sdk-0.5.0/libredte_lib_sdk/billing/identifier/models.py +75 -0
  18. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/models.py +3 -3
  19. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/models.py +3 -3
  20. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/models.py +7 -7
  21. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/sdk.py +1 -3
  22. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/pyproject.toml +1 -1
  23. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/conftest.py +22 -14
  24. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_book.py +8 -6
  25. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_caf.py +6 -2
  26. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_certificate_loader.py +5 -5
  27. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_loader.py +9 -8
  28. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_examples.py +8 -8
  29. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_ownership_transfer.py +1 -2
  30. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/test_models.py +126 -20
  31. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/test_services.py +228 -51
  32. libredte_lib_sdk-0.3.0/libredte_lib_sdk/billing/book/models.py +0 -70
  33. libredte_lib_sdk-0.3.0/libredte_lib_sdk/billing/identifier/models.py +0 -40
  34. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/.github/workflows/ci.yml +0 -0
  35. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/.github/workflows/docs.yml +0 -0
  36. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/.gitignore +0 -0
  37. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/.readthedocs.yaml +0 -0
  38. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/CLAUDE.md +0 -0
  39. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/LICENSE +0 -0
  40. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/Makefile +0 -0
  41. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/docs/Makefile +0 -0
  42. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/docs/conf.py +0 -0
  43. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/docs/index.rst +0 -0
  44. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/__init__.py +0 -0
  45. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/__init__.py +0 -0
  46. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/validator.py +0 -0
  47. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/enums.py +0 -0
  48. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/__init__.py +0 -0
  49. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/document_response.py +0 -0
  50. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/models.py +0 -0
  51. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/__init__.py +0 -0
  52. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_faker.py +0 -0
  53. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_loader.py +0 -0
  54. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_validator.py +0 -0
  55. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/__init__.py +0 -0
  56. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_dte.py +0 -0
  57. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_rcv.py +0 -0
  58. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_rtc.py +0 -0
  59. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/__init__.py +0 -0
  60. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/aec.py +0 -0
  61. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/__init__.py +0 -0
  62. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/mandatario_manager.py +0 -0
  63. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/client.py +0 -0
  64. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/exceptions.py +0 -0
  65. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/__init__.py +0 -0
  66. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/integration/__init__.py +0 -0
  67. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/integration/previred_provider.py +0 -0
  68. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/__init__.py +0 -0
  69. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/calculator.py +0 -0
  70. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/renderer.py +0 -0
  71. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/py.typed +0 -0
  72. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/__init__.py +0 -0
  73. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/certificate/__init__.py +0 -0
  74. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/certificate/loader.py +0 -0
  75. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/repository/__init__.py +0 -0
  76. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/repository/catalog.py +0 -0
  77. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/__init__.py +0 -0
  78. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/conftest.py +0 -0
  79. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/__init__.py +0 -0
  80. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_rendering.py +0 -0
  81. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_validation.py +0 -0
  82. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_exchange.py +0 -0
  83. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_human_resources.py +0 -0
  84. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_operation_not_found.py +0 -0
  85. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_repository.py +0 -0
  86. {libredte_lib_sdk-0.3.0 → libredte_lib_sdk-0.5.0}/tests/test_client.py +0 -0
@@ -0,0 +1 @@
1
+ {"sessionId":"64edfb1a-a3db-44cc-af25-6c7405269fdf","pid":73057,"procStart":"Mon Sep 7 03:13:55 2026","acquiredAt":1789224889985}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: libredte-lib-sdk
3
- Version: 0.3.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
- 'rut': '76192083-9',
138
- 'razon_social': 'SASCO SpA',
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
  )
@@ -220,7 +213,7 @@ necesita para poblar sus propios datos, no operaciones de facturación.
220
213
  ejemplos = b.document.examples.list() # [ExampleSummary(id=..., category=..., case=...), ...]
221
214
  ejemplo = b.document.examples.get(ejemplos[0].id)
222
215
  documento = b.document.builder.build_signed(
223
- ejemplo.input_data, caf_xml=caf.xml_base64, certificate=certificate,
216
+ ejemplo.example, caf_xml=caf.xml_base64, certificate=certificate,
224
217
  )
225
218
 
226
219
  # Catálogos/repositorios reales (comunas, tipos de documento, etc.)
@@ -238,7 +231,7 @@ necesita para poblar sus propios datos, no operaciones de facturación.
238
231
  # ya extraídos en una sola llamada — mismo `Certificate` que usan
239
232
  # build_signed()/dispatcher.create(), ya con todos los datos.
240
233
  certificate = s.certificate.loader.load(datos_del_archivo_pfx, 'contraseña')
241
- print(certificate.rut, certificate.nombre, certificate.email)
234
+ print(certificate.id, certificate.name, certificate.email)
242
235
 
243
236
  Errores
244
237
  -------
@@ -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
- 'rut': '76192083-9',
94
- 'razon_social': 'SASCO SpA',
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
  )
@@ -176,7 +169,7 @@ necesita para poblar sus propios datos, no operaciones de facturación.
176
169
  ejemplos = b.document.examples.list() # [ExampleSummary(id=..., category=..., case=...), ...]
177
170
  ejemplo = b.document.examples.get(ejemplos[0].id)
178
171
  documento = b.document.builder.build_signed(
179
- ejemplo.input_data, caf_xml=caf.xml_base64, certificate=certificate,
172
+ ejemplo.example, caf_xml=caf.xml_base64, certificate=certificate,
180
173
  )
181
174
 
182
175
  # Catálogos/repositorios reales (comunas, tipos de documento, etc.)
@@ -194,7 +187,7 @@ necesita para poblar sus propios datos, no operaciones de facturación.
194
187
  # ya extraídos en una sola llamada — mismo `Certificate` que usan
195
188
  # build_signed()/dispatcher.create(), ya con todos los datos.
196
189
  certificate = s.certificate.loader.load(datos_del_archivo_pfx, 'contraseña')
197
- print(certificate.rut, certificate.nombre, certificate.email)
190
+ print(certificate.id, certificate.name, certificate.email)
198
191
 
199
192
  Errores
200
193
  -------
@@ -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 Book, BookBag
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 Book
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
- ) -> Book:
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 Book.from_api(data)
44
+ return BookBag.from_api(data)
@@ -31,8 +31,8 @@ class BookLoaderService:
31
31
  Normaliza `caratula`/`detalle` de `bag` en un `BookBag`.
32
32
 
33
33
  `bag` trae `tipo`/`caratula`/`detalle`/`emisor`, tal cual el
34
- formato SII. `BookBag.datos` siempre viene `None` (no construye
35
- el libro); `BookBag.auth` viene poblado solo si
34
+ formato SII. `BookBag.book` siempre viene `None` (no construye
35
+ el libro); `BookBag.book_auth` viene poblado solo si
36
36
  `bag['emisor']` incluye `autorizacion_dte`.
37
37
  """
38
38
  data = self._client.call(
@@ -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__ = ()
@@ -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',
@@ -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 Document
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
- ) -> Document:
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`, incluyendo
42
- `Encabezado.IdDoc.Folio` (el SDK no asigna folios: eso lo decide
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`). Sin
48
- CAF ni certificado, el resultado no queda timbrado
49
- (`Document.is_timbrado` es `False`).
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 Document.from_api(data)
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
- ) -> Document:
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
- Requiere un CAF real (XML tal como lo entrega el SII, cubriendo
71
- el folio indicado en `input_data`) y el certificado digital del
72
- emisor. Para pruebas, ambos se pueden generar con
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 Document.from_api(data)
85
+ return DocumentBag.from_api(data)
@@ -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 ..trading_parties.models import Certificate
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
- `document_xml_base64` es el XML en base64 del documento a
41
- envolver — típicamente `Document.xml_base64` de un documento ya
42
- timbrado y firmado (`DocumentBuilderService.build_signed`). El
43
- sobre resultante es lo que se envía al SII (`SiiDteService.send`).
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
- Caso de uso típico: reprocesar un sobre ya guardado, o normalizar
64
- un DTE suelto recibido de un tercero (la API arma un sobre nuevo
65
- a partir de él).
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)
@@ -17,7 +17,7 @@ class DocumentExamplesService:
17
17
  de `libredte-lib-core` (`tests/fixtures/yaml/documentos_ok/`) — uno
18
18
  por variante de negocio real (descuentos, impuesto adicional, pago a
19
19
  crédito, exportación, etc.), no datos inventados por el SDK.
20
- `Example.input_data` de `get()` se le pasa tal cual a
20
+ `Example.example` de `get()` se le pasa tal cual a
21
21
  `DocumentBuilderService.build_draft()`/`.build_signed()`, típicamente
22
22
  reemplazando `Encabezado.IdDoc.Folio` y `Encabezado.Receptor` por los
23
23
  propios de quien usa el SDK antes de construir el documento.
@@ -26,6 +26,13 @@ class DocumentLoaderService:
26
26
  self._client = client
27
27
 
28
28
  def load_xml(self, xml_base64: str) -> DocumentBag:
29
- """Carga y normaliza el documento cuyo XML (base64) es `xml_base64`."""
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)