arca-client 0.6.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 (55) hide show
  1. arca_client-0.6.0/.gitignore +21 -0
  2. arca_client-0.6.0/CHANGELOG.md +102 -0
  3. arca_client-0.6.0/CLAUDE.md +31 -0
  4. arca_client-0.6.0/LICENSE +21 -0
  5. arca_client-0.6.0/PKG-INFO +465 -0
  6. arca_client-0.6.0/README.md +448 -0
  7. arca_client-0.6.0/examples/_wsaa.py +32 -0
  8. arca_client-0.6.0/examples/factura_a.py +71 -0
  9. arca_client-0.6.0/examples/factura_c.py +50 -0
  10. arca_client-0.6.0/examples/login.py +23 -0
  11. arca_client-0.6.0/examples/verificar_comprobante.py +39 -0
  12. arca_client-0.6.0/pyproject.toml +49 -0
  13. arca_client-0.6.0/src/arca_client/__init__.py +81 -0
  14. arca_client-0.6.0/src/arca_client/certificates.py +121 -0
  15. arca_client-0.6.0/src/arca_client/environments.py +50 -0
  16. arca_client-0.6.0/src/arca_client/exceptions.py +113 -0
  17. arca_client-0.6.0/src/arca_client/setup_wizard.py +382 -0
  18. arca_client-0.6.0/src/arca_client/transport.py +36 -0
  19. arca_client-0.6.0/src/arca_client/wsaa/__init__.py +13 -0
  20. arca_client-0.6.0/src/arca_client/wsaa/cache.py +194 -0
  21. arca_client-0.6.0/src/arca_client/wsaa/client.py +138 -0
  22. arca_client-0.6.0/src/arca_client/wsaa/signer.py +35 -0
  23. arca_client-0.6.0/src/arca_client/wsaa/ticket.py +67 -0
  24. arca_client-0.6.0/src/arca_client/wsaa/tra.py +37 -0
  25. arca_client-0.6.0/src/arca_client/wscdc/__init__.py +3 -0
  26. arca_client-0.6.0/src/arca_client/wscdc/client.py +305 -0
  27. arca_client-0.6.0/src/arca_client/wsfe/__init__.py +41 -0
  28. arca_client-0.6.0/src/arca_client/wsfe/client.py +997 -0
  29. arca_client-0.6.0/src/arca_client/wsfe/fonts/LiberationSans-Bold.ttf +0 -0
  30. arca_client-0.6.0/src/arca_client/wsfe/fonts/LiberationSans-Regular.ttf +0 -0
  31. arca_client-0.6.0/src/arca_client/wsfe/fonts/OFL.txt +104 -0
  32. arca_client-0.6.0/src/arca_client/wsfe/pdf.py +628 -0
  33. arca_client-0.6.0/src/arca_client/wsfe/qr.py +125 -0
  34. arca_client-0.6.0/src/arca_client/wsfe/types.py +279 -0
  35. arca_client-0.6.0/src/arca_client/wspci/__init__.py +3 -0
  36. arca_client-0.6.0/src/arca_client/wspci/client.py +369 -0
  37. arca_client-0.6.0/tests/conftest.py +70 -0
  38. arca_client-0.6.0/tests/fixtures/wscdc.wsdl +437 -0
  39. arca_client-0.6.0/tests/fixtures/wsfev1.wsdl +1596 -0
  40. arca_client-0.6.0/tests/fixtures/wspci.wsdl +401 -0
  41. arca_client-0.6.0/tests/integration/__init__.py +0 -0
  42. arca_client-0.6.0/tests/integration/test_wspci.py +104 -0
  43. arca_client-0.6.0/tests/test_cache.py +180 -0
  44. arca_client-0.6.0/tests/test_certificates.py +56 -0
  45. arca_client-0.6.0/tests/test_client.py +163 -0
  46. arca_client-0.6.0/tests/test_cuit_from_cert.py +107 -0
  47. arca_client-0.6.0/tests/test_qr_pdf.py +422 -0
  48. arca_client-0.6.0/tests/test_setup_wizard.py +78 -0
  49. arca_client-0.6.0/tests/test_signer.py +15 -0
  50. arca_client-0.6.0/tests/test_ticket.py +32 -0
  51. arca_client-0.6.0/tests/test_tra.py +60 -0
  52. arca_client-0.6.0/tests/test_wscdc.py +475 -0
  53. arca_client-0.6.0/tests/test_wsfe.py +939 -0
  54. arca_client-0.6.0/tests/test_wspci.py +617 -0
  55. arca_client-0.6.0/uv.lock +1250 -0
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+
10
+ # Credenciales y caché de tickets: nunca commitear
11
+ *.key
12
+ *.crt
13
+ *.pem
14
+ *.csr
15
+ *.p12
16
+ .ta_cache/
17
+ certs/
18
+ .env
19
+
20
+ # PDFs generados por los ejemplos
21
+ factura_*.pdf
@@ -0,0 +1,102 @@
1
+ # Cambios
2
+
3
+ Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/); versiones según [SemVer](https://semver.org/lang/es/) (en `0.x`, la versión minor puede traer cambios de comportamiento: están marcados abajo).
4
+
5
+ ## [0.6.0] - 2026-11-14
6
+
7
+ ### Seguridad
8
+
9
+ - `parse_qr_url` / `WSCDC.verify_qr`: un QR con un número con exponente gigante (por ejemplo `"ptoVta": 1e999999999`) dejaba el proceso colgado al convertirlo a entero, y en el importe lanzaba `decimal.InvalidOperation` en vez de `ValueError`. Ahora se validan los datos del QR antes de convertirlos: los enteros (`ver`, `cuit`, `ptoVta`, `tipoCmp`, `nroCmp`, `tipoDocRec`, `nroDocRec`, `codAut`) tienen que ser enteros de JSON con una cantidad acotada de dígitos, y el importe, un número de hasta 13 enteros y 2 decimales, sin exponente. Cualquier otro formato lanza `ValueError` sin consultar a ARCA.
10
+ - `WSCDC.verify_qr`: el receptor del QR reemplazaba en silencio al que indicaba quien llama, así que una factura auténtica emitida a otra persona podía salir `aprobada=True`. Ahora, si se indica `doc_tipo_receptor`/`doc_nro_receptor` y el QR trae otro receptor, se lanza `ValueError` sin consultar a ARCA.
11
+ - Los importes (`_monto`) ya no dejan escapar `decimal.InvalidOperation`: un valor fuera de rango, `NaN` o `Infinity` lanza `ValueError`.
12
+ - `FileTicketCache.set` escribe el TA en un temporal con nombre único (`tempfile.mkstemp`, 0600) en vez de `<clave>.tmp`: en un directorio compartido, otro usuario ya no puede plantar un symlink para redirigir las credenciales.
13
+ - `FileTicketCache` verifica que la carpeta sea privada antes de usarla (en `get`, `set` y `lock`): tiene que ser del usuario actual y sin permisos para grupo ni otros (si la ruta es un symlink, se verifica la carpeta a la que apunta). Si no, lanza `ArcaError` (no corrige los permisos). Los `.json` y `.lock` se abren con `O_NOFOLLOW`: un symlink plantado da error en vez de seguirse. Así, en una carpeta compartida, otro usuario ya no puede plantar tickets falsos ni hacer que se creen archivos en otro lado. En Windows no se verifican dueño ni permisos.
14
+
15
+ ### Agregado
16
+
17
+ - `Constatacion.comprobante`: un `ComprobanteConsultado` con los datos que se enviaron a ARCA (con `verify_qr`, los del QR), para compararlos con la factura impresa.
18
+
19
+ ### Cambiado
20
+
21
+ - `parse_qr_url` rechaza QR que antes aceptaba: números con exponente o con más dígitos de los que admite la especificación, enteros escritos con decimales (`3.0`) o como texto (`"5"`), y `true`/`false` en lugar de números.
22
+ - **Cambio de comportamiento:** `FileTicketCache` rechaza con `ArcaError` una carpeta que ya existe y no es privada (por ejemplo, `ARCA_TA_CACHE` apuntando a una carpeta con permisos `755`). Se arregla con `chmod 700 <carpeta>`.
23
+ - Las respuestas que no se pueden interpretar ya no se copian en el mensaje del error (pueden tener CUIT, CAE y datos del receptor, y terminar en logs). WSCDC lanza `RespuestaNoReconocida` (nueva, subclase de `ArcaError`) con la respuesta en el atributo `respuesta`. En WSFE, `WSFEConnectionError.respuesta` la tiene cuando el corte fue una respuesta irreconocible.
24
+ - `WSCDC.verify_qr` trata CUIT (80), CUIL (86) y CDI (87) como el mismo documento al comparar el receptor del QR con el indicado: un QR con CUIL y quien llama con CUIT del mismo número ya no dan `ValueError`. Si el número coincide, a ARCA se envía el tipo y el número del QR (los registrados en el comprobante): con el tipo de quien llama, ARCA podría no encontrar una factura emitida a un CUIL y rechazarla. `Constatacion.comprobante` refleja lo que se envió.
25
+
26
+ ## [0.5.0] - 2026-11-05
27
+
28
+ ### Agregado
29
+
30
+ - **Constancia de Inscripción (WSPCI)**: `WSPCI.get_constancia(cuit)` devuelve una `Constancia` con los datos del contribuyente (`denominacion`, `tipo_persona`, `domicilio_fiscal`, `impuestos`, `categoria_monotributo`, etc.). `WSPCI.dummy()` consulta el estado de los servidores sin pedir ticket. Nuevos `Domicilio`, `Impuesto`, `Categoria`, `TipoPersona`, `WSPCIError` y `PersonaNoEncontrada`.
31
+ - `Constancia.condicion_iva`: condición frente al IVA deducida de los impuestos, o `None` si no es segura. La deducción es de esta librería, no de ARCA.
32
+ - `tests/integration/test_wspci.py`: pruebas contra homologación, que se saltean sin `ARCA_TEST_CERT`, `ARCA_TEST_KEY` y `ARCA_TEST_CUIT`. Las CUIT a consultar se indican con `ARCA_TEST_CUITS`.
33
+
34
+ ### Para actualizar
35
+
36
+ - El certificado hay que autorizarlo a `ws_sr_constancia_inscripcion`, aunque ya funcione con `wsfe`: hasta entonces, ARCA responde `coe.notAuthorized`. En homologación se hace desde WSASS y en producción desde el *Administrador de Relaciones de Clave Fiscal*.
37
+
38
+ ### Sin confirmar
39
+
40
+ - Probado solo en homologación y con pocos casos, no en producción. Tampoco se confirmó si el impuesto 32 es IVA exento ni cómo responde ARCA con una CUIL. Ver "Limitaciones conocidas" en el README.
41
+
42
+ ## [0.4.0] - 2026-10-27
43
+
44
+ ### Agregado
45
+
46
+ - Notas de crédito y débito B (`create_credit_note_b` / `create_debit_note_b`, tipos 8 y 7) y C (`create_credit_note_c` / `create_debit_note_c`, tipos 13 y 12). Llevan los mismos argumentos que la Factura B o C, más `asociado=` o `periodo=`. El PDF las muestra como las notas A.
47
+ - `CbteTipo` suma `NOTA_DEBITO_B`, `NOTA_CREDITO_B`, `NOTA_DEBITO_C` y `NOTA_CREDITO_C`.
48
+ - **Constatación de comprobantes (WSCDC)**: `WSCDC.verify_voucher(...)` y `WSCDC.verify_qr(texto_del_qr)` verifican en ARCA las facturas recibidas de proveedores y devuelven una `Constatacion` (`aprobada`, `observaciones`, `errores`). También: `tipos_comprobante()`, `tipos_documento()`, `modalidades()` y `estado()`. Nuevos `WSCDCError` y `ModoAutorizacion`.
49
+ - `parse_qr_url(texto)`: lee el JSON del QR de un comprobante (RG 4892).
50
+ - Ejemplo `examples/verificar_comprobante.py`.
51
+ - Los ejemplos admiten claves privadas con contraseña: `ARCA_KEY_PASSWORD` o, si no está definida, la piden por teclado.
52
+
53
+ ### Cambiado
54
+
55
+ - **PDF**: usa Liberation Sans (incluida, licencia SIL OFL 1.1) en lugar de Helvetica. El diseño no cambia (mismas métricas), pero ahora se imprimen tal cual los guiones largos, las comillas tipográficas, €, ™, el griego, el cirílico, etc. Solo los caracteres que la fuente no tiene (emojis, CJK) se reemplazan por `?`. El PDF pesa unos 30 KB más porque incluye un subconjunto de la fuente.
56
+ - El extra `pdf` declara `fonttools` (ya venía como dependencia de `fpdf2`).
57
+
58
+ ### Corregido
59
+
60
+ - **Producción**: WSFEv1 (y WSCDC) no conectaban con OpenSSL 3 (`DH_KEY_TOO_SMALL`), porque `servicios1.afip.gov.ar` usa la misma clave DH chica que homologación. Ahora se acepta `SECLEVEL=1` solo para ese servidor, sin dejar de verificar el certificado ni el host. El WSAA de producción sigue con la configuración TLS por defecto.
61
+
62
+ ## [0.3.0] - 2026-10-26
63
+
64
+ ### Agregado
65
+
66
+ - `cuit_from_certificate` es parte de la API pública (`from arca_client import cuit_from_certificate`) y acepta, además de un `x509.Certificate`, el PEM en bytes o una ruta, igual que `WSAA(cert=...)`.
67
+ - `certificate_paths(alias, env, base_dir="certs")`: rutas `<base_dir>/<ambiente>/<alias>.crt|.key` donde guarda los archivos `arca-setup`.
68
+ - `WSAA.from_alias(alias, env, base_dir="certs", **kwargs)`: carga el certificado y la clave desde esa convención; si falta alguno, `ArcaError` con la ruta buscada.
69
+
70
+ ### Cambiado
71
+
72
+ - `arca-setup` pide la carpeta base (por defecto `certs`) en lugar de la carpeta final, y siempre guarda en `<carpeta base>/<ambiente>/`. Con los valores por defecto, las rutas son las mismas que antes.
73
+
74
+ ## [0.2.0] - 2026-10-26
75
+
76
+ ### Agregado
77
+
78
+ - **Responsable inscripto**: `create_invoice_a`, `create_invoice_b`, `create_credit_note_a` y `create_debit_note_a`. Los importes se pasan como neto por alícuota (`neto={Alicuota.IVA_21: "1000.00"}`) y la librería calcula el IVA (half even) y el total. Admiten `exento=`, `no_gravado=` y `tributos=`.
79
+ - Otros tributos (`Tributo`, `TipoTributo`): percepciones, impuestos internos, etc., también en `create_invoice_c`.
80
+ - Nuevos tipos: `Alicuota`, `IvaAlicuota`, `ComprobanteAsociado`; `CbteTipo` suma `FACTURA_A`, `NOTA_DEBITO_A`, `NOTA_CREDITO_A` y `FACTURA_B`.
81
+ - `ComprobanteAutorizado` suma (con valores por defecto) `importe_neto`, `importe_iva`, `importe_no_gravado`, `importe_exento`, `importe_tributos`, `iva`, `tributos`, `asociados` y `periodo_asociado`. `get_voucher` los completa.
82
+ - Consultas de parámetros: `condiciones_iva_receptor(clase)`, `tipos_iva()` y `tipos_tributo()`.
83
+ - PDF de comprobantes A, B y notas de crédito/débito: IVA discriminado en la A, precios con IVA y leyenda de la Ley 27.743 en la B, comprobante asociado y tabla de otros tributos. `Item.alicuota` y `neto_por_alicuota(items)`.
84
+ - Ejemplo `examples/factura_a.py`.
85
+ - `FileTicketCache` serializa los pedidos de TA de varios procesos con un archivo de bloqueo; los backends propios pueden hacer lo mismo con un método `lock(key)` opcional.
86
+
87
+ ### Cambiado
88
+
89
+ - **Error 10016 al emitir**: si otro proceso usó el número, se reintenta con el siguiente (hasta `numbering_attempts`, 3 por defecto) en vez de lanzar `WSFEError`. Si el último autorizado no avanzó, el 10016 se sigue informando como antes.
90
+ - **CUIT/CUIL del receptor inválida**: ahora se rechaza localmente con `ValueError` (dígito verificador), antes de llamar a ARCA. Antes llegaba como `WSFEError`.
91
+ - La recuperación ante cortes compara también el neto y el IVA del comprobante encontrado.
92
+ - El mensaje de `WSFEConnectionError` sin verificación dice "NO reintentes la emisión" (antes nombraba `create_invoice_c`).
93
+ - `Emisor.condicion_iva` es `None` por defecto y se deriva de la letra; en la Factura C sigue imprimiendo "Responsable Monotributo".
94
+
95
+ ### Corregido
96
+
97
+ - WSAA: se reutiliza el TA aunque el reloj local atrase respecto del de ARCA.
98
+ - WSAA: con backends de caché sin `lock(key)`, ante `coe.alreadyAuthenticated` por pedidos simultáneos se relee el caché una vez antes de fallar.
99
+
100
+ ## [0.1.0]
101
+
102
+ Versión inicial: WSAA (TRA, firma CMS, caché de tickets), asistente `arca-setup`, Factura C con recuperación ante cortes, código QR y PDF.
@@ -0,0 +1,31 @@
1
+ # arca-client
2
+
3
+ Cliente Python para los web services SOAP de ARCA (ex AFIP): WSAA (autenticación) y WSFEv1 (Factura C).
4
+ Código, docstrings, mensajes y README en español.
5
+
6
+ ## Comandos
7
+ - `uv run pytest` — tests (no tocan la red)
8
+ - `uv run ruff check . && uv run ruff format .`
9
+ - `uv run arca-setup` — asistente interactivo (no se puede correr desde Claude: necesita teclado)
10
+
11
+ ## Reglas importantes
12
+ - **No asumir nombres de campos por el manual de WSFEv1**: sus ejemplos XML tienen errores
13
+ (dice `FEDetResponse`, el real es `FECAEDetResponse`; `Observaciones.Obs`). La fuente de verdad
14
+ es el WSDL: `tests/fixtures/wsfev1.wsdl` (y `tests/fixtures/wscdc.wsdl` para WSCDC). Los fakes
15
+ de los tests se arman con esos tipos (`_t(...)`).
16
+ - **Nunca reintentar `create_invoice_c` a ciegas**: recalcula el número y puede duplicar la factura.
17
+ Ante cortes se verifica con `get_voucher` (FECompConsultar). Una respuesta que no se entiende
18
+ jamás se informa como rechazo.
19
+ - **Llamadas reales a ARCA**: en homologación, `FECAESolicitar` emite comprobantes de verdad
20
+ (sin validez fiscal, pero consumen numeración). Avisar antes de emitir; consultar
21
+ (`last_voucher_number`, `get_voucher`) es seguro.
22
+ - **Ticket de acceso (TA)**: dura 12 h y ARCA no permite pedir otro mientras haya uno vigente
23
+ (`coe.alreadyAuthenticated`). No borrar `~/.cache/arca-client/tickets/` ni forzar `force=True`.
24
+ - **Datos personales**: el repo es público. Nunca commitear `certs/`, PDFs generados, CUIT ni CAE
25
+ reales (en tests usar valores ficticios, ej. CUIT 30712345671; 20123456786 existe en el padrón de homologación). Revisar el diff antes de cada commit.
26
+ - Importes siempre `Decimal`/`str`/`int`, nunca `float`; redondeo half even (criterio de ARCA).
27
+ - `SECLEVEL=1` en TLS (`transport.py`): en homologación para todo y en producción solo para
28
+ `servicios1.afip.gov.ar` (WSFEv1, WSCDC; el WSAA no lo necesita). Todas las llamadas usan `operation_timeout`.
29
+
30
+ ## Commits
31
+ Mensajes en español. Solo commitear/pushear cuando el usuario lo pide.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fzurs
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,465 @@
1
+ Metadata-Version: 2.5
2
+ Name: arca-client
3
+ Version: 0.6.0
4
+ Summary: Cliente Python para los web services de ARCA (ex AFIP)
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: cryptography>=42
9
+ Requires-Dist: lxml>=5
10
+ Requires-Dist: requests>=2.31
11
+ Requires-Dist: zeep>=4.2
12
+ Provides-Extra: pdf
13
+ Requires-Dist: fonttools>=4.34; extra == 'pdf'
14
+ Requires-Dist: fpdf2>=2.8; extra == 'pdf'
15
+ Requires-Dist: segno>=1.6; extra == 'pdf'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # arca-client
19
+
20
+ Cliente Python para los web services SOAP de **ARCA** (ex AFIP).
21
+
22
+ Estado actual: **autenticación (WSAA)**, **factura electrónica (WSFEv1)**, **constatación de comprobantes (WSCDC)** y **constancia de inscripción (WSPCI)**. Con WSFEv1 se emiten la Factura C y sus notas de crédito/débito para monotributistas, y la Factura A, la Factura B y sus notas de crédito/débito para responsables inscriptos. Con WSCDC se verifican las facturas recibidas de proveedores. Con WSPCI se obtienen los datos de un contribuyente a partir de su CUIT.
23
+
24
+ ## Instalación
25
+
26
+ ```bash
27
+ uv sync # desarrollo
28
+ pip install arca-client[pdf] # como dependencia, con generación de PDF
29
+ ```
30
+
31
+ ## Requisitos previos: certificado
32
+
33
+ Para usar cualquier web service de ARCA se necesita un certificado X.509 emitido por ARCA, asociado al servicio que se quiere consumir.
34
+
35
+ ### Asistente interactivo (recomendado)
36
+
37
+ ```bash
38
+ uv run arca-setup
39
+ ```
40
+
41
+ El asistente:
42
+
43
+ 1. Pide el ambiente, la CUIT (valida el dígito verificador), la razón social y el alias.
44
+ 2. Genera la clave privada (`<alias>.key`, permisos 0600, contraseña opcional) y el CSR (`<alias>.csr`) en `<carpeta base>/<ambiente>/` (por defecto `certs/homologacion/` o `certs/produccion/`).
45
+ 3. Abre el login de ARCA en el navegador y explica paso a paso dónde pegar el CSR: WSASS en homologación, *Administración de Certificados Digitales* en producción.
46
+ 4. Pide que pegues el certificado emitido (o su ruta), verifica que corresponda a la clave y lo guarda como `<alias>.crt`.
47
+ 5. Explica cómo autorizar el certificado a un web service: WSASS en homologación, *Administrador de Relaciones de Clave Fiscal* en producción.
48
+ 6. Opcionalmente prueba un login real contra el WSAA.
49
+
50
+ Los archivos quedan en `certs/<ambiente>/<alias>.crt|.key`, y `WSAA.from_alias` los carga desde ahí:
51
+
52
+ ```python
53
+ from arca_client import WSAA, Environment, certificate_paths
54
+
55
+ wsaa = WSAA.from_alias("miapp", Environment.HOMOLOGACION)
56
+ # Equivale a WSAA("certs/homologacion/miapp.crt", "certs/homologacion/miapp.key", ...)
57
+
58
+ cert_path, key_path = certificate_paths("miapp", "produccion", base_dir="/srv/app/certs")
59
+ ```
60
+
61
+ `base_dir` (por defecto `"certs"`) se resuelve contra el directorio actual: una aplicación conviene que pase una ruta absoluta, por ejemplo `WSAA.from_alias("miapp", env, base_dir=BASE_DIR / "certs")`. El resto de los argumentos (`key_password`, `cache`, `timeout`, …) van igual que en `WSAA(...)`. Si falta el `.crt` o el `.key`, lanza `ArcaError` con la ruta buscada.
62
+
63
+ ### Manual con OpenSSL
64
+
65
+ 1. Generar la clave privada y el CSR:
66
+ ```bash
67
+ openssl genrsa -out private.key 2048
68
+ openssl req -new -key private.key -out pedido.csr \
69
+ -subj "/C=AR/O=Mi Empresa/CN=mi-sistema/serialNumber=CUIT 20123456780"
70
+ ```
71
+ 2. **Homologación**: con clave fiscal, entrar a *WSASS - Autogestión Certificados Homologación*, pegar el CSR y descargar el certificado. Después, en WSASS, crear la autorización del certificado para el servicio (ej. `wsfe`).
72
+ 3. **Producción**: usar *Administrador de Certificados Digitales* para emitir el certificado y *Administrador de Relaciones de Clave Fiscal* para asociarlo al servicio.
73
+
74
+ Documentación oficial: <https://www.arca.gob.ar/ws/documentacion/wsaa.asp>
75
+
76
+ ## Uso
77
+
78
+ ```python
79
+ from arca_client import WSAA, Environment
80
+
81
+ wsaa = WSAA("cert.crt", "private.key", Environment.HOMOLOGACION)
82
+ ticket = wsaa.get_ticket("wsfe")
83
+
84
+ ticket.token, ticket.sign, ticket.expiration_time
85
+ ```
86
+
87
+ O con el ejemplo incluido:
88
+
89
+ ```bash
90
+ ARCA_CERT=cert.crt ARCA_KEY=private.key uv run python examples/login.py wsfe
91
+ ```
92
+
93
+ Todos los ejemplos leen el ambiente de `ARCA_ENV` (`homologacion` por defecto). Si la clave privada tiene contraseña, la toman de `ARCA_KEY_PASSWORD` o, si no está definida, la piden por teclado.
94
+
95
+ ### Caché de tickets
96
+
97
+ Un Ticket de Acceso (TA) dura **12 horas**, y el WSAA rechaza pedir uno nuevo mientras siga vigente el anterior (`coe.alreadyAuthenticated`). Por eso `get_ticket()` guarda el TA en disco (con permisos `0600`) y lo reutiliza hasta 5 minutos antes de que venza.
98
+
99
+ La ubicación es fija, sin importar desde qué directorio se ejecute:
100
+
101
+ 1. `$ARCA_TA_CACHE`, si está definida.
102
+ 2. `$XDG_CACHE_HOME/arca-client/tickets/`.
103
+ 3. `~/.cache/arca-client/tickets/` (valor por defecto).
104
+
105
+ Cada archivo se llama `<ambiente>-<servicio>-<huella del certificado>.json`. **No borres esta carpeta mientras haya tickets vigentes**: si se pierde el TA, ARCA no deja pedir otro hasta que venza (hasta 12 h).
106
+
107
+ **La carpeta tiene que ser privada**, porque el TA es una credencial. Si no existe, se crea con permisos `0700`. Si ya existe, tiene que ser tuya y sin permisos para grupo ni otros; si la ruta es un symlink, se verifica la carpeta a la que apunta. Si no cumple, se lanza `ArcaError` y no se corrigen los permisos solos: no apuntes `ARCA_TA_CACHE` a una carpeta compartida como `/tmp`, o hacela privada con `chmod 700 <carpeta>`. Los archivos `.json` y `.lock` se abren sin seguir symlinks. En Windows no se verifican dueño ni permisos.
108
+
109
+ Se puede usar otro backend con cualquier objeto que tenga `get(key)` y `set(key, ticket)`, por ejemplo `MemoryTicketCache()` o uno propio sobre Redis:
110
+
111
+ ```python
112
+ from arca_client import MemoryTicketCache
113
+
114
+ wsaa = WSAA(cert, key, cache=MemoryTicketCache())
115
+ ```
116
+
117
+ `MemoryTicketCache` pierde el TA cuando termina el proceso, así que solo conviene para tests o para un proceso que corre de forma permanente.
118
+
119
+ Si varios procesos piden el TA a la vez, `FileTicketCache` los serializa con un archivo de bloqueo (`<clave>.lock`): solo uno llama al WSAA y los demás leen el TA del caché. Un backend propio puede hacer lo mismo con un método `lock(key)` que devuelva un context manager (por ejemplo, un lock de Redis); si no lo tiene, ante `coe.alreadyAuthenticated` se relee el caché una vez antes de fallar. El bloqueo de archivo solo coordina procesos de la misma máquina.
120
+
121
+ ## Factura electrónica (WSFEv1)
122
+
123
+ | Método | Comprobante | Emisor | Receptor |
124
+ |---|---|---|---|
125
+ | `create_invoice_c` | Factura C (11) | Monotributista | Cualquiera |
126
+ | `create_invoice_a` | Factura A (1) | Responsable inscripto | Responsable inscripto o monotributista |
127
+ | `create_invoice_b` | Factura B (6) | Responsable inscripto | Consumidor final, exento, no categorizado, etc. |
128
+ | `create_credit_note_a` / `create_debit_note_a` | Nota de Crédito A (3) / Nota de Débito A (2) | Responsable inscripto | Como la Factura A |
129
+ | `create_credit_note_b` / `create_debit_note_b` | Nota de Crédito B (8) / Nota de Débito B (7) | Responsable inscripto | Como la Factura B |
130
+ | `create_credit_note_c` / `create_debit_note_c` | Nota de Crédito C (13) / Nota de Débito C (12) | Monotributista | Como la Factura C |
131
+
132
+ ### Factura C (monotributistas)
133
+
134
+ ```python
135
+ from datetime import date
136
+ from arca_client import WSAA, WSFE, Concepto, CondicionIVAReceptor, DocTipo, Environment
137
+
138
+ wsaa = WSAA("cert.crt", "private.key", Environment.HOMOLOGACION)
139
+ wsfe = WSFE(wsaa) # la CUIT emisora se toma del certificado (o pasala con cuit=...)
140
+
141
+ # Consumidor final sin identificar, venta de productos
142
+ factura = wsfe.create_invoice_c(punto_venta=1, importe_total="1500.00")
143
+ print(factura.numero, factura.cae, factura.cae_vencimiento)
144
+
145
+ # Servicios a un receptor identificado
146
+ wsfe.create_invoice_c(
147
+ punto_venta=1,
148
+ importe_total="25000.00",
149
+ concepto=Concepto.SERVICIOS,
150
+ doc_tipo=DocTipo.CUIT,
151
+ doc_nro=30712345671,
152
+ condicion_iva_receptor=CondicionIVAReceptor.IVA_RESPONSABLE_INSCRIPTO,
153
+ servicio_desde=date(2026, 10, 1),
154
+ servicio_hasta=date(2026, 10, 31),
155
+ )
156
+ ```
157
+
158
+ Para leer la CUIT del certificado sin crear un `WSFE` (por ejemplo, para el PDF), `cuit_from_certificate` acepta las mismas entradas que `WSAA`: una ruta, el PEM en bytes o un `x509.Certificate`. Devuelve `None` si el certificado no trae CUIT.
159
+
160
+ ```python
161
+ from arca_client import cuit_from_certificate
162
+
163
+ cuit_from_certificate("certs/homologacion/miapp.crt") # "20123456786"
164
+ ```
165
+
166
+ `create_invoice_c`:
167
+
168
+ - Consulta el último número autorizado (`FECompUltimoAutorizado`) y usa el siguiente.
169
+ - Si otro proceso usa ese número antes (rechazo 10016 y el último autorizado avanzó), reintenta con el siguiente, hasta `numbering_attempts` veces (3 por defecto). Es seguro: un comprobante rechazado no se emite ni consume número. Si el último no avanzó, el 10016 es por otra causa (por ejemplo, una fecha anterior a la del último comprobante) y se informa sin reintentar.
170
+ - Arma el comprobante C: `ImpNeto` = `ImpTotal`, IVA, exento y no gravado en 0, sin array de IVA, moneda `PES`.
171
+ - Informa la condición frente al IVA del receptor (obligatoria, RG 5616). Para consumidor final se completa sola; con un receptor identificado hay que indicarla.
172
+ - Valida localmente lo básico: importe positivo con hasta 2 decimales (como `Decimal`, `int` o `str`, nunca `float`), documento y período de servicio.
173
+ - Devuelve un `ComprobanteAutorizado` con el CAE y su vencimiento. Si el comprobante se rechaza, lanza `WSFEError` con los `errores` y `observaciones` de ARCA (los códigos están en `e.codes`).
174
+
175
+ Ejemplo de línea de comandos:
176
+
177
+ ```bash
178
+ ARCA_CERT=cert.crt ARCA_KEY=private.key uv run python examples/factura_c.py 1500.00 1
179
+ ```
180
+
181
+ ### Responsable inscripto: Factura A y B
182
+
183
+ Los importes se pasan como **neto por alícuota**; la librería calcula el IVA de cada alícuota sobre su base (redondeo half even, criterio de ARCA) y el total.
184
+
185
+ ```python
186
+ from arca_client import Alicuota, CondicionIVAReceptor, TipoTributo, Tributo
187
+
188
+ factura = wsfe.create_invoice_a(
189
+ punto_venta=1,
190
+ doc_nro=30712345671, # CUIT del receptor (obligatoria en la A)
191
+ condicion_iva_receptor=CondicionIVAReceptor.IVA_RESPONSABLE_INSCRIPTO,
192
+ neto={Alicuota.IVA_21: "1000.00", Alicuota.IVA_10_5: "500.00"},
193
+ exento="100.00", # opcionales: exento= y no_gravado=
194
+ tributos=[Tributo(TipoTributo.PERCEPCION_IIBB, base_imponible="1500.00", alicuota="3")],
195
+ )
196
+ factura.importe_neto, factura.importe_iva, factura.importe_tributos, factura.importe_total
197
+ # Decimal('1500.00'), Decimal('262.50'), Decimal('45.00'), Decimal('1907.50')
198
+ factura.iva # [IvaAlicuota(IVA_10_5, 500.00, 52.50), IvaAlicuota(IVA_21, 1000.00, 210.00)]
199
+
200
+ # Factura B a consumidor final sin identificar (también admite receptor identificado)
201
+ wsfe.create_invoice_b(punto_venta=1, neto={Alicuota.IVA_21: "1000.00"})
202
+ ```
203
+
204
+ - `ImpTotal = neto + no gravado + exento + IVA + tributos`. Si hay neto gravado se envía el array `Iva`; si todo es exento o no gravado, no.
205
+ - La B también informa el IVA discriminado a ARCA (así lo pide WSFEv1); lo que cambia es el PDF, que muestra los precios con IVA incluido.
206
+ - La condición frente al IVA del receptor se valida localmente contra la clase: la A es para responsables inscriptos y monotributistas (RG 5003), la B para el resto (`CONDICIONES_A` / `CONDICIONES_B`). La CUIT/CUIL del receptor se valida con su dígito verificador.
207
+ - Alícuotas: `IVA_0`, `IVA_2_5`, `IVA_5`, `IVA_10_5`, `IVA_21`, `IVA_27`.
208
+ - `Tributo(tipo, base_imponible, alicuota, importe=None, descripcion="")`: si no se indica `importe` se calcula `base × alícuota / 100`. Sirve para percepciones de IVA o IIBB, impuestos internos, etc., y también se puede usar en la Factura C (`create_invoice_c(..., tributos=[...])`, donde el neto pasa a ser el total menos los tributos).
209
+ - El tope a partir del cual hay que identificar al consumidor final en la B lo valida ARCA.
210
+ - La recuperación ante cortes, el reintento de numeración y `get_voucher` funcionan igual para todos los tipos; `get_voucher` devuelve también el detalle de IVA, tributos y comprobantes asociados.
211
+
212
+ Ejemplo de línea de comandos (Factura A al 21 %):
213
+
214
+ ```bash
215
+ ARCA_CERT=cert.crt ARCA_KEY=private.key uv run python examples/factura_a.py 30712345671 1000.00 1
216
+ ```
217
+
218
+ ### Notas de crédito y débito
219
+
220
+ Cada nota lleva los mismos argumentos que la factura de su clase (`create_invoice_a`, `create_invoice_b` o `create_invoice_c`), más el comprobante al que se refiere (`asociado=`) o, si abarca varios, el período (`periodo=(desde, hasta)`). Hay que indicar uno de los dos, y el asociado tiene que ser de la misma letra que la nota: una nota B solo puede referirse a un comprobante B.
221
+
222
+ ```python
223
+ nota = wsfe.create_credit_note_a(
224
+ punto_venta=1,
225
+ asociado=factura, # un ComprobanteAutorizado o ComprobanteAsociado(tipo, punto_venta, numero)
226
+ doc_nro=30712345671,
227
+ condicion_iva_receptor=CondicionIVAReceptor.IVA_RESPONSABLE_INSCRIPTO,
228
+ neto={Alicuota.IVA_21: "100.00"},
229
+ )
230
+
231
+ # B: neto por alícuota; consumidor final sin identificar por defecto, como la Factura B
232
+ wsfe.create_credit_note_b(punto_venta=1, asociado=factura_b, neto={Alicuota.IVA_21: "1000.00"})
233
+
234
+ # C: importe total, como la Factura C
235
+ wsfe.create_credit_note_c(1, "500.00", asociado=factura_c)
236
+ wsfe.create_debit_note_c(1, "100.00", periodo=(date(2026, 9, 1), date(2026, 9, 30)))
237
+ ```
238
+
239
+ ### Consultar parámetros de ARCA
240
+
241
+ Consultas seguras (no emiten nada), útiles para verificar códigos:
242
+
243
+ ```python
244
+ wsfe.condiciones_iva_receptor(
245
+ "A"
246
+ ) # {1: 'IVA Responsable Inscripto', 6: 'Responsable Monotributo', ...}
247
+ wsfe.tipos_iva() # {3: '0%', 4: '10.5%', 5: '21%', ...}
248
+ wsfe.tipos_tributo() # {1: 'Impuestos nacionales', ..., 7: 'Percepción de IIBB', ...}
249
+ ```
250
+
251
+ ### Cortes de comunicación
252
+
253
+ Si se corta la conexión después de enviar `FECAESolicitar` (timeout, error de red, respuesta ilegible), no se sabe si ARCA autorizó el comprobante. Reintentar con `create_invoice_c` calcularía el **número siguiente** y, si el primero se había autorizado, emitiría una factura duplicada.
254
+
255
+ Por eso, ante un corte, `create_invoice_c` consulta ese mismo número con `FECompConsultar`, como recomienda el manual de ARCA ("Operatoria con errores de comunicación"):
256
+
257
+ | Resultado de la consulta | Qué pasa |
258
+ |---|---|
259
+ | El comprobante existe con los datos enviados | Devuelve el `ComprobanteAutorizado` con su CAE, como si no hubiera habido corte. |
260
+ | ARCA no lo registra (error 602) | Lanza `WSFEConnectionError` con `safe_to_retry=True`: se puede reintentar. |
261
+ | La consulta también falla (3 intentos) | Lanza `WSFEConnectionError` con `safe_to_retry=False`: **no reintentar**. Verificá con `wsfe.get_voucher(e.punto_venta, e.tipo, e.numero)` cuando vuelva la conexión. |
262
+ | Existe pero con otros datos | Lanza `WSFEConnectionError` con `safe_to_retry=False` (probablemente otro proceso usó el número). |
263
+
264
+ ```python
265
+ from arca_client import WSFEConnectionError
266
+
267
+ try:
268
+ factura = wsfe.create_invoice_c(punto_venta=1, importe_total="1500.00")
269
+ except WSFEConnectionError as e:
270
+ if e.safe_to_retry:
271
+ factura = wsfe.create_invoice_c(punto_venta=1, importe_total="1500.00")
272
+ else:
273
+ # Guardar e.punto_venta / e.numero y verificar más tarde con get_voucher
274
+ raise
275
+ ```
276
+
277
+ Lo mismo vale para `create_invoice_a`, `create_invoice_b` y las notas de crédito/débito.
278
+
279
+ `wsfe.get_voucher(punto_venta, tipo, numero)` también sirve por sí solo para consultar cualquier comprobante emitido; devuelve `None` si no existe. Todas las llamadas a ARCA tienen timeout (30 s por defecto, configurable con `WSFE(..., timeout=...)`).
280
+
281
+ ### PDF con código QR
282
+
283
+ ARCA no ofrece un servicio para generar el PDF de las facturas emitidas por web service: lo tiene que generar el emisor. `render_invoice_pdf` arma la representación impresa de cualquiera de los comprobantes anteriores con el código QR obligatorio (RG 4892). Requiere el extra `pdf` (`fpdf2` y `segno`).
284
+
285
+ ```python
286
+ from datetime import date
287
+ from pathlib import Path
288
+ from arca_client import Emisor, Item, Receptor, render_invoice_pdf
289
+
290
+ emisor = Emisor(
291
+ razon_social="Juan Pérez",
292
+ domicilio="Av. Corrientes 1234, CABA",
293
+ inicio_actividades=date(2020, 3, 1),
294
+ ingresos_brutos="20-12345678-6",
295
+ )
296
+ items = [
297
+ Item("Diseño de logotipo", "18000.00", codigo="D01"),
298
+ Item("Horas de soporte", "2550.00", cantidad=3, unidad="horas", bonificacion="10"),
299
+ ]
300
+ pdf = render_invoice_pdf(factura, emisor, items, Receptor("Empresa S.A.", "San Martín 500"))
301
+ Path("factura.pdf").write_bytes(pdf)
302
+ ```
303
+
304
+ - La suma de los ítems tiene que coincidir con el importe autorizado; si no, se lanza `ValueError`. Para no calcularlo a mano, autorizá con `importe_total=sum(i.subtotal for i in items)`.
305
+ - **A y B**: cada ítem gravado lleva su alícuota (`Item("Diseño", "1000.00", alicuota=Alicuota.IVA_21)`) y su precio va **sin IVA**. El neto de los ítems por alícuota tiene que coincidir con lo autorizado; para no calcularlo dos veces, autorizá con `neto=neto_por_alicuota(items)`. Los ítems sin alícuota cuentan como no gravados o exentos.
306
+ - **A**: la tabla agrega las columnas "% IVA" y "Subtotal c/IVA", y los totales discriminan el neto gravado, el IVA de cada alícuota, el exento y el no gravado.
307
+ - **B**: los precios se muestran con IVA incluido y, si el receptor es consumidor final, se imprime el "Régimen de Transparencia Fiscal al Consumidor" de la Ley 27.743 (IVA contenido y otros impuestos nacionales indirectos). Como el IVA de cada ítem se redondea por separado, la suma de los ítems con IVA puede diferir en centavos del total autorizado, que es el que se imprime en los totales.
308
+ - **Notas de crédito/débito**: el título cambia a "NOTA DE CRÉDITO" / "NOTA DE DÉBITO" y se imprime el comprobante o período asociado.
309
+ - Si hay tributos, se agrega una tabla "Otros tributos".
310
+ - La condición frente al IVA del emisor sale de la letra ("Responsable Monotributo" en C, "IVA Responsable Inscripto" en A y B); se puede fijar con `Emisor(..., condicion_iva="...")`.
311
+ - `bonificacion` es un porcentaje de descuento (0 a 100) por ítem. Si algún ítem tiene descuento, la tabla agrega las columnas "% Bonif" e "Imp. Bonif.", y el subtotal del ítem ya lo tiene aplicado.
312
+ - Si la condición frente al IVA del receptor es consumidor final, se imprime la leyenda "A CONSUMIDOR FINAL" (RG 1415, Anexo II).
313
+ - El documento y la condición frente al IVA del receptor, el período de servicio y el CAE salen del `ComprobanteAutorizado`.
314
+ - `copias=("ORIGINAL", "DUPLICADO")` genera una copia por cada etiqueta. Si los ítems no entran en una hoja, se pagina y cada hoja lleva el número de comprobante y "Pág. X/Y".
315
+ - `qr_url(factura)` devuelve solo el texto del QR (`https://www.arca.gob.ar/fe/qr/?p=...`), por si querés armar tu propio diseño.
316
+ - El PDF usa Liberation Sans, incluida en la librería (licencia SIL OFL 1.1, en `arca_client/wsfe/fonts/OFL.txt`). Tiene las mismas métricas que Helvetica/Arial y cubre, además del español, otros alfabetos latinos, el griego, el cirílico y símbolos como € o ™. Los caracteres que no tiene (emojis, chino, japonés, etc.) se reemplazan por `?`.
317
+
318
+ Para facturar se necesita autorizar el certificado también al servicio `wsfe` (en homologación, desde WSASS). En producción, además, hace falta un punto de venta habilitado para web services.
319
+
320
+ ## Verificar comprobantes de proveedores (WSCDC)
321
+
322
+ `WSCDC` consulta a ARCA si un comprobante recibido existe y está autorizado con los datos que figuran en él (`ComprobanteConstatar`). Sirve para controlar las facturas de proveedores antes de registrarlas o pagarlas. No emite nada.
323
+
324
+ Lo más simple es verificar con el texto del código QR de la factura, que trae todos los datos necesarios:
325
+
326
+ ```python
327
+ from arca_client import WSCDC
328
+
329
+ wscdc = WSCDC(wsaa) # la CUIT que consulta se toma del certificado
330
+ resultado = wscdc.verify_qr("https://www.arca.gob.ar/fe/qr/?p=eyJ2ZXIiOjEs...")
331
+ if not resultado.aprobada:
332
+ print("No coincide con ARCA:", resultado.observaciones, resultado.errores)
333
+ ```
334
+
335
+ O con los datos impresos, si no hay QR:
336
+
337
+ ```python
338
+ from datetime import date
339
+ from arca_client import CbteTipo, DocTipo
340
+
341
+ resultado = wscdc.verify_voucher(
342
+ cuit_emisor=30712345671,
343
+ punto_venta=3,
344
+ tipo=CbteTipo.FACTURA_A, # o cualquier código de ARCA, ej. 201 (FCE A)
345
+ numero=42,
346
+ fecha=date(2026, 10, 19),
347
+ importe_total="1210.00",
348
+ codigo_autorizacion="76123456789012", # el CAE
349
+ doc_tipo_receptor=DocTipo.CUIT, # opcional: para facturas a tu nombre, tu CUIT
350
+ doc_nro_receptor=20123456786,
351
+ )
352
+ ```
353
+
354
+ - `resultado.aprobada` es `True` solo si ARCA devuelve `Resultado = "A"` sin errores. Si no coincide algún dato, es `False` y `observaciones`/`errores` dicen por qué (ej. `100`: el CAE no existe, `105`: el número no corresponde al CAE, `107`: la fecha no corresponde, `110`: el importe no corresponde).
355
+ - **ARCA tolera alrededor de $1 de diferencia en el importe**: en homologación aprobó diferencias de ±1,01 y rechazó ±1,50. Tampoco devuelve el importe registrado. Si necesitás un control exacto, compará además el importe con lo que tenés registrado.
356
+ - Si ARCA no llega a procesar la consulta (por ejemplo, falta de autorización), se lanza `WSCDCError`. Eso no dice nada sobre el comprobante: no se pudo verificar. Si responde algo que no se puede interpretar, se lanza `RespuestaNoReconocida` (subclase de `ArcaError`): la respuesta completa queda en su atributo `respuesta` y no va en el mensaje, porque puede tener CUIT y CAE.
357
+ - `modo=ModoAutorizacion.CAEA` (o `CAI`) para comprobantes autorizados por otra vía. `verify_qr` lo toma del QR.
358
+ - **Receptor en `verify_qr`**: si pasás `doc_tipo_receptor` y `doc_nro_receptor`, se consulta con esos datos. Es la forma de exigir que la factura esté a tu nombre: si el QR trae otro receptor, se lanza `ValueError` y no se consulta a ARCA (si no, un proveedor podría presentar una factura auténtica emitida a otra persona). Si no los pasás, se usa el receptor del QR, y si el QR tampoco lo trae (consumidor final), se consulta sin receptor. CUIT (80), CUIL (86) y CDI (87) cuentan como el mismo documento si el número coincide. Si el QR trae el mismo documento que indicaste, a ARCA se envían el tipo y el número del QR, que son los registrados en el comprobante (por ejemplo, CUIL si la factura se emitió a tu CUIL aunque pases `DocTipo.CUIT`); `resultado.comprobante` muestra lo que se envió. Si el QR no trae receptor, se envía lo que indicaste.
359
+
360
+ ```python
361
+ resultado = wscdc.verify_qr(texto_qr, doc_tipo_receptor=DocTipo.CUIT, doc_nro_receptor=30712345671)
362
+ ```
363
+
364
+ - `resultado.comprobante` tiene los datos con los que se consultó (CUIT del emisor, punto de venta, tipo, número, fecha, importe, código de autorización y receptor). Con `verify_qr` salen del QR: comparalos con la factura impresa o con tus registros.
365
+ - `parse_qr_url(texto)` devuelve el JSON del QR (con el importe como `Decimal`), por si querés guardar los datos. Como el QR lo arma quien emite, se valida: los datos numéricos tienen que ser enteros con una cantidad acotada de dígitos y el importe, un número de hasta 13 enteros y 2 decimales sin exponente. Si no, lanza `ValueError`.
366
+ - Consultas: `tipos_comprobante()`, `tipos_documento()`, `modalidades()` y `estado()` (estado de los servidores; no necesita ticket).
367
+
368
+ Hay que autorizar el certificado al servicio `wscdc` (en homologación, desde WSASS; en producción, desde el *Administrador de Relaciones de Clave Fiscal*).
369
+
370
+ ```bash
371
+ ARCA_CERT=cert.crt ARCA_KEY=private.key \
372
+ uv run python examples/verificar_comprobante.py "https://www.arca.gob.ar/fe/qr/?p=..."
373
+ ```
374
+
375
+ ## Datos de un contribuyente (WSPCI, Constancia de Inscripción)
376
+
377
+ `WSPCI` obtiene de ARCA los datos de una CUIT para completar la ficha de un cliente: nombre o razón social, tipo de persona, domicilio fiscal e impuestos. Es solo lectura.
378
+
379
+ ```python
380
+ from arca_client import WSPCI, PersonaNoEncontrada
381
+
382
+ wspci = WSPCI(wsaa) # la CUIT que consulta se toma del certificado (o pasala con cuit=...)
383
+
384
+ try:
385
+ c = wspci.get_constancia("30-71234567-1")
386
+ except PersonaNoEncontrada:
387
+ ... # ARCA no registra esa CUIT
388
+
389
+ c.denominacion # la razón social o "APELLIDO NOMBRE"
390
+ c.tipo_persona # TipoPersona.FISICA o TipoPersona.JURIDICA
391
+ c.activa # True solo si estado_clave == "ACTIVO"
392
+ str(c.domicilio_fiscal) # "AV FICTICIA 123, VILLA INVENTADA, 1234, BUENOS AIRES"
393
+ c.condicion_iva # CondicionIVAReceptor o None
394
+ c.impuestos, c.impuestos_monotributo, c.categoria_monotributo
395
+
396
+ wspci.dummy() # {"appserver": "OK", "authserver": "OK", "dbserver": "OK"}, no pide ticket
397
+ ```
398
+
399
+ ### Condición frente al IVA
400
+
401
+ `condicion_iva` **es una deducción de esta librería, no un dato de ARCA**: el manual no define una tabla de condición frente al IVA. Por eso nunca adivina, y hay que estar preparado para recibir `None`:
402
+
403
+ | Situación | `condicion_iva` |
404
+ |---|---|
405
+ | Impuesto 20 vigente en `impuestos_monotributo`, sin 30 | `RESPONSABLE_MONOTRIBUTO` (6) |
406
+ | Impuesto 30 vigente en `impuestos`, sin 20 | `IVA_RESPONSABLE_INSCRIPTO` (1) |
407
+ | Cualquier error en `errores_constancia`, `errores_regimen_general` o `errores_monotributo` | `None` |
408
+ | Impuestos 20 y 30 a la vez, o ninguno de los dos | `None` |
409
+ | Categoría de monotributo social | `None` |
410
+
411
+ Un impuesto está vigente si su estado es `"AC"` (el valor que devuelve homologación), `"ACTIVO"` o si ARCA no informa estado; cualquier otro, como `"NA"`, no cuenta. Sin impuestos **no** se deduce "consumidor final": esa decisión queda en la aplicación.
412
+
413
+ Tampoco se devuelven `IVA_SUJETO_EXENTO` ni `MONOTRIBUTISTA_SOCIAL`. Si la categoría de monotributo contiene `SOCIAL` (ej. `A MONOTRIBUTO SOCIAL AGROP.`), `condicion_iva` es `None`: esa forma de reconocerlo sale de un único caso de homologación, y `categoria_monotributo` queda disponible para que la aplicación decida.
414
+
415
+ ### Errores
416
+
417
+ Que falte un dato por un error no significa que no exista:
418
+
419
+ | Respuesta de ARCA | Resultado |
420
+ |---|---|
421
+ | CUIT inválida (dígito verificador) | `ValueError`, sin pedir ticket ni llamar a ARCA |
422
+ | `No existe persona con ese Id` (SOAP Fault o `errorConstancia` sin datos generales) | `PersonaNoEncontrada` |
423
+ | Otro `errorConstancia` (CUIT limitada, inactiva, etc.) | `Constancia` con `errores_constancia` y el nombre y la CUIT que trae el error |
424
+ | `errorRegimenGeneral` o `errorMonotributo` | `Constancia` con `errores_regimen_general` o `errores_monotributo` |
425
+ | Otro SOAP Fault | `WSPCIError`, con los textos en `errores` |
426
+ | Respuesta sin datos generales ni errores | `ArcaError` |
427
+
428
+ `PersonaNoEncontrada` hereda de `WSPCIError` y esta de `ArcaError`. `WSAAError` y los errores de conexión se propagan sin envolver.
429
+
430
+ ### Autorizar el certificado
431
+
432
+ Cada servicio se autoriza aparte. Un certificado que ya factura con `wsfe` no queda habilitado para este: ARCA responde `coe.notAuthorized` hasta autorizarlo.
433
+
434
+ - **Homologación**: en WSASS, «Crear autorización a servicio», servicio `ws_sr_constancia_inscripcion`.
435
+ - **Producción**: en el *Administrador de Relaciones de Clave Fiscal*, «Nueva Relación», *ARCA › Web Services*, y elegir el servicio de la Constancia de Inscripción (el nombre con que aparece en esa lista todavía no se verificó).
436
+
437
+ ### Errores
438
+
439
+ Los SoapFault del WSAA se convierten en `WSAAError`, con `code` (ej. `cms.cert.expired`) y `retryable`. Según la especificación, solo se puede reintentar ante `wsaa.*` y `wsn.unavailable`.
440
+
441
+ ## Limitaciones conocidas
442
+
443
+ - **Factura de Crédito Electrónica MiPyME (FCE, tipos 201 a 213) no soportada.** Una MiPyME que le vende a una gran empresa por encima del monto mínimo vigente está obligada a emitir FCE, y ARCA rechaza la Factura A, B o C común. Para soportarla faltan:
444
+ - los tipos 201/202/203 (A), 206/207/208 (B) y 211/212/213 (C);
445
+ - el envío de `Opcionales`: CBU (2101) o alias (2102) del emisor y la modalidad de transferencia (27: `SCA` o `ADC`);
446
+ - el vencimiento del pago obligatorio también para productos;
447
+ - en las notas, el comprobante asociado con CUIT y fecha y la anulación (22: `S`/`N`);
448
+ - las leyendas propias en el PDF.
449
+
450
+ La consulta de si un receptor está obligado y la aceptación o el rechazo de la FCE son de otro web service (WSFECRED), que tampoco está implementado.
451
+ - **Solo pesos.** `MonId` y `MonCotiz` van fijos en `PES` y 1. Para emitir en moneda extranjera faltan la moneda y la cotización como parámetros, `CanMisMonExt` (RG 5616), las consultas `FEParamGetTiposMonedas` y `FEParamGetCotizacion` y el PDF en otra moneda.
452
+ - **Otros comprobantes no soportados**: A con leyenda "Operación sujeta a retención" (51 a 54: factura, notas de débito y crédito, y recibo), recibos (4, 9 y 15) y otros tipos de nicho de `FEParamGetTiposCbte` (notas de venta al contado, liquidaciones, cuentas de venta y líquido producto, etc.).
453
+ - **PDF sin emojis ni escrituras asiáticas.** Liberation Sans no las cubre: esos caracteres se reemplazan por `?`.
454
+ - **WSPCI probado solo en homologación y con pocos casos.** Falta probarlo en producción. Tampoco se confirmó si el impuesto 32 es IVA exento ni cómo responde ARCA con una CUIL. Otros estados de impuesto, además de `AC` y `NA`, se tratan como no vigentes. Se puede probar con `tests/integration/test_wspci.py`.
455
+
456
+ ## Desarrollo
457
+
458
+ ```bash
459
+ uv run pytest
460
+ uv run ruff check .
461
+ ```
462
+
463
+ ## Licencia
464
+
465
+ MIT