libredte-lib-sdk 0.6.1__tar.gz → 0.7.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 (98) hide show
  1. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/.gitignore +2 -0
  2. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/CLAUDE.md +0 -8
  3. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/PKG-INFO +57 -9
  4. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/README.rst +56 -8
  5. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/book/validator.py +7 -3
  6. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/common.py +10 -0
  7. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/dispatcher.py +46 -12
  8. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/loader.py +2 -2
  9. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/models.py +12 -13
  10. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/validator.py +9 -4
  11. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/exchange/document_response.py +7 -5
  12. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/exchange/models.py +2 -2
  13. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/identifier/models.py +12 -12
  14. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/integration/__init__.py +25 -0
  15. libredte_lib_sdk-0.7.0/libredte_lib_sdk/billing/integration/models.py +631 -0
  16. libredte_lib_sdk-0.7.0/libredte_lib_sdk/billing/integration/request.py +51 -0
  17. libredte_lib_sdk-0.7.0/libredte_lib_sdk/billing/integration/sii_boleta.py +131 -0
  18. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/integration/sii_dte.py +20 -25
  19. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/integration/sii_rcv.py +18 -20
  20. libredte_lib_sdk-0.7.0/libredte_lib_sdk/billing/integration/sii_rtc.py +150 -0
  21. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/ownership_transfer/aec.py +5 -3
  22. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/trading_parties/mandatario_manager.py +2 -2
  23. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/trading_parties/models.py +4 -4
  24. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/client.py +29 -10
  25. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/exceptions.py +93 -11
  26. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/human_resources/integration/previred_provider.py +11 -1
  27. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/sdk.py +3 -1
  28. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/system/repository/catalog.py +3 -3
  29. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/pyproject.toml +1 -1
  30. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/conftest.py +54 -1
  31. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_batch_processor.py +4 -2
  32. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_book.py +5 -2
  33. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_caf.py +1 -1
  34. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_document_loader.py +1 -1
  35. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_document_rendering.py +7 -3
  36. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_document_validation.py +24 -4
  37. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_exchange.py +2 -0
  38. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_human_resources.py +1 -1
  39. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_operation_not_found.py +2 -2
  40. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_ownership_transfer.py +1 -0
  41. libredte_lib_sdk-0.7.0/tests/integration/test_sii_certification.py +456 -0
  42. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/test_client.py +114 -0
  43. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/test_exceptions.py +4 -4
  44. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/test_models.py +30 -30
  45. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/test_response_registry.py +25 -1
  46. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/test_services.py +107 -107
  47. libredte_lib_sdk-0.7.0/tests/test_sii_boleta_and_rtc.py +551 -0
  48. libredte_lib_sdk-0.7.0/tests/test_sii_request_options.py +135 -0
  49. libredte_lib_sdk-0.6.1/libredte_lib_sdk/billing/integration/models.py +0 -285
  50. libredte_lib_sdk-0.6.1/libredte_lib_sdk/billing/integration/sii_rtc.py +0 -57
  51. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/.github/workflows/ci.yml +0 -0
  52. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/.github/workflows/docs.yml +0 -0
  53. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/.readthedocs.yaml +0 -0
  54. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/LICENSE +0 -0
  55. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/Makefile +0 -0
  56. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/docs/Makefile +0 -0
  57. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/docs/conf.py +0 -0
  58. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/docs/index.rst +0 -0
  59. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/__init__.py +0 -0
  60. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/__init__.py +0 -0
  61. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/book/__init__.py +0 -0
  62. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/book/builder.py +0 -0
  63. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/book/loader.py +0 -0
  64. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/book/models.py +0 -0
  65. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/__init__.py +0 -0
  66. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/batch_processor.py +0 -0
  67. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/builder.py +0 -0
  68. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/examples.py +0 -0
  69. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/parser.py +0 -0
  70. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/document/renderer.py +0 -0
  71. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/enums.py +0 -0
  72. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/exchange/__init__.py +0 -0
  73. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/identifier/__init__.py +0 -0
  74. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/identifier/caf_faker.py +0 -0
  75. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/identifier/caf_loader.py +0 -0
  76. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/identifier/caf_validator.py +0 -0
  77. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/ownership_transfer/__init__.py +0 -0
  78. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/ownership_transfer/models.py +0 -0
  79. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/billing/trading_parties/__init__.py +0 -0
  80. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/human_resources/__init__.py +0 -0
  81. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/human_resources/integration/__init__.py +0 -0
  82. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/human_resources/payroll/__init__.py +0 -0
  83. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/human_resources/payroll/calculator.py +0 -0
  84. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/human_resources/payroll/renderer.py +0 -0
  85. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/py.typed +0 -0
  86. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/response_registry.py +0 -0
  87. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/system/__init__.py +0 -0
  88. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/system/certificate/__init__.py +0 -0
  89. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/system/certificate/loader.py +0 -0
  90. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/libredte_lib_sdk/system/repository/__init__.py +0 -0
  91. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/__init__.py +0 -0
  92. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/conftest.py +0 -0
  93. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/fixtures/certificate_112223339.p12 +0 -0
  94. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/__init__.py +0 -0
  95. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_certificate_loader.py +0 -0
  96. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_document_parser.py +0 -0
  97. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_examples.py +0 -0
  98. {libredte_lib_sdk-0.6.1 → libredte_lib_sdk-0.7.0}/tests/integration/test_repository.py +0 -0
@@ -17,5 +17,7 @@ docs/_build/
17
17
  docs/libredte_lib_sdk*.rst
18
18
  docs/modules.rst
19
19
 
20
+ .env
21
+
20
22
  # Archivos específicos del sistema operativo.
21
23
  .DS_Store
@@ -2,14 +2,6 @@
2
2
 
3
3
  ## Pendiente
4
4
 
5
- - **Tests en vivo de `integration.sii_dte`/`sii_rcv`/`sii_rtc`** (10
6
- operaciones): tienen test unitario (mockeado), pero ninguno en
7
- `live`. A diferencia del resto de la suite `live` (que usa
8
- `fake_certificate`, autofirmado), estas operaciones hablan con el SII
9
- real y necesitan un certificado real vigente — no hay fixture para
10
- eso hoy, y committear uno no correspondería. Falta decidir el
11
- mecanismo (ej. certificado real vía variable de entorno, `skip` si
12
- no está disponible) antes de poder escribirlos.
13
5
  - **DTO `Liquidacion` sin construir.** `human_resources.payroll
14
6
  .calculator::calculate()` recibe `empleado`/`contrato`/`contexto`
15
7
  como `dict` (correcto — son entradas) pero **devuelve** la
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: libredte-lib-sdk
3
- Version: 0.6.1
3
+ Version: 0.7.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
@@ -74,7 +74,11 @@ Uso
74
74
 
75
75
  .. code-block:: python
76
76
 
77
+ import dataclasses
78
+
77
79
  from libredte_lib_sdk import LibreDTE
80
+ from libredte_lib_sdk.billing.common import SII_RUT
81
+ from libredte_lib_sdk.billing.integration import SiiRequestOptions
78
82
  from libredte_lib_sdk.billing.trading_parties import Mandatario
79
83
 
80
84
  sdk = LibreDTE() # base_url y api_token también vía env vars, ver abajo
@@ -90,6 +94,7 @@ Uso
90
94
  'RUTEmisor': '76192083-9',
91
95
  'RznSoc': 'SASCO SpA',
92
96
  'GiroEmis': 'Servicios',
97
+ 'Acteco': 620200,
93
98
  'DirOrigen': 'Santiago',
94
99
  'CmnaOrigen': 'Santiago',
95
100
  },
@@ -116,7 +121,7 @@ Uso
116
121
  codigo_documento=33, folio_desde=1, folio_hasta=100,
117
122
  )
118
123
  certificate = b.trading_parties.mandatario_manager.create_fake_certificate(
119
- Mandatario(rut='76192083-9', nombre='SASCO SpA', email='sasco@example.com'),
124
+ Mandatario(run='76192083-9', nombre='SASCO SpA', email='sasco@example.com'),
120
125
  )
121
126
 
122
127
  # En producción: cargar y validar el CAF real que el SII le entregó
@@ -132,17 +137,46 @@ Uso
132
137
  input_data, caf_xml=caf.xml_base64, certificate=certificate,
133
138
  )
134
139
 
135
- # 3. Envío al SII (arma el sobre EnvioDTE y luego lo envía).
136
- # `documento` ya trae certificate/emisor incluidos — no hace falta
137
- # pasarlos aparte.
138
- sobre = b.document.dispatcher.create(documento)
140
+ # 3. Sobres. El envío normal usa dos: uno para el SII y otro para el
141
+ # receptor del documento (el cliente). `documento` ya trae
142
+ # certificate/emisor incluidos — no hace falta pasarlos aparte, salvo
143
+ # `autorizacion_dte` del emisor (`fecha_resolucion` y
144
+ # `numero_resolucion`), que va en la carátula.
145
+ documento = dataclasses.replace(
146
+ documento,
147
+ emisor={
148
+ **documento.emisor,
149
+ 'autorizacion_dte': {
150
+ 'fecha_resolucion': '2019-12-23',
151
+ 'numero_resolucion': 0,
152
+ },
153
+ },
154
+ )
155
+
156
+ # El sobre para el SII siempre tiene al SII como receptor.
157
+ sobre_sii = b.document.dispatcher.create(documento, receptor_rut=SII_RUT)
158
+
159
+ # El sobre para el cliente toma el receptor del documento.
160
+ sobre_cliente = b.document.dispatcher.create(documento)
161
+
162
+ # 4. Envío al SII y estado del envío, por Track ID.
139
163
  envio = b.integration.sii_dte.send(
140
- sobre.xml_base64, certificate=certificate, company_rut='76192083-9',
164
+ sobre_sii.xml_base64, certificate=certificate,
165
+ company_rut='76192083-9',
166
+ )
167
+ estado = b.integration.sii_dte.check_status(
168
+ envio.track_id, certificate=certificate, company_rut='76192083-9',
141
169
  )
142
170
 
143
- # 4. Estado del envío, por Track ID.
171
+ # Las boletas (tipos 39 y 41) se envían por otra API del SII, solo
172
+ # disponible en Lib Pro: `b.integration.sii_boleta` tiene `send()`,
173
+ # `check_status()` y `validate_document()` con la misma forma.
174
+
175
+ # Reintentos y tiempos de una llamada al SII (todos opcionales; la API
176
+ # usa los suyos si no se indican y los ajusta a su rango).
144
177
  estado = b.integration.sii_dte.check_status(
145
178
  envio.track_id, certificate=certificate, company_rut='76192083-9',
179
+ options=SiiRequestOptions(retries=3, timeout=20, max_duration=60),
146
180
  )
147
181
 
148
182
  # 5. HTML o PDF — misma forma de respuesta para ambos.
@@ -254,7 +288,10 @@ levanta ``libredte_lib_sdk.LibreDteApiError``, con ``.status_code``,
254
288
  si la API la informó — ej. ``DocumentException``, o una excepción de
255
289
  autenticación del SII). Un ``429`` de la API levanta
256
290
  ``LibreDteRateLimitError`` (subclase de lo anterior), con
257
- ``.retry_after``/``.limit``/``.remaining``. Un problema de red levanta
291
+ ``.retry_after`` y ``.reset_at`` (cuándo se libera), ``.policy`` (cuál límite
292
+ se agotó), el cupo del usuario (``.limit``/``.remaining``, solo con
293
+ autenticación) y el límite por IP (``.ip_limit``/``.ip_remaining``). Cada
294
+ dato es ``None`` si la API no lo informa. Un problema de red levanta
258
295
  ``libredte_lib_sdk.LibreDteConnectionError``.
259
296
 
260
297
  Desarrollo
@@ -270,6 +307,17 @@ Los tests unitarios no hacen red real: mockean la API con ``respx``. Los
270
307
  de ``tests/integration/`` sí (marker ``live``), y están excluidos por
271
308
  defecto de ``pytest``/``make test``/``make check``.
272
309
 
310
+ Los tests ``live`` usan ``LIBREDTE_LIB_SDK_BASE_URL`` y
311
+ ``LIBREDTE_LIB_SDK_TOKEN``. Los de ``tests/integration/test_sii_certification.py``
312
+ envían documentos reales al ambiente de certificación del SII y se omiten si
313
+ faltan estas variables de entorno:
314
+
315
+ - ``LIBREDTE_CERTIFICATE_CERT`` y ``LIBREDTE_CERTIFICATE_PKEY``: certificado
316
+ digital y su clave privada, en PEM (un ``\n`` literal vale como salto de
317
+ línea).
318
+ - ``LIBREDTE_CAF_DIR``: directorio con los CAF de certificación, uno por tipo
319
+ de documento (``033.xml``, ``034.xml``, ``039.xml``, ...).
320
+
273
321
  Términos y condiciones de uso
274
322
  ------------------------------
275
323
 
@@ -30,7 +30,11 @@ Uso
30
30
 
31
31
  .. code-block:: python
32
32
 
33
+ import dataclasses
34
+
33
35
  from libredte_lib_sdk import LibreDTE
36
+ from libredte_lib_sdk.billing.common import SII_RUT
37
+ from libredte_lib_sdk.billing.integration import SiiRequestOptions
34
38
  from libredte_lib_sdk.billing.trading_parties import Mandatario
35
39
 
36
40
  sdk = LibreDTE() # base_url y api_token también vía env vars, ver abajo
@@ -46,6 +50,7 @@ Uso
46
50
  'RUTEmisor': '76192083-9',
47
51
  'RznSoc': 'SASCO SpA',
48
52
  'GiroEmis': 'Servicios',
53
+ 'Acteco': 620200,
49
54
  'DirOrigen': 'Santiago',
50
55
  'CmnaOrigen': 'Santiago',
51
56
  },
@@ -72,7 +77,7 @@ Uso
72
77
  codigo_documento=33, folio_desde=1, folio_hasta=100,
73
78
  )
74
79
  certificate = b.trading_parties.mandatario_manager.create_fake_certificate(
75
- Mandatario(rut='76192083-9', nombre='SASCO SpA', email='sasco@example.com'),
80
+ Mandatario(run='76192083-9', nombre='SASCO SpA', email='sasco@example.com'),
76
81
  )
77
82
 
78
83
  # En producción: cargar y validar el CAF real que el SII le entregó
@@ -88,17 +93,46 @@ Uso
88
93
  input_data, caf_xml=caf.xml_base64, certificate=certificate,
89
94
  )
90
95
 
91
- # 3. Envío al SII (arma el sobre EnvioDTE y luego lo envía).
92
- # `documento` ya trae certificate/emisor incluidos — no hace falta
93
- # pasarlos aparte.
94
- sobre = b.document.dispatcher.create(documento)
96
+ # 3. Sobres. El envío normal usa dos: uno para el SII y otro para el
97
+ # receptor del documento (el cliente). `documento` ya trae
98
+ # certificate/emisor incluidos — no hace falta pasarlos aparte, salvo
99
+ # `autorizacion_dte` del emisor (`fecha_resolucion` y
100
+ # `numero_resolucion`), que va en la carátula.
101
+ documento = dataclasses.replace(
102
+ documento,
103
+ emisor={
104
+ **documento.emisor,
105
+ 'autorizacion_dte': {
106
+ 'fecha_resolucion': '2019-12-23',
107
+ 'numero_resolucion': 0,
108
+ },
109
+ },
110
+ )
111
+
112
+ # El sobre para el SII siempre tiene al SII como receptor.
113
+ sobre_sii = b.document.dispatcher.create(documento, receptor_rut=SII_RUT)
114
+
115
+ # El sobre para el cliente toma el receptor del documento.
116
+ sobre_cliente = b.document.dispatcher.create(documento)
117
+
118
+ # 4. Envío al SII y estado del envío, por Track ID.
95
119
  envio = b.integration.sii_dte.send(
96
- sobre.xml_base64, certificate=certificate, company_rut='76192083-9',
120
+ sobre_sii.xml_base64, certificate=certificate,
121
+ company_rut='76192083-9',
122
+ )
123
+ estado = b.integration.sii_dte.check_status(
124
+ envio.track_id, certificate=certificate, company_rut='76192083-9',
97
125
  )
98
126
 
99
- # 4. Estado del envío, por Track ID.
127
+ # Las boletas (tipos 39 y 41) se envían por otra API del SII, solo
128
+ # disponible en Lib Pro: `b.integration.sii_boleta` tiene `send()`,
129
+ # `check_status()` y `validate_document()` con la misma forma.
130
+
131
+ # Reintentos y tiempos de una llamada al SII (todos opcionales; la API
132
+ # usa los suyos si no se indican y los ajusta a su rango).
100
133
  estado = b.integration.sii_dte.check_status(
101
134
  envio.track_id, certificate=certificate, company_rut='76192083-9',
135
+ options=SiiRequestOptions(retries=3, timeout=20, max_duration=60),
102
136
  )
103
137
 
104
138
  # 5. HTML o PDF — misma forma de respuesta para ambos.
@@ -210,7 +244,10 @@ levanta ``libredte_lib_sdk.LibreDteApiError``, con ``.status_code``,
210
244
  si la API la informó — ej. ``DocumentException``, o una excepción de
211
245
  autenticación del SII). Un ``429`` de la API levanta
212
246
  ``LibreDteRateLimitError`` (subclase de lo anterior), con
213
- ``.retry_after``/``.limit``/``.remaining``. Un problema de red levanta
247
+ ``.retry_after`` y ``.reset_at`` (cuándo se libera), ``.policy`` (cuál límite
248
+ se agotó), el cupo del usuario (``.limit``/``.remaining``, solo con
249
+ autenticación) y el límite por IP (``.ip_limit``/``.ip_remaining``). Cada
250
+ dato es ``None`` si la API no lo informa. Un problema de red levanta
214
251
  ``libredte_lib_sdk.LibreDteConnectionError``.
215
252
 
216
253
  Desarrollo
@@ -226,6 +263,17 @@ Los tests unitarios no hacen red real: mockean la API con ``respx``. Los
226
263
  de ``tests/integration/`` sí (marker ``live``), y están excluidos por
227
264
  defecto de ``pytest``/``make test``/``make check``.
228
265
 
266
+ Los tests ``live`` usan ``LIBREDTE_LIB_SDK_BASE_URL`` y
267
+ ``LIBREDTE_LIB_SDK_TOKEN``. Los de ``tests/integration/test_sii_certification.py``
268
+ envían documentos reales al ambiente de certificación del SII y se omiten si
269
+ faltan estas variables de entorno:
270
+
271
+ - ``LIBREDTE_CERTIFICATE_CERT`` y ``LIBREDTE_CERTIFICATE_PKEY``: certificado
272
+ digital y su clave privada, en PEM (un ``\n`` literal vale como salto de
273
+ línea).
274
+ - ``LIBREDTE_CAF_DIR``: directorio con los CAF de certificación, uno por tipo
275
+ de documento (``033.xml``, ``034.xml``, ``039.xml``, ...).
276
+
229
277
  Términos y condiciones de uso
230
278
  ------------------------------
231
279
 
@@ -18,8 +18,10 @@ class BookValidatorService:
18
18
  vacío en caso de éxito.
19
19
  """
20
20
 
21
- _VALIDATE_SCHEMA_OPERATION = 'billing.book.validator::validateSchema'
22
- _VALIDATE_SIGNATURE_OPERATION = 'billing.book.validator::validateSignature'
21
+ _VALIDATE_SCHEMA_OPERATION = 'billing.book.validator::validate_schema'
22
+ _VALIDATE_SIGNATURE_OPERATION = (
23
+ 'billing.book.validator::validate_signature'
24
+ )
23
25
 
24
26
  def __init__(self, client: ApiClient) -> None:
25
27
  """Guarda el `ApiClient` compartido usado para llamar a la API."""
@@ -40,7 +42,9 @@ class BookValidatorService:
40
42
  Valida la firma electrónica del libro.
41
43
 
42
44
  El libro tiene una sola firma — devuelve un único `dict`, no
43
- una lista.
45
+ una lista, con `{'valid': bool, 'error': str | None,
46
+ 'signature': {...}}`. Una firma inválida no levanta error, hay
47
+ que revisar `valid`.
44
48
  """
45
49
  return cast(
46
50
  'dict[str, Any]',
@@ -8,6 +8,16 @@ from __future__ import annotations
8
8
  import base64
9
9
  from typing import Any
10
10
 
11
+ SII_RUT = '60803000-K'
12
+ """
13
+ RUT del SII, receptor de la carátula de todo sobre que se envía al SII.
14
+
15
+ Un sobre para el SII (`SiiDteService.send`, `SiiBoletaService.send`) se
16
+ arma con `receptor_rut=SII_RUT`. Un sobre para el receptor real del
17
+ documento (el cliente) se arma sin `receptor_rut`: la carátula toma el
18
+ receptor del primer documento.
19
+ """
20
+
11
21
 
12
22
  def encode_content(content: str | bytes) -> str:
13
23
  """
@@ -17,34 +17,53 @@ class DocumentDispatcherService:
17
17
  """Arma y valida el sobre `EnvioDTE` (`billing.document.dispatcher`)."""
18
18
 
19
19
  _CREATE_OPERATION = 'billing.document.dispatcher::create'
20
- _CREATE_MANY_OPERATION = 'billing.document.dispatcher::createMany'
21
- _LOAD_XML_OPERATION = 'billing.document.dispatcher::loadXml'
20
+ _CREATE_MANY_OPERATION = 'billing.document.dispatcher::create_many'
21
+ _LOAD_XML_OPERATION = 'billing.document.dispatcher::load_xml'
22
22
  _VALIDATE_OPERATION = 'billing.document.dispatcher::validate'
23
- _VALIDATE_SCHEMA_OPERATION = 'billing.document.dispatcher::validateSchema'
23
+ _VALIDATE_SCHEMA_OPERATION = 'billing.document.dispatcher::validate_schema'
24
24
  _VALIDATE_SIGNATURE_OPERATION = (
25
- 'billing.document.dispatcher::validateSignature'
25
+ 'billing.document.dispatcher::validate_signature'
26
26
  )
27
27
 
28
28
  def __init__(self, client: ApiClient) -> None:
29
29
  """Guarda el `ApiClient` compartido usado para llamar a la API."""
30
30
  self._client = client
31
31
 
32
- def create(self, bag: DocumentBag) -> DocumentEnvelope:
32
+ def create(
33
+ self,
34
+ bag: DocumentBag,
35
+ *,
36
+ receptor_rut: str | None = None,
37
+ ) -> DocumentEnvelope:
33
38
  """
34
- Envuelve y firma un documento ya timbrado en un sobre `EnvioDTE`.
39
+ Envuelve y firma un documento ya timbrado en un sobre.
35
40
 
36
41
  `bag` es el `DocumentBag` ya timbrado y firmado que devuelve
37
42
  `DocumentBuilderService.build_signed()` — trae `certificate`/
38
43
  `emisor` incluidos, no hace falta pasarlos aparte. El sobre
39
44
  resultante es lo que se envía al SII (`SiiDteService.send`).
45
+
46
+ `receptor_rut` es el RUT del receptor de la carátula del sobre.
47
+ Sin indicarlo es el receptor del documento, que es el sobre para
48
+ enviar al cliente. El sobre que va al SII se arma con
49
+ `receptor_rut=SII_RUT` (`60803000-K`): el SII lo exige y rechaza
50
+ el envío con otro receptor. El envío normal usa dos sobres, uno
51
+ para el SII y otro para el cliente. Para enviar el sobre al SII
52
+ el emisor debe traer `autorizacion_dte`, con `fecha_resolucion` y
53
+ `numero_resolucion` de su autorización.
40
54
  """
41
55
  response = self._client.call_response(
42
56
  self._CREATE_OPERATION,
43
- bag=self._bag_payload(bag),
57
+ bag=self._bag_payload(bag, receptor_rut),
44
58
  )
45
59
  return build_response(DocumentEnvelope, response)
46
60
 
47
- def create_many(self, bags: Sequence[DocumentBag]) -> DocumentEnvelope:
61
+ def create_many(
62
+ self,
63
+ bags: Sequence[DocumentBag],
64
+ *,
65
+ receptor_rut: str | None = None,
66
+ ) -> DocumentEnvelope:
48
67
  """
49
68
  Envuelve y firma 2+ documentos ya timbrados en un sobre `EnvioDTE`.
50
69
 
@@ -53,15 +72,24 @@ class DocumentDispatcherService:
53
72
  `emisor`. El sobre resultante queda firmado con el certificado
54
73
  del PRIMER `DocumentBag` (el sobre en sí también se firma, no
55
74
  solo cada documento).
75
+
76
+ `receptor_rut` funciona igual que en `create()`: es el receptor
77
+ de la carátula del sobre, y para el SII debe ser `SII_RUT`.
56
78
  """
57
79
  response = self._client.call_response(
58
80
  self._CREATE_MANY_OPERATION,
59
- bags=[self._bag_payload(bag) for bag in bags],
81
+ bags=[
82
+ self._bag_payload(bag, receptor_rut if index == 0 else None)
83
+ for index, bag in enumerate(bags)
84
+ ],
60
85
  )
61
86
  return build_response(DocumentEnvelope, response)
62
87
 
63
88
  @staticmethod
64
- def _bag_payload(bag: DocumentBag) -> dict[str, Any]:
89
+ def _bag_payload(
90
+ bag: DocumentBag,
91
+ receptor_rut: str | None = None,
92
+ ) -> dict[str, Any]:
65
93
  if bag.certificate is None:
66
94
  raise ValueError(
67
95
  'Este DocumentBag no tiene certificado — ¿se firmó con '
@@ -73,11 +101,15 @@ class DocumentDispatcherService:
73
101
  '`builder.build_draft/build_signed()` o se cargó con '
74
102
  '`loader.load_xml()`?',
75
103
  )
76
- return {
104
+ payload: dict[str, Any] = {
77
105
  'xmlDocument': bag.xml_base64,
78
106
  'certificate': bag.certificate.to_payload(),
79
107
  'emisor': bag.emisor,
80
108
  }
109
+ if receptor_rut is not None:
110
+ payload['receptor'] = {'rut': receptor_rut}
111
+
112
+ return payload
81
113
 
82
114
  def load_xml(self, xml_base64: str) -> DocumentEnvelope:
83
115
  """
@@ -121,7 +153,9 @@ class DocumentDispatcherService:
121
153
 
122
154
  Un sobre trae más de una firma (la del DTE y la del propio
123
155
  `EnvioDTE`) — devuelve una lista, un ítem por firma. Cada ítem
124
- es un `dict` vacío en caso de éxito.
156
+ es `{'valid': bool, 'error': str | None, 'signature': {...}}`,
157
+ igual que en `DocumentValidatorService.validate_signature()`: una
158
+ firma inválida no levanta error, hay que revisar `valid`.
125
159
  """
126
160
  return cast(
127
161
  'list[dict[str, Any]]',
@@ -14,13 +14,13 @@ class DocumentLoaderService:
14
14
  """
15
15
  Carga un documento tributario completo desde su XML.
16
16
 
17
- Solo cubre `loadXml` — pensado para reconstruir los datos
17
+ Solo cubre `load_xml` — pensado para reconstruir los datos
18
18
  normalizados de un DTE ya emitido (timbrado y firmado) a partir de
19
19
  su XML, ej. un documento recibido de un tercero o recuperado de un
20
20
  respaldo.
21
21
  """
22
22
 
23
- _LOAD_XML_OPERATION = 'billing.document.loader::loadXml'
23
+ _LOAD_XML_OPERATION = 'billing.document.loader::load_xml'
24
24
 
25
25
  def __init__(self, client: ApiClient) -> None:
26
26
  """Guarda el `ApiClient` compartido usado para llamar a la API."""
@@ -31,15 +31,15 @@ class DocumentBag:
31
31
  """
32
32
  Bolsa de un documento tributario.
33
33
 
34
- Devuelta por `document.builder::build`/`document.loader::loadXml`/
34
+ Devuelta por `document.builder::build`/`document.loader::load_xml`/
35
35
  `document.parser::parse` — todas en esta misma forma, con los campos
36
36
  que correspondan a cuánto se hizo con el documento. `document`
37
37
  (`Encabezado`/`Detalle`, con el merge de `libredte_data.extra.dte`
38
38
  si se pasó al renderizar) es la vista "de datos" limpia del
39
39
  documento — SIN los campos opcionales no usados (se omiten, no
40
- quedan en `False`). `document_normalized` es la data cruda tal como
40
+ quedan en `None`). `document_normalized` es la data cruda tal como
41
41
  la usa el builder internamente para generar el XML (con esos
42
- campos opcionales en `False` en vez de omitidos): es la que hay que
42
+ campos opcionales en `None` en vez de omitidos): es la que hay que
43
43
  guardar para poder reconstruir el documento después con
44
44
  `options={'normalizer': {'normalize': False}}` sin volver a
45
45
  normalizar — usar `document` para eso corrompe la reconstrucción.
@@ -48,8 +48,8 @@ class DocumentBag:
48
48
  puede ser un borrador, un documento timbrado, o timbrado y firmado,
49
49
  no una subclase distinta: es el mismo recurso en distintos estados
50
50
  según qué datos se le hayan pasado a `DocumentBuilderService`. Un
51
- borrador sin folio asignado tiene folio `0` en `document_id` (ver
52
- `AbstractDocument::getId()` en la biblioteca), nunca lanza error.
51
+ borrador sin folio asignado tiene folio `0` en `document_id`
52
+ (criterio de la biblioteca), nunca lanza error.
53
53
 
54
54
  `document_stamp_base64` es el TED — `None` si el documento aún no
55
55
  está timbrado (ver `is_timbrado`). `document_extra` son los campos
@@ -64,7 +64,7 @@ class DocumentBag:
64
64
  ninguna al construir/cargar el documento.
65
65
 
66
66
  `certificate`/`caf` son `None` salvo que el documento se haya
67
- construido con ellos (`build_signed`, o un `loadXml` de un XML ya
67
+ construido con ellos (`build_signed`, o un `load_xml` de un XML ya
68
68
  firmado/timbrado) — un borrador (`build_draft`) no los trae.
69
69
  `emisor`/`receptor` sí están siempre que haya un documento
70
70
  construido (`Encabezado.Emisor`/`Encabezado.Receptor` ya
@@ -209,8 +209,8 @@ class DocumentEnvelope(XmlPayloadMixin):
209
209
 
210
210
  `documents` trae una bolsa (`DocumentBag`) por cada documento
211
211
  tributario que el sobre contiene — uno solo si se armó con
212
- `create()`, uno o más si se armó con `createMany()` o si se cargó
213
- con `loadXml()` un sobre que agrupaba varios documentos. `caratula`
212
+ `create()`, uno o más si se armó con `create_many()` o si se cargó
213
+ con `load_xml()` un sobre que agrupaba varios documentos. `caratula`
214
214
  es el `dict` con los datos de la carátula del sobre (`RutEmisor`,
215
215
  `RutEnvia`, `RutReceptor`, `FchResol`, `NroResol`, `TmstFirmaEnv`,
216
216
  `SubTotDTE`).
@@ -249,9 +249,8 @@ class RenderedDocument:
249
249
  liquidación de sueldo, que siempre es un único archivo).
250
250
  `copies`/`copy_number` identifican esta copia entre las pedidas de
251
251
  ese mismo `label` (ej. `copies=2, copy_number=1` es la primera de
252
- 2 copias tributarias). El nombre de los campos en la API
253
- (`content`/`mimeType`/`filename`/`copyNumber`) es camelCase; acá
254
- quedan en snake_case como el resto del SDK.
252
+ 2 copias tributarias). La API entrega los campos como
253
+ `content`/`mime_type`/`filename`/`copy_number`.
255
254
  """
256
255
 
257
256
  content_base64: str
@@ -271,11 +270,11 @@ class RenderedDocument:
271
270
  """Construye desde un ítem de `data.renderings`."""
272
271
  return cls(
273
272
  content_base64=data['content'],
274
- mime_type=data['mimeType'],
273
+ mime_type=data['mime_type'],
275
274
  filename=data['filename'],
276
275
  label=data.get('label'),
277
276
  copies=data['copies'],
278
- copy_number=data['copyNumber'],
277
+ copy_number=data['copy_number'],
279
278
  )
280
279
 
281
280
 
@@ -20,9 +20,9 @@ class DocumentValidatorService:
20
20
  """
21
21
 
22
22
  _VALIDATE_OPERATION = 'billing.document.validator::validate'
23
- _VALIDATE_SCHEMA_OPERATION = 'billing.document.validator::validateSchema'
23
+ _VALIDATE_SCHEMA_OPERATION = 'billing.document.validator::validate_schema'
24
24
  _VALIDATE_SIGNATURE_OPERATION = (
25
- 'billing.document.validator::validateSignature'
25
+ 'billing.document.validator::validate_signature'
26
26
  )
27
27
 
28
28
  def __init__(self, client: ApiClient) -> None:
@@ -58,8 +58,13 @@ class DocumentValidatorService:
58
58
  """
59
59
  Valida la firma electrónica del documento.
60
60
 
61
- Devuelve un `dict` vacío en caso de éxito. Si la firma no es
62
- válida, la API levanta `LibreDteApiError`.
61
+ Devuelve `{'valid': bool, 'error': str | None, 'signature': {...}}`.
62
+ La API responde con éxito tanto si la firma es válida como si no
63
+ lo es: el resultado está en `valid`, y `error` explica el motivo
64
+ cuando es `False` (por ejemplo, que el contenido se modificó
65
+ después de firmarlo). `signature` trae los datos de la firma
66
+ (`reference`, `signature_method`, `digest_method`, el certificado
67
+ y `signer`, con los datos del certificado del firmante).
63
68
  """
64
69
  return cast(
65
70
  'dict[str, Any]',
@@ -23,16 +23,16 @@ class DocumentResponseService:
23
23
  """
24
24
 
25
25
  _BUILD_ENVIO_RECIBOS_OPERATION = (
26
- 'billing.exchange.document_response::buildEnvioRecibos'
26
+ 'billing.exchange.document_response::build_envio_recibos'
27
27
  )
28
28
  _BUILD_RESPUESTA_ENVIO_OPERATION = (
29
- 'billing.exchange.document_response::buildRespuestaEnvio'
29
+ 'billing.exchange.document_response::build_respuesta_envio'
30
30
  )
31
31
  _VALIDATE_SCHEMA_OPERATION = (
32
- 'billing.exchange.document_response::validateSchema'
32
+ 'billing.exchange.document_response::validate_schema'
33
33
  )
34
34
  _VALIDATE_SIGNATURE_OPERATION = (
35
- 'billing.exchange.document_response::validateSignature'
35
+ 'billing.exchange.document_response::validate_signature'
36
36
  )
37
37
 
38
38
  def __init__(self, client: ApiClient) -> None:
@@ -111,7 +111,9 @@ class DocumentResponseService:
111
111
  `EnvioRecibos` trae múltiples firmas (una por recibo, más la
112
112
  del `SetRecibos`); `RespuestaDTE` trae solo la del nodo
113
113
  `Resultado`. En ambos casos se devuelve una lista, un ítem por
114
- firma.
114
+ firma, cada uno con `{'valid': bool, 'error': str | None,
115
+ 'signature': {...}}`. Una firma inválida no levanta error, hay
116
+ que revisar `valid`.
115
117
  """
116
118
  return cast(
117
119
  'list[dict[str, Any]]',
@@ -18,7 +18,7 @@ from ..common import XmlPayloadMixin
18
18
  @dataclass(frozen=True, slots=True)
19
19
  class EnvioRecibos(XmlPayloadMixin):
20
20
  """
21
- XML `EnvioRecibos` construido (`document_response::buildEnvioRecibos`).
21
+ XML `EnvioRecibos` construido (`document_response::build_envio_recibos`).
22
22
 
23
23
  Recibo de mercaderías o servicios prestados por un proveedor.
24
24
  `datos` es la estructura ya parseada, `xml_base64` el XML completo
@@ -43,7 +43,7 @@ class EnvioRecibos(XmlPayloadMixin):
43
43
  @dataclass(frozen=True, slots=True)
44
44
  class RespuestaEnvio(XmlPayloadMixin):
45
45
  """
46
- XML `RespuestaDTE` construido (`document_response::buildRespuestaEnvio`).
46
+ XML `RespuestaDTE` construido (`document_response::build_respuesta_envio`).
47
47
 
48
48
  Acuse de recibo del envío (`RecepcionEnvio`) o resultado de
49
49
  validación por documento (`ResultadoDTE`), según los datos
@@ -62,13 +62,13 @@ class Caf(XmlPayloadMixin):
62
62
  return cls(
63
63
  id=data['id'],
64
64
  emisor=data['emisor'],
65
- tipo_documento=data['tipoDocumento'],
66
- folio_desde=data['folioDesde'],
67
- folio_hasta=data['folioHasta'],
68
- cantidad_folios=data['cantidadFolios'],
69
- fecha_autorizacion=data['fechaAutorizacion'],
70
- fecha_vencimiento=data.get('fechaVencimiento'),
71
- meses_autorizacion=data['mesesAutorizacion'],
65
+ tipo_documento=data['tipo_documento'],
66
+ folio_desde=data['folio_desde'],
67
+ folio_hasta=data['folio_hasta'],
68
+ cantidad_folios=data['cantidad_folios'],
69
+ fecha_autorizacion=data['fecha_autorizacion'],
70
+ fecha_vencimiento=data.get('fecha_vencimiento'),
71
+ meses_autorizacion=data['meses_autorizacion'],
72
72
  vigente=data['vigente'],
73
73
  vence=data['vence'],
74
74
  idk=data['idk'],
@@ -76,12 +76,12 @@ class Caf(XmlPayloadMixin):
76
76
  if ambiente is not None
77
77
  else None,
78
78
  # Mismo valor que `ambiente`, ya resuelto a `int` — la API lo
79
- # entrega redundante así (`Caf::getCertificacion()` en
80
- # libredte-lib-core es literalmente `getEnvironment()?->value`),
81
- # se mantiene igual acá para no diverger del shape real.
79
+ # entrega redundante así (en libredte-lib-core es literalmente
80
+ # el valor de `ambiente`), se mantiene igual acá para no diverger
81
+ # del shape real.
82
82
  certificacion=data.get('certificacion'),
83
- public_key=data['publicKey'],
84
- private_key=data['privateKey'],
83
+ public_key=data['public_key'],
84
+ private_key=data['private_key'],
85
85
  xml_base64=data['xml'],
86
86
  raw=data,
87
87
  )