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.
Files changed (86) hide show
  1. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/PKG-INFO +4 -11
  2. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/README.rst +3 -10
  3. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/__init__.py +1 -2
  4. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/builder.py +6 -4
  5. libredte_lib_sdk-0.5.0/libredte_lib_sdk/billing/book/models.py +79 -0
  6. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/common.py +8 -0
  7. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/__init__.py +0 -2
  8. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/builder.py +18 -16
  9. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/dispatcher.py +48 -25
  10. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/loader.py +8 -1
  11. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/models.py +116 -43
  12. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/renderer.py +1 -1
  13. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/validator.py +1 -1
  14. libredte_lib_sdk-0.5.0/libredte_lib_sdk/billing/identifier/models.py +75 -0
  15. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/sdk.py +1 -3
  16. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/pyproject.toml +1 -1
  17. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/conftest.py +22 -14
  18. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_book.py +8 -6
  19. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_caf.py +6 -2
  20. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_loader.py +9 -8
  21. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_ownership_transfer.py +1 -2
  22. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/test_models.py +124 -18
  23. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/test_services.py +214 -37
  24. libredte_lib_sdk-0.4.0/libredte_lib_sdk/billing/book/models.py +0 -70
  25. libredte_lib_sdk-0.4.0/libredte_lib_sdk/billing/identifier/models.py +0 -40
  26. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.claude/scheduled_tasks.lock +0 -0
  27. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.github/workflows/ci.yml +0 -0
  28. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.github/workflows/docs.yml +0 -0
  29. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.gitignore +0 -0
  30. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/.readthedocs.yaml +0 -0
  31. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/CLAUDE.md +0 -0
  32. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/LICENSE +0 -0
  33. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/Makefile +0 -0
  34. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/docs/Makefile +0 -0
  35. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/docs/conf.py +0 -0
  36. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/docs/index.rst +0 -0
  37. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/__init__.py +0 -0
  38. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/__init__.py +0 -0
  39. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/loader.py +0 -0
  40. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/book/validator.py +0 -0
  41. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/document/examples.py +0 -0
  42. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/enums.py +0 -0
  43. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/__init__.py +0 -0
  44. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/document_response.py +0 -0
  45. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/exchange/models.py +0 -0
  46. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/__init__.py +0 -0
  47. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_faker.py +0 -0
  48. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_loader.py +0 -0
  49. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/identifier/caf_validator.py +0 -0
  50. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/__init__.py +0 -0
  51. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/models.py +0 -0
  52. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_dte.py +0 -0
  53. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_rcv.py +0 -0
  54. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/integration/sii_rtc.py +0 -0
  55. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/__init__.py +0 -0
  56. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/aec.py +0 -0
  57. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/ownership_transfer/models.py +0 -0
  58. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/__init__.py +0 -0
  59. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/mandatario_manager.py +0 -0
  60. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/billing/trading_parties/models.py +0 -0
  61. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/client.py +0 -0
  62. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/exceptions.py +0 -0
  63. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/__init__.py +0 -0
  64. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/integration/__init__.py +0 -0
  65. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/integration/previred_provider.py +0 -0
  66. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/__init__.py +0 -0
  67. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/calculator.py +0 -0
  68. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/human_resources/payroll/renderer.py +0 -0
  69. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/py.typed +0 -0
  70. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/__init__.py +0 -0
  71. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/certificate/__init__.py +0 -0
  72. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/certificate/loader.py +0 -0
  73. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/repository/__init__.py +0 -0
  74. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/libredte_lib_sdk/system/repository/catalog.py +0 -0
  75. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/__init__.py +0 -0
  76. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/conftest.py +0 -0
  77. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/__init__.py +0 -0
  78. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_certificate_loader.py +0 -0
  79. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_rendering.py +0 -0
  80. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_document_validation.py +0 -0
  81. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_examples.py +0 -0
  82. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_exchange.py +0 -0
  83. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_human_resources.py +0 -0
  84. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_operation_not_found.py +0 -0
  85. {libredte_lib_sdk-0.4.0 → libredte_lib_sdk-0.5.0}/tests/integration/test_repository.py +0 -0
  86. {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.4.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
  )
@@ -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
  )
@@ -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)
@@ -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)
@@ -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)
@@ -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 Document(XmlPayloadMixin):
24
+ class DocumentBag:
23
25
  """
24
- Documento tributario construido.
25
-
26
- Puede ser un borrador, un documento timbrado, o timbrado y firmado —
27
- lo distingue `is_timbrado` (`ted is not None`), no una subclase
28
- distinta: es el mismo recurso en distintos estados, según qué datos
29
- se le hayan pasado a `DocumentBuilderService`.
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
- id: str
33
- datos: dict[str, Any]
34
- ted: dict[str, Any] | None
35
- xml_base64: str
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.ted is not None
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
- @dataclass(frozen=True, slots=True)
54
- class DocumentBag:
55
- """
56
- Bolsa normalizada devuelta por `document.loader::loadXml`.
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
- document: dict[str, Any]
67
- document_type: dict[str, Any]
68
- document_stamp: str | None
69
- document_extra: dict[str, Any] | None
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
- document_stamp=data.get('document_stamp'),
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
- """Sobre `EnvioDTE` construido y firmado, listo para enviar al SII."""
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(tag=data['tag'], xml_base64=data['xml'])
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)
@@ -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
- `Document.xml_base64`).
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
@@ -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. `Document.xml_base64`
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
  """