factusmart 0.1.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.
- factusmart-0.1.0/LICENSE +21 -0
- factusmart-0.1.0/PKG-INFO +239 -0
- factusmart-0.1.0/README.md +210 -0
- factusmart-0.1.0/pyproject.toml +44 -0
- factusmart-0.1.0/setup.cfg +4 -0
- factusmart-0.1.0/src/factusmart/__init__.py +48 -0
- factusmart-0.1.0/src/factusmart/_version.py +1 -0
- factusmart-0.1.0/src/factusmart/calculo.py +121 -0
- factusmart-0.1.0/src/factusmart/cliente.py +341 -0
- factusmart-0.1.0/src/factusmart/errores.py +75 -0
- factusmart-0.1.0/src/factusmart/py.typed +0 -0
- factusmart-0.1.0/src/factusmart/recursos.py +171 -0
- factusmart-0.1.0/src/factusmart.egg-info/PKG-INFO +239 -0
- factusmart-0.1.0/src/factusmart.egg-info/SOURCES.txt +18 -0
- factusmart-0.1.0/src/factusmart.egg-info/dependency_links.txt +1 -0
- factusmart-0.1.0/src/factusmart.egg-info/top_level.txt +1 -0
- factusmart-0.1.0/tests/test_calculo.py +89 -0
- factusmart-0.1.0/tests/test_contrato.py +53 -0
- factusmart-0.1.0/tests/test_integracion.py +128 -0
- factusmart-0.1.0/tests/test_unitarios.py +335 -0
factusmart-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FactuSmart
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: factusmart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK oficial de Python del API de facturación electrónica de FactuSmart (SUNAT, Perú).
|
|
5
|
+
Author: FactuSmart
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentacion, https://factusmart.pe/api-facturacion-electronica/docs/sdks
|
|
8
|
+
Project-URL: Repositorio, https://github.com/factusmart/factusmart-python
|
|
9
|
+
Keywords: factusmart,sunat,facturacion electronica,peru,cpe,boleta,factura
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: Spanish
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# FactuSmart — SDK de Python
|
|
31
|
+
|
|
32
|
+
SDK oficial del **API de facturación electrónica de FactuSmart** (SUNAT, Perú):
|
|
33
|
+
facturas, boletas, notas de crédito/débito, guías de remisión (GRE 2.0) y
|
|
34
|
+
anulaciones.
|
|
35
|
+
|
|
36
|
+
- Python 3.8 o superior.
|
|
37
|
+
- **Cero dependencias**: solo librería estándar (`urllib`).
|
|
38
|
+
- Con type hints (`py.typed`).
|
|
39
|
+
- Documentación completa del API: https://factusmart.pe/api-facturacion-electronica/docs
|
|
40
|
+
|
|
41
|
+
## Instalar
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install factusmart
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Uso en 10 líneas
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import factusmart
|
|
51
|
+
|
|
52
|
+
fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
|
|
53
|
+
|
|
54
|
+
calc = factusmart.calcular_comprobante([
|
|
55
|
+
{"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
r = fs.comprobantes.emitir({
|
|
59
|
+
"codigo_tipo_documento": "01",
|
|
60
|
+
"datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
|
|
61
|
+
"items": calc["items"], "totales": calc["totales"],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
print(r["aceptado_por_sunat"], r["serie_numero"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`base_url` **no es igual para todas las cuentas**: cada cuenta vive en un
|
|
68
|
+
servidor distinto. Cópiala de tu panel de integrador (pestaña Configuración,
|
|
69
|
+
"URL base de la API").
|
|
70
|
+
|
|
71
|
+
## Opciones del cliente
|
|
72
|
+
|
|
73
|
+
| Opción | Obligatoria | Por defecto | Para qué |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `api_key` | sí | — | Tu API key (va en el header `X-API-Key`) |
|
|
76
|
+
| `base_url` | sí | — | URL base de tu cuenta, sin barra final |
|
|
77
|
+
| `ruc` | no | — | RUC emisor por defecto. Se cambia en cada llamada con `ruc=` |
|
|
78
|
+
| `timeout` | no | `60` | Segundos por petición (SUNAT a veces tarda) |
|
|
79
|
+
| `max_reintentos` | no | `3` | Ver "Reintentos e idempotencia" |
|
|
80
|
+
| `verificar_ssl` | no | `True` | `False` **solo** en pruebas locales con certificado propio |
|
|
81
|
+
|
|
82
|
+
Si falta `api_key` o `base_url`, el cliente lanza `ValueError` al crearse.
|
|
83
|
+
|
|
84
|
+
## Métodos
|
|
85
|
+
|
|
86
|
+
Los datos se envían y se reciben **tal cual los define el API** (claves en
|
|
87
|
+
español con guion bajo). Todos los métodos que operan sobre un RUC aceptan
|
|
88
|
+
`ruc=` para cambiar el del constructor (en GET va como `?ruc=`; en POST va en el
|
|
89
|
+
cuerpo, y si tú ya pusiste `ruc` en los datos, se respeta el tuyo).
|
|
90
|
+
|
|
91
|
+
| Método | Endpoint | Devuelve |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `fs.ping(ruc=None)` | `GET /ping` | dict |
|
|
94
|
+
| `fs.cuenta.saldo()` | `GET /account` | dict |
|
|
95
|
+
| `fs.series.listar(ruc=None)` | `GET /series` | dict |
|
|
96
|
+
| `fs.comprobantes.emitir(datos, ruc=None, idempotency_key=None)` | `POST /documents` | dict |
|
|
97
|
+
| `fs.comprobantes.listar(filtros=None, ruc=None)` | `GET /documents` | dict |
|
|
98
|
+
| `fs.comprobantes.consultar(external_id, ruc=None)` | `GET /documents/{id}` | dict |
|
|
99
|
+
| `fs.comprobantes.descargar(external_id, tipo, ruc=None)` | `GET /documents/{id}/{pdf\|xml\|cdr}` | **bytes** |
|
|
100
|
+
| `fs.comprobantes.reenviar(external_id, ruc=None)` | `POST /documents/{id}/reenviar` | dict |
|
|
101
|
+
| `fs.comprobantes.consultar_sunat(external_id, ruc=None)` | `POST /documents/{id}/consultar-sunat` | dict |
|
|
102
|
+
| `fs.comprobantes.historial(external_id, ruc=None)` | `GET /documents/{id}/historial` | dict |
|
|
103
|
+
| `fs.anulaciones.crear(datos, ruc=None)` | `POST /anulaciones` | dict |
|
|
104
|
+
| `fs.anulaciones.estado(ticket, ruc=None)` | `GET /anulaciones/{ticket}/status` | dict |
|
|
105
|
+
| `fs.guias.emitir(datos, ruc=None)` | `POST /guias` | dict |
|
|
106
|
+
| `fs.guias.estado(external_id, ruc=None)` | `GET /guias/{id}/status` | dict |
|
|
107
|
+
| `fs.rucs.listar()` | `GET /rucs` | dict |
|
|
108
|
+
| `fs.rucs.crear(datos)` | `POST /rucs` | dict |
|
|
109
|
+
| `fs.rucs.activar(ruc, activo)` | `PATCH /rucs/{ruc}/activation` | dict |
|
|
110
|
+
|
|
111
|
+
Filtros de `comprobantes.listar`: `estado`, `desde`, `hasta` (YYYY-MM-DD),
|
|
112
|
+
`limite` (1 a 100) y `antes_de` (pasa el `siguiente_cursor` de la página
|
|
113
|
+
anterior).
|
|
114
|
+
|
|
115
|
+
Guardar el PDF:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
with open("comprobante.pdf", "wb") as f:
|
|
119
|
+
f.write(fs.comprobantes.descargar(r["external_id"], "pdf"))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Lo que `comprobantes.emitir` completa por ti
|
|
123
|
+
|
|
124
|
+
Solo si no lo mandas:
|
|
125
|
+
|
|
126
|
+
- `numero_documento`: `"#"` (FactuSmart asigna el correlativo). Si mandas tu
|
|
127
|
+
propio número, se respeta.
|
|
128
|
+
- `fecha_de_emision` y `hora_de_emision`: fecha y hora **de Perú**
|
|
129
|
+
(America/Lima), no la de tu servidor.
|
|
130
|
+
- `codigo_tipo_moneda`: `"PEN"`.
|
|
131
|
+
- `codigo_tipo_operacion`: `"0101"` en facturas y boletas (en notas no aplica).
|
|
132
|
+
|
|
133
|
+
`guias.emitir` completa igual `numero_documento`, `fecha_de_emision` y
|
|
134
|
+
`hora_de_emision`.
|
|
135
|
+
|
|
136
|
+
## Calcular IGV y totales
|
|
137
|
+
|
|
138
|
+
El API espera los importes ya calculados. `calcular_comprobante` lo hace por ti:
|
|
139
|
+
recibe cada ítem con `descripcion`, `cantidad`, `valor_unitario` (**sin IGV**) y
|
|
140
|
+
`afectacion`, y devuelve `{"items": [...], "totales": {...}}` listo para enviar.
|
|
141
|
+
|
|
142
|
+
| `afectacion` | Significado | IGV |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `"10"` | Gravado | 18% |
|
|
145
|
+
| `"20"` | Exonerado | 0 |
|
|
146
|
+
| `"30"` | Inafecto | 0 |
|
|
147
|
+
| `"40"` | Exportación | 0 (va en `total_exportacion`) |
|
|
148
|
+
|
|
149
|
+
Opcionales por ítem: `codigo_interno` (recomendado: el SKU de tu sistema) y
|
|
150
|
+
`unidad_de_medida` (por defecto `NIU`; usa `ZZ` para servicios).
|
|
151
|
+
|
|
152
|
+
Redondea a 2 decimales por ítem con `decimal.Decimal` y ROUND_HALF_UP, así da
|
|
153
|
+
**exactamente los mismos números** que los SDK de PHP y Node.
|
|
154
|
+
|
|
155
|
+
## La regla de oro: revisa `aceptado_por_sunat`
|
|
156
|
+
|
|
157
|
+
Un HTTP 200 significa que FactuSmart **generó** tu comprobante, no que SUNAT lo
|
|
158
|
+
aceptó. SUNAT puede estar caída o rechazarlo: recibirás `estado: "01"` con
|
|
159
|
+
`aceptado_por_sunat: False` y el motivo en `sunat["descripcion"]`.
|
|
160
|
+
|
|
161
|
+
**Eso NO es una excepción.** El SDK te devuelve la respuesta tal cual y tú
|
|
162
|
+
decides:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
r = fs.comprobantes.emitir(datos)
|
|
166
|
+
if not r["aceptado_por_sunat"]:
|
|
167
|
+
print("SUNAT no lo aceptó todavía:", r.get("sunat"), r.get("accion_requerida"))
|
|
168
|
+
# corrige y reintenta gratis con: fs.comprobantes.reenviar(r["external_id"])
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Mira también `entorno` (`"prueba"` o `"produccion"`) y `tiene_validez_fiscal`.
|
|
172
|
+
|
|
173
|
+
## Errores
|
|
174
|
+
|
|
175
|
+
Todas las excepciones están en `factusmart.errores` (y también en `factusmart`)
|
|
176
|
+
y heredan de `FactuSmartError`, que trae:
|
|
177
|
+
|
|
178
|
+
- `status`: código HTTP (`None` si fue un error de red),
|
|
179
|
+
- `mensaje`: el mensaje del API, en español,
|
|
180
|
+
- `respuesta`: el cuerpo completo de la respuesta.
|
|
181
|
+
|
|
182
|
+
| HTTP | Excepción |
|
|
183
|
+
|---|---|
|
|
184
|
+
| 401 | `ErrorDeAutenticacion` |
|
|
185
|
+
| 402 | `SinCreditos` |
|
|
186
|
+
| 403 | `RucNoAutorizado` |
|
|
187
|
+
| 404 | `NoEncontrado` |
|
|
188
|
+
| 422 | `ErrorDeValidacion` |
|
|
189
|
+
| 429 | `LimiteDePeticiones` (con `segundos_de_espera` si se pudo leer) |
|
|
190
|
+
| otros / red | `FactuSmartError` |
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from factusmart import errores
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
r = fs.comprobantes.emitir(datos)
|
|
197
|
+
except errores.ErrorDeValidacion as e:
|
|
198
|
+
print("Datos inválidos:", e.mensaje) # no se cobró el crédito
|
|
199
|
+
except errores.SinCreditos:
|
|
200
|
+
print("Recarga créditos")
|
|
201
|
+
except errores.FactuSmartError as e:
|
|
202
|
+
print("Error", e.status, e.mensaje)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Reintentos e idempotencia
|
|
206
|
+
|
|
207
|
+
- `comprobantes.emitir` **siempre** manda el header `Idempotency-Key`: la tuya
|
|
208
|
+
(`idempotency_key="id-de-tu-venta"`) o un UUID v4 generado. **La misma clave
|
|
209
|
+
se usa en todos los reintentos de esa llamada**, así un reintento nunca emite
|
|
210
|
+
dos veces ni cobra dos veces. Si llamas dos veces con la misma clave,
|
|
211
|
+
recibes la misma respuesta.
|
|
212
|
+
- Se reintenta solo ante: error de red/timeout, HTTP 429, 502, 503 y 504, con
|
|
213
|
+
espera creciente (1 s, 2 s, 4 s…), hasta `max_reintentos` veces. Ante un
|
|
214
|
+
límite de peticiones se respeta el header `Retry-After` o el "Espera N
|
|
215
|
+
segundos" del mensaje.
|
|
216
|
+
- En `emitir`, el HTTP 409 ("ya hay una emisión en curso con esa
|
|
217
|
+
Idempotency-Key") también se reintenta con la misma clave.
|
|
218
|
+
- Se reintentan las consultas (GET) y `rucs.crear` (el API es idempotente por
|
|
219
|
+
RUC).
|
|
220
|
+
- **Nunca** se reintentan solas: `anulaciones.crear`, `guias.emitir`,
|
|
221
|
+
`comprobantes.reenviar`, `comprobantes.consultar_sunat` y `rucs.activar`.
|
|
222
|
+
|
|
223
|
+
## Pruebas (para quien mantiene el SDK)
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
python -m unittest discover -s tests -v
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- `test_unitarios.py` y `test_calculo.py` no usan red.
|
|
230
|
+
- `test_contrato.py` lee `openapi.yaml` (variable `FACTUSMART_OPENAPI`) y falla
|
|
231
|
+
si algún `operationId` no tiene método en el SDK.
|
|
232
|
+
- `test_integracion.py` va contra el API **local** (`FACTUSMART_TEST_BASE_URL`,
|
|
233
|
+
`FACTUSMART_TEST_API_KEY`, `FACTUSMART_TEST_RUC`; `FACTUSMART_INTEGRACION=0`
|
|
234
|
+
para saltarlas). Gasta 2 créditos por corrida. Se niega a correr contra
|
|
235
|
+
`factusmart.pe`.
|
|
236
|
+
|
|
237
|
+
## Licencia
|
|
238
|
+
|
|
239
|
+
MIT. Ver [LICENSE](LICENSE).
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# FactuSmart — SDK de Python
|
|
2
|
+
|
|
3
|
+
SDK oficial del **API de facturación electrónica de FactuSmart** (SUNAT, Perú):
|
|
4
|
+
facturas, boletas, notas de crédito/débito, guías de remisión (GRE 2.0) y
|
|
5
|
+
anulaciones.
|
|
6
|
+
|
|
7
|
+
- Python 3.8 o superior.
|
|
8
|
+
- **Cero dependencias**: solo librería estándar (`urllib`).
|
|
9
|
+
- Con type hints (`py.typed`).
|
|
10
|
+
- Documentación completa del API: https://factusmart.pe/api-facturacion-electronica/docs
|
|
11
|
+
|
|
12
|
+
## Instalar
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install factusmart
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Uso en 10 líneas
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import factusmart
|
|
22
|
+
|
|
23
|
+
fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
|
|
24
|
+
|
|
25
|
+
calc = factusmart.calcular_comprobante([
|
|
26
|
+
{"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
r = fs.comprobantes.emitir({
|
|
30
|
+
"codigo_tipo_documento": "01",
|
|
31
|
+
"datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
|
|
32
|
+
"items": calc["items"], "totales": calc["totales"],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
print(r["aceptado_por_sunat"], r["serie_numero"])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`base_url` **no es igual para todas las cuentas**: cada cuenta vive en un
|
|
39
|
+
servidor distinto. Cópiala de tu panel de integrador (pestaña Configuración,
|
|
40
|
+
"URL base de la API").
|
|
41
|
+
|
|
42
|
+
## Opciones del cliente
|
|
43
|
+
|
|
44
|
+
| Opción | Obligatoria | Por defecto | Para qué |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `api_key` | sí | — | Tu API key (va en el header `X-API-Key`) |
|
|
47
|
+
| `base_url` | sí | — | URL base de tu cuenta, sin barra final |
|
|
48
|
+
| `ruc` | no | — | RUC emisor por defecto. Se cambia en cada llamada con `ruc=` |
|
|
49
|
+
| `timeout` | no | `60` | Segundos por petición (SUNAT a veces tarda) |
|
|
50
|
+
| `max_reintentos` | no | `3` | Ver "Reintentos e idempotencia" |
|
|
51
|
+
| `verificar_ssl` | no | `True` | `False` **solo** en pruebas locales con certificado propio |
|
|
52
|
+
|
|
53
|
+
Si falta `api_key` o `base_url`, el cliente lanza `ValueError` al crearse.
|
|
54
|
+
|
|
55
|
+
## Métodos
|
|
56
|
+
|
|
57
|
+
Los datos se envían y se reciben **tal cual los define el API** (claves en
|
|
58
|
+
español con guion bajo). Todos los métodos que operan sobre un RUC aceptan
|
|
59
|
+
`ruc=` para cambiar el del constructor (en GET va como `?ruc=`; en POST va en el
|
|
60
|
+
cuerpo, y si tú ya pusiste `ruc` en los datos, se respeta el tuyo).
|
|
61
|
+
|
|
62
|
+
| Método | Endpoint | Devuelve |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `fs.ping(ruc=None)` | `GET /ping` | dict |
|
|
65
|
+
| `fs.cuenta.saldo()` | `GET /account` | dict |
|
|
66
|
+
| `fs.series.listar(ruc=None)` | `GET /series` | dict |
|
|
67
|
+
| `fs.comprobantes.emitir(datos, ruc=None, idempotency_key=None)` | `POST /documents` | dict |
|
|
68
|
+
| `fs.comprobantes.listar(filtros=None, ruc=None)` | `GET /documents` | dict |
|
|
69
|
+
| `fs.comprobantes.consultar(external_id, ruc=None)` | `GET /documents/{id}` | dict |
|
|
70
|
+
| `fs.comprobantes.descargar(external_id, tipo, ruc=None)` | `GET /documents/{id}/{pdf\|xml\|cdr}` | **bytes** |
|
|
71
|
+
| `fs.comprobantes.reenviar(external_id, ruc=None)` | `POST /documents/{id}/reenviar` | dict |
|
|
72
|
+
| `fs.comprobantes.consultar_sunat(external_id, ruc=None)` | `POST /documents/{id}/consultar-sunat` | dict |
|
|
73
|
+
| `fs.comprobantes.historial(external_id, ruc=None)` | `GET /documents/{id}/historial` | dict |
|
|
74
|
+
| `fs.anulaciones.crear(datos, ruc=None)` | `POST /anulaciones` | dict |
|
|
75
|
+
| `fs.anulaciones.estado(ticket, ruc=None)` | `GET /anulaciones/{ticket}/status` | dict |
|
|
76
|
+
| `fs.guias.emitir(datos, ruc=None)` | `POST /guias` | dict |
|
|
77
|
+
| `fs.guias.estado(external_id, ruc=None)` | `GET /guias/{id}/status` | dict |
|
|
78
|
+
| `fs.rucs.listar()` | `GET /rucs` | dict |
|
|
79
|
+
| `fs.rucs.crear(datos)` | `POST /rucs` | dict |
|
|
80
|
+
| `fs.rucs.activar(ruc, activo)` | `PATCH /rucs/{ruc}/activation` | dict |
|
|
81
|
+
|
|
82
|
+
Filtros de `comprobantes.listar`: `estado`, `desde`, `hasta` (YYYY-MM-DD),
|
|
83
|
+
`limite` (1 a 100) y `antes_de` (pasa el `siguiente_cursor` de la página
|
|
84
|
+
anterior).
|
|
85
|
+
|
|
86
|
+
Guardar el PDF:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
with open("comprobante.pdf", "wb") as f:
|
|
90
|
+
f.write(fs.comprobantes.descargar(r["external_id"], "pdf"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Lo que `comprobantes.emitir` completa por ti
|
|
94
|
+
|
|
95
|
+
Solo si no lo mandas:
|
|
96
|
+
|
|
97
|
+
- `numero_documento`: `"#"` (FactuSmart asigna el correlativo). Si mandas tu
|
|
98
|
+
propio número, se respeta.
|
|
99
|
+
- `fecha_de_emision` y `hora_de_emision`: fecha y hora **de Perú**
|
|
100
|
+
(America/Lima), no la de tu servidor.
|
|
101
|
+
- `codigo_tipo_moneda`: `"PEN"`.
|
|
102
|
+
- `codigo_tipo_operacion`: `"0101"` en facturas y boletas (en notas no aplica).
|
|
103
|
+
|
|
104
|
+
`guias.emitir` completa igual `numero_documento`, `fecha_de_emision` y
|
|
105
|
+
`hora_de_emision`.
|
|
106
|
+
|
|
107
|
+
## Calcular IGV y totales
|
|
108
|
+
|
|
109
|
+
El API espera los importes ya calculados. `calcular_comprobante` lo hace por ti:
|
|
110
|
+
recibe cada ítem con `descripcion`, `cantidad`, `valor_unitario` (**sin IGV**) y
|
|
111
|
+
`afectacion`, y devuelve `{"items": [...], "totales": {...}}` listo para enviar.
|
|
112
|
+
|
|
113
|
+
| `afectacion` | Significado | IGV |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `"10"` | Gravado | 18% |
|
|
116
|
+
| `"20"` | Exonerado | 0 |
|
|
117
|
+
| `"30"` | Inafecto | 0 |
|
|
118
|
+
| `"40"` | Exportación | 0 (va en `total_exportacion`) |
|
|
119
|
+
|
|
120
|
+
Opcionales por ítem: `codigo_interno` (recomendado: el SKU de tu sistema) y
|
|
121
|
+
`unidad_de_medida` (por defecto `NIU`; usa `ZZ` para servicios).
|
|
122
|
+
|
|
123
|
+
Redondea a 2 decimales por ítem con `decimal.Decimal` y ROUND_HALF_UP, así da
|
|
124
|
+
**exactamente los mismos números** que los SDK de PHP y Node.
|
|
125
|
+
|
|
126
|
+
## La regla de oro: revisa `aceptado_por_sunat`
|
|
127
|
+
|
|
128
|
+
Un HTTP 200 significa que FactuSmart **generó** tu comprobante, no que SUNAT lo
|
|
129
|
+
aceptó. SUNAT puede estar caída o rechazarlo: recibirás `estado: "01"` con
|
|
130
|
+
`aceptado_por_sunat: False` y el motivo en `sunat["descripcion"]`.
|
|
131
|
+
|
|
132
|
+
**Eso NO es una excepción.** El SDK te devuelve la respuesta tal cual y tú
|
|
133
|
+
decides:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
r = fs.comprobantes.emitir(datos)
|
|
137
|
+
if not r["aceptado_por_sunat"]:
|
|
138
|
+
print("SUNAT no lo aceptó todavía:", r.get("sunat"), r.get("accion_requerida"))
|
|
139
|
+
# corrige y reintenta gratis con: fs.comprobantes.reenviar(r["external_id"])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Mira también `entorno` (`"prueba"` o `"produccion"`) y `tiene_validez_fiscal`.
|
|
143
|
+
|
|
144
|
+
## Errores
|
|
145
|
+
|
|
146
|
+
Todas las excepciones están en `factusmart.errores` (y también en `factusmart`)
|
|
147
|
+
y heredan de `FactuSmartError`, que trae:
|
|
148
|
+
|
|
149
|
+
- `status`: código HTTP (`None` si fue un error de red),
|
|
150
|
+
- `mensaje`: el mensaje del API, en español,
|
|
151
|
+
- `respuesta`: el cuerpo completo de la respuesta.
|
|
152
|
+
|
|
153
|
+
| HTTP | Excepción |
|
|
154
|
+
|---|---|
|
|
155
|
+
| 401 | `ErrorDeAutenticacion` |
|
|
156
|
+
| 402 | `SinCreditos` |
|
|
157
|
+
| 403 | `RucNoAutorizado` |
|
|
158
|
+
| 404 | `NoEncontrado` |
|
|
159
|
+
| 422 | `ErrorDeValidacion` |
|
|
160
|
+
| 429 | `LimiteDePeticiones` (con `segundos_de_espera` si se pudo leer) |
|
|
161
|
+
| otros / red | `FactuSmartError` |
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from factusmart import errores
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
r = fs.comprobantes.emitir(datos)
|
|
168
|
+
except errores.ErrorDeValidacion as e:
|
|
169
|
+
print("Datos inválidos:", e.mensaje) # no se cobró el crédito
|
|
170
|
+
except errores.SinCreditos:
|
|
171
|
+
print("Recarga créditos")
|
|
172
|
+
except errores.FactuSmartError as e:
|
|
173
|
+
print("Error", e.status, e.mensaje)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Reintentos e idempotencia
|
|
177
|
+
|
|
178
|
+
- `comprobantes.emitir` **siempre** manda el header `Idempotency-Key`: la tuya
|
|
179
|
+
(`idempotency_key="id-de-tu-venta"`) o un UUID v4 generado. **La misma clave
|
|
180
|
+
se usa en todos los reintentos de esa llamada**, así un reintento nunca emite
|
|
181
|
+
dos veces ni cobra dos veces. Si llamas dos veces con la misma clave,
|
|
182
|
+
recibes la misma respuesta.
|
|
183
|
+
- Se reintenta solo ante: error de red/timeout, HTTP 429, 502, 503 y 504, con
|
|
184
|
+
espera creciente (1 s, 2 s, 4 s…), hasta `max_reintentos` veces. Ante un
|
|
185
|
+
límite de peticiones se respeta el header `Retry-After` o el "Espera N
|
|
186
|
+
segundos" del mensaje.
|
|
187
|
+
- En `emitir`, el HTTP 409 ("ya hay una emisión en curso con esa
|
|
188
|
+
Idempotency-Key") también se reintenta con la misma clave.
|
|
189
|
+
- Se reintentan las consultas (GET) y `rucs.crear` (el API es idempotente por
|
|
190
|
+
RUC).
|
|
191
|
+
- **Nunca** se reintentan solas: `anulaciones.crear`, `guias.emitir`,
|
|
192
|
+
`comprobantes.reenviar`, `comprobantes.consultar_sunat` y `rucs.activar`.
|
|
193
|
+
|
|
194
|
+
## Pruebas (para quien mantiene el SDK)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
python -m unittest discover -s tests -v
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `test_unitarios.py` y `test_calculo.py` no usan red.
|
|
201
|
+
- `test_contrato.py` lee `openapi.yaml` (variable `FACTUSMART_OPENAPI`) y falla
|
|
202
|
+
si algún `operationId` no tiene método en el SDK.
|
|
203
|
+
- `test_integracion.py` va contra el API **local** (`FACTUSMART_TEST_BASE_URL`,
|
|
204
|
+
`FACTUSMART_TEST_API_KEY`, `FACTUSMART_TEST_RUC`; `FACTUSMART_INTEGRACION=0`
|
|
205
|
+
para saltarlas). Gasta 2 créditos por corrida. Se niega a correr contra
|
|
206
|
+
`factusmart.pe`.
|
|
207
|
+
|
|
208
|
+
## Licencia
|
|
209
|
+
|
|
210
|
+
MIT. Ver [LICENSE](LICENSE).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "factusmart"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "SDK oficial de Python del API de facturación electrónica de FactuSmart (SUNAT, Perú)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "FactuSmart" }]
|
|
13
|
+
keywords = ["factusmart", "sunat", "facturacion electronica", "peru", "cpe", "boleta", "factura"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Natural Language :: Spanish",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Documentacion = "https://factusmart.pe/api-facturacion-electronica/docs/sdks"
|
|
35
|
+
Repositorio = "https://github.com/factusmart/factusmart-python"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.dynamic]
|
|
38
|
+
version = { attr = "factusmart._version.__version__" }
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
factusmart = ["py.typed"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""SDK de Python del API de facturación electrónica de FactuSmart (SUNAT, Perú).
|
|
2
|
+
|
|
3
|
+
Uso::
|
|
4
|
+
|
|
5
|
+
import factusmart
|
|
6
|
+
|
|
7
|
+
fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
|
|
8
|
+
calc = factusmart.calcular_comprobante([
|
|
9
|
+
{"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
|
|
10
|
+
])
|
|
11
|
+
r = fs.comprobantes.emitir({
|
|
12
|
+
"codigo_tipo_documento": "01",
|
|
13
|
+
"datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
|
|
14
|
+
**calc,
|
|
15
|
+
})
|
|
16
|
+
print(r["aceptado_por_sunat"], r["serie_numero"])
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from . import errores
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
from .calculo import calcular_comprobante
|
|
22
|
+
from .cliente import OPERACIONES, Cliente, Transporte, hora_peru
|
|
23
|
+
from .errores import (
|
|
24
|
+
ErrorDeAutenticacion,
|
|
25
|
+
ErrorDeValidacion,
|
|
26
|
+
FactuSmartError,
|
|
27
|
+
LimiteDePeticiones,
|
|
28
|
+
NoEncontrado,
|
|
29
|
+
RucNoAutorizado,
|
|
30
|
+
SinCreditos,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"__version__",
|
|
35
|
+
"Cliente",
|
|
36
|
+
"Transporte",
|
|
37
|
+
"OPERACIONES",
|
|
38
|
+
"calcular_comprobante",
|
|
39
|
+
"hora_peru",
|
|
40
|
+
"errores",
|
|
41
|
+
"FactuSmartError",
|
|
42
|
+
"ErrorDeAutenticacion",
|
|
43
|
+
"SinCreditos",
|
|
44
|
+
"RucNoAutorizado",
|
|
45
|
+
"NoEncontrado",
|
|
46
|
+
"ErrorDeValidacion",
|
|
47
|
+
"LimiteDePeticiones",
|
|
48
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|