YOSOYYONOSOYOTRO commited on
Commit
4223796
Β·
verified Β·
1 Parent(s): 83bdb4a

Upload folder using huggingface_hub (part 2)

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +45 -0
  3. README.md +17 -10
  4. app.py +33 -0
  5. app/pyafipws/tests/wscdc.py +88 -0
  6. app/pyafipws/tests/wsfev1.py +212 -0
  7. app/pyafipws/tests/wslpg.py +544 -0
  8. app/pyafipws/tests/wslpg_cert_autorizar_resp.xml +31 -0
  9. app/pyafipws/tests/wsmtx.py +246 -0
  10. app/pyafipws/trazafito.py +591 -0
  11. app/pyafipws/trazamed.py +1041 -0
  12. app/pyafipws/trazaprodmed.py +555 -0
  13. app/pyafipws/trazarenpre.py +366 -0
  14. app/pyafipws/trazavet.py +598 -0
  15. app/pyafipws/typelib/pyafipws.idl +48 -0
  16. app/pyafipws/typelib/trazamed.idl +80 -0
  17. app/pyafipws/typelib/trazamed.tlb +0 -0
  18. app/pyafipws/typelib/wsaa.idl +106 -0
  19. app/pyafipws/typelib/wsaa.tlb +0 -0
  20. app/pyafipws/typelib/wsfev1.idl +225 -0
  21. app/pyafipws/typelib/wsfev1.tlb +0 -0
  22. app/pyafipws/utils.py +936 -0
  23. app/pyafipws/venv.bat +29 -0
  24. app/pyafipws/wdigdepfiel.py +186 -0
  25. app/pyafipws/ws_sr_padron.py +380 -0
  26. app/pyafipws/wsaa.py +525 -0
  27. app/pyafipws/wsbfev1.py +659 -0
  28. app/pyafipws/wscdc.py +398 -0
  29. app/pyafipws/wscoc.py +1032 -0
  30. app/pyafipws/wsct.py +820 -0
  31. app/pyafipws/wsctg.py +1198 -0
  32. app/pyafipws/wsfev1.py +1349 -0
  33. app/pyafipws/wsfexv1.py +799 -0
  34. app/pyafipws/wslpg.py +0 -0
  35. app/pyafipws/wslpg_datos.py +113 -0
  36. app/pyafipws/wslsp.py +1210 -0
  37. app/pyafipws/wsltv.py +1069 -0
  38. app/pyafipws/wslum.py +923 -0
  39. app/pyafipws/wsmtx.py +1243 -0
  40. app/pyafipws/wsremcarne.py +691 -0
  41. app/reports/__init__.py +1 -0
  42. app/reports/builders.py +682 -0
  43. app/reports/fallback.py +111 -0
  44. app/reports/kyc_service.py +156 -0
  45. app/reports/models.py +42 -0
  46. app/reports/orchestrator.py +244 -0
  47. app/reports/registry.py +104 -0
  48. app/reports/router.py +763 -0
  49. app/reports/schema_validators.py +203 -0
  50. app/reports/schemas.py +629 -0
.gitattributes CHANGED
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  app/cache/ddjj_pep.csv filter=lfs diff=lfs merge=lfs -text
37
  app/pyafipws/ejemplos/wsfe/delphi/Project1.exe filter=lfs diff=lfs merge=lfs -text
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  app/cache/ddjj_pep.csv filter=lfs diff=lfs merge=lfs -text
37
  app/pyafipws/ejemplos/wsfe/delphi/Project1.exe filter=lfs diff=lfs merge=lfs -text
38
+ app/utils/logo.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies for:
6
+ # - PostgreSQL (libpq-dev)
7
+ # - ddddocr OCR (libglib2.0-0 libsm6 libxext6 libxrender-dev libgl1-mesa-glx)
8
+ # - Playwright Chromium (via --with-deps)
9
+ # - curl_cffi build tools (gcc, curl)
10
+ RUN apt-get update && apt-get install -y \
11
+ gcc \
12
+ g++ \
13
+ libpq-dev \
14
+ curl \
15
+ wget \
16
+ gnupg \
17
+ libglib2.0-0 \
18
+ libsm6 \
19
+ libxext6 \
20
+ libxrender1 \
21
+ libgl1 \
22
+ libgomp1 \
23
+ libssl-dev \
24
+ libsasl2-dev \
25
+ swig \
26
+ && rm -rf /var/lib/apt/lists/*
27
+
28
+ # Install Python dependencies
29
+ COPY requirements.txt .
30
+ RUN pip install --no-cache-dir -r requirements.txt
31
+
32
+ # Configure Playwright browser path to be accessible by any user ID
33
+ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
34
+ RUN mkdir -p /ms-playwright && chmod -R 777 /ms-playwright
35
+ RUN playwright install chromium --with-deps
36
+
37
+ # Copy application code
38
+ COPY . .
39
+
40
+ # Hugging Face Spaces uses port 7860 by default
41
+ EXPOSE 7860
42
+
43
+ # Start FastAPI on port 7860 (or $PORT if overridden)
44
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860} --workers 2"]
45
+
README.md CHANGED
@@ -1,15 +1,22 @@
1
  ---
2
- title: Crowdata
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
- app_file: app.py
10
  pinned: false
11
  license: mit
12
- short_description: InformaciΓ³n pΓΊblica a tu alcance
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: CrowData API
3
+ emoji: πŸ”Ž
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
 
 
 
7
  pinned: false
8
  license: mit
9
+ app_port: 7860
10
  ---
11
 
12
+ # CrowData API
13
+
14
+ API backend de CrowData β€” Plataforma de inteligencia de datos personales y empresariales en Argentina.
15
+
16
+ ## Endpoints
17
+
18
+ - `GET /api/health` β€” Health check
19
+ - `POST /api/auth/jwt/login` β€” AutenticaciΓ³n JWT
20
+ - `GET /api/reports/persona/{cuit}` β€” Informe persona
21
+ - `GET /api/reports/empresa/{cuit}` β€” Informe empresa
22
+ - `GET /api/docs` β€” Swagger UI
app.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import logging
5
+
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger("hf_app")
8
+
9
+ # Ensure Playwright browser binaries are installed
10
+ try:
11
+ logger.info("Verificando/instalando Playwright Chromium...")
12
+ subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=False)
13
+ except Exception as e:
14
+ logger.warning(f"Advertencia al instalar Playwright Chromium: {e}")
15
+
16
+ import gradio as gr
17
+ from app.main import app as fastapi_app
18
+
19
+ # Crear interfaz Gradio de aterrizaje para Hugging Face Spaces
20
+ with gr.Blocks(title="CrowData API Backend") as demo:
21
+ gr.Markdown("# πŸ¦… CrowData API Backend")
22
+ gr.Markdown("El backend de CrowData (FastAPI) estΓ‘ ejecutΓ‘ndose correctamente en Hugging Face Spaces con 16 GB RAM.")
23
+ gr.Markdown("### πŸ”— Accesos Directos:")
24
+ gr.Markdown("- πŸ“„ [DocumentaciΓ³n de la API (Swagger UI)](/api/docs)")
25
+ gr.Markdown("- 🩺 [Health Check](/api/health)")
26
+
27
+ # Montar Gradio en /ui para preservar las rutas principales /api/* en FastAPI
28
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/ui")
29
+
30
+ if __name__ == "__main__":
31
+ import uvicorn
32
+ port = int(os.environ.get("PORT", 7860))
33
+ uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
app/pyafipws/tests/wscdc.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ import pysimplesoap.client
14
+ from pyafipws.wscdc import WSCDC
15
+ from pyafipws.wsaa import WSAA
16
+ from pyafipws import utils
17
+ "Pruebas para el servicio web ConstataciΓ³n de Comprobantes de AFIP"
18
+
19
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
20
+ __copyright__ = "Copyright (C) 2013 Mariano Reingart"
21
+ __license__ = "GPL 3.0"
22
+
23
+
24
+ import unittest
25
+ import sys
26
+ from decimal import Decimal
27
+
28
+ sys.path.append("/home/reingart") # TODO: proper packaging
29
+
30
+
31
+ print(pysimplesoap.client.__version__)
32
+ #assert pysimplesoap.client.__version__ >= "1.08c"
33
+
34
+
35
+ WSDL = "https://wswhomo.afip.gov.ar/WSCDC/service.asmx?WSDL"
36
+ CUIT = 20267565393
37
+ CERT = "/home/reingart/pyafipws/reingart.crt"
38
+ PRIVATEKEY = "/home/reingart/pyafipws/reingart.key"
39
+ CACERT = "/home/reingart/pyafipws/afip_root_desa_ca.crt"
40
+ CACHE = "/home/reingart/pyafipws/cache"
41
+
42
+ # AutenticaciΓ³n:
43
+ wsaa = WSAA()
44
+ tra = wsaa.CreateTRA(service="wscdc")
45
+ cms = wsaa.SignTRA(tra, CERT, PRIVATEKEY)
46
+ wsaa.Conectar()
47
+ wsaa.LoginCMS(cms)
48
+
49
+
50
+ class TestWSCDC(unittest.TestCase):
51
+
52
+ def setUp(self):
53
+ sys.argv.append("--trace") # TODO: use logging
54
+ self.wscdc = wslpg = WSCDC()
55
+ wslpg.LanzarExcepciones = True
56
+ wslpg.Conectar(wsdl=WSDL, cacert=None, cache=CACHE)
57
+ wslpg.Cuit = CUIT
58
+ wslpg.Token = wsaa.Token
59
+ wslpg.Sign = wsaa.Sign
60
+
61
+ def test_constatacion_no(self):
62
+ "Prueba de ConstataciΓ³n de Comprobantes (facturas electrΓ³nicas)"
63
+ wscdc = self.wscdc
64
+ cbte_modo = "CAE"
65
+ cuit_emisor = "20267565393"
66
+ pto_vta = 4002
67
+ cbte_tipo = 1
68
+ cbte_nro = 109
69
+ cbte_fch = "20131227"
70
+ imp_total = "121.0"
71
+ cod_autorizacion = "63523178385550"
72
+ doc_tipo_receptor = 80
73
+ doc_nro_receptor = "30628789661"
74
+ ok = wscdc.ConstatarComprobante(cbte_modo, cuit_emisor, pto_vta, cbte_tipo,
75
+ cbte_nro, cbte_fch, imp_total, cod_autorizacion,
76
+ doc_tipo_receptor, doc_nro_receptor)
77
+ self.assertTrue(ok)
78
+ self.assertEqual(wscdc.Resultado, "R") # Rechazado
79
+ self.assertEqual(wscdc.Obs, "100: El NΒ° de CAI/CAE/CAEA consultado no existe en las bases del organismo.")
80
+ self.assertEqual(wscdc.PuntoVenta, pto_vta)
81
+ self.assertEqual(wscdc.CbteNro, cbte_nro)
82
+ self.assertEqual(wscdc.ImpTotal, imp_total)
83
+ self.assertEqual(wscdc.CAE, cod_autorizacion)
84
+ self.assertEqual(wscdc.EmisionTipo, "CAE")
85
+
86
+
87
+ if __name__ == '__main__':
88
+ unittest.main()
app/pyafipws/tests/wsfev1.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ from pyafipws.wsaa import WSAA
14
+ from pyafipws.wsfev1 import WSFEv1
15
+ "Pruebas para WSFEv1 de AFIP (Factura ElectrοΏ½nica Mercado Interno sin detalle)"
16
+
17
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
18
+ __copyright__ = "Copyright (C) 2010 Mariano Reingart"
19
+ __license__ = "GPL 3.0"
20
+
21
+ import unittest
22
+ import os
23
+ import time
24
+ import sys
25
+ from decimal import Decimal
26
+ import datetime
27
+
28
+ sys.path.append("/home/reingart") # TODO: proper packaging
29
+
30
+
31
+ WSDL = "https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL"
32
+ CUIT = 20267565393
33
+ CERT = "/home/reingart/pyafipws/reingart.crt"
34
+ PRIVATEKEY = "/home/reingart/pyafipws/reingart.key"
35
+ CACERT = "/home/reingart/pyafipws/afip_root_desa_ca.crt"
36
+ CACHE = "/home/reingart/pyafipws/cache"
37
+
38
+ # AutenticaciοΏ½n:
39
+ wsaa = WSAA()
40
+ tra = wsaa.CreateTRA(service="wsfe")
41
+ cms = wsaa.SignTRA(tra, CERT, PRIVATEKEY)
42
+ wsaa.Conectar()
43
+ wsaa.LoginCMS(cms)
44
+
45
+
46
+ class TestFE(unittest.TestCase):
47
+
48
+ def setUp(self):
49
+ sys.argv.append("--trace") # TODO: use logging
50
+ self.wsfev1 = wsfev1 = WSFEv1()
51
+ wsfev1.Cuit = CUIT
52
+ wsfev1.Token = wsaa.Token
53
+ wsfev1.Sign = wsaa.Sign
54
+ wsfev1.Conectar(CACHE, WSDL)
55
+
56
+ def atest_dummy(self):
57
+ print(wsfev1.client.help("dummy"))
58
+ wsfev1.Dummy()
59
+ print("AppServerStatus", wsfev1.AppServerStatus)
60
+ print("DbServerStatus", wsfev1.DbServerStatus)
61
+ print("AuthServerStatus", wsfev1.AuthServerStatus)
62
+
63
+ def test_autorizar_comprobante(self, tipo_cbte=1, cbte_nro=None, servicios=True):
64
+ "Prueba de autorizaciοΏ½n de un comprobante (obtenciοΏ½n de CAE)"
65
+ wsfev1 = self.wsfev1
66
+
67
+ # datos generales del comprobante:
68
+ punto_vta = 4000
69
+ if not cbte_nro:
70
+ # si no me especifοΏ½can nro de comprobante, busco el prοΏ½ximo
71
+ cbte_nro = wsfev1.CompUltimoAutorizado(tipo_cbte, punto_vta)
72
+ cbte_nro = int(cbte_nro) + 1
73
+ fecha = datetime.datetime.now().strftime("%Y%m%d")
74
+ tipo_doc = 80
75
+ nro_doc = "30000000007" # "30500010912" # CUIT BNA
76
+ cbt_desde = cbte_nro
77
+ cbt_hasta = cbt_desde
78
+ imp_total = "122.00"
79
+ imp_tot_conc = "0.00"
80
+ imp_neto = "100.00"
81
+ imp_trib = "1.00"
82
+ imp_op_ex = "0.00"
83
+ imp_iva = "21.00"
84
+ fecha_cbte = fecha
85
+ # Fechas del perοΏ½odo del servicio facturado (solo si concepto = 1?)
86
+ if servicios:
87
+ concepto = 3
88
+ fecha_venc_pago = fecha
89
+ fecha_serv_desde = fecha
90
+ fecha_serv_hasta = fecha
91
+ else:
92
+ concepto = 1
93
+ fecha_venc_pago = fecha_serv_desde = fecha_serv_hasta = None
94
+ moneda_id = 'PES'
95
+ moneda_ctz = '1.000'
96
+ obs = "Observaciones Comerciales, libre"
97
+
98
+ wsfev1.CrearFactura(concepto, tipo_doc, nro_doc, tipo_cbte, punto_vta,
99
+ cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto,
100
+ imp_iva, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
101
+ fecha_serv_desde, fecha_serv_hasta, # --
102
+ moneda_id, moneda_ctz)
103
+
104
+ # agrego un comprobante asociado (solo notas de crοΏ½dito / dοΏ½bito)
105
+ if tipo_cbte in (2, 3):
106
+ tipo = 1
107
+ pv = 2
108
+ nro = 1234
109
+ wsfev1.AgregarCmpAsoc(tipo, pv, nro)
110
+
111
+ # agrego otros tributos:
112
+ tributo_id = 99
113
+ desc = 'Impuesto Municipal Matanza'
114
+ base_imp = "100.00"
115
+ alic = "1.00"
116
+ importe = "1.00"
117
+ wsfev1.AgregarTributo(tributo_id, desc, base_imp, alic, importe)
118
+
119
+ # agrego el subtotal por tasa de IVA:
120
+ iva_id = 5 # 21%
121
+ base_im = 100
122
+ importe = 21
123
+ wsfev1.AgregarIva(iva_id, base_imp, importe)
124
+
125
+ # llamo al websevice para obtener el CAE:
126
+ wsfev1.CAESolicitar()
127
+
128
+ self.assertEqual(wsfev1.Resultado, "A") # Aprobado!
129
+ self.assertIsInstance(wsfev1.CAE, str)
130
+ self.assertEqual(len(wsfev1.CAE), len("63363178822329"))
131
+ self.assertEqual(len(wsfev1.Vencimiento), len("20130907"))
132
+ wsfev1.AnalizarXml("XmlResponse")
133
+ # observaciοΏ½n "... no se encuentra registrado en los padrones de AFIP.":
134
+ self.assertEqual(wsfev1.ObtenerTagXml('Obs', 0, 'Code'), '10017')
135
+
136
+ def test_consulta(self):
137
+ "Prueba de obtener los datos de un comprobante autorizado"
138
+ wsfev1 = self.wsfev1
139
+ # autorizo un comprobante:
140
+ tipo_cbte = 1
141
+ self.test_autorizar_comprobante(tipo_cbte)
142
+ # obtengo datos para comprobar
143
+ cae = wsfev1.CAE
144
+ wsfev1.AnalizarXml("XmlRequest")
145
+ imp_total = float(wsfev1.ObtenerTagXml('ImpTotal'))
146
+ concepto = int(wsfev1.ObtenerTagXml('Concepto'))
147
+ punto_vta = wsfev1.PuntoVenta
148
+ cbte_nro = wsfev1.CbteNro
149
+
150
+ # llamo al webservice para consultar y validar manualmente el CAE:
151
+ wsfev1.CompConsultar(tipo_cbte, punto_vta, cbte_nro)
152
+
153
+ self.assertEqual(wsfev1.CAE, cae)
154
+ self.assertEqual(wsfev1.CbteNro, cbte_nro)
155
+ self.assertEqual(wsfev1.ImpTotal, imp_total)
156
+
157
+ wsfev1.AnalizarXml("XmlResponse")
158
+ self.assertEqual(wsfev1.ObtenerTagXml('CodAutorizacion'), str(wsfev1.CAE))
159
+ self.assertEqual(wsfev1.ObtenerTagXml('Concepto'), str(concepto))
160
+
161
+ def test_reproceso_servicios(self):
162
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
163
+ wsfev1 = self.wsfev1
164
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
165
+ tipo_cbte = 1
166
+ punto_vta = 4000
167
+ nro = wsfev1.CompUltimoAutorizado(tipo_cbte, punto_vta)
168
+ cbte_nro = int(nro) + 1
169
+ # obtengo CAE
170
+ wsfev1.Reprocesar = True
171
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro)
172
+ self.assertEqual(wsfev1.Reproceso, "")
173
+ # intento reprocesar:
174
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro)
175
+ self.assertEqual(wsfev1.Reproceso, "S")
176
+
177
+ def test_reproceso_productos(self):
178
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
179
+ wsfev1 = self.wsfev1
180
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
181
+ tipo_cbte = 1
182
+ punto_vta = 4000
183
+ nro = wsfev1.CompUltimoAutorizado(tipo_cbte, punto_vta)
184
+ cbte_nro = int(nro) + 1
185
+ # obtengo CAE
186
+ wsfev1.Reprocesar = True
187
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
188
+ self.assertEqual(wsfev1.Reproceso, "")
189
+ # intento reprocesar:
190
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
191
+ self.assertEqual(wsfev1.Reproceso, "S")
192
+
193
+ def test_reproceso_nota_debito(self):
194
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
195
+ # N/D con comprobantes asociados
196
+ wsfev1 = self.wsfev1
197
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
198
+ tipo_cbte = 2
199
+ punto_vta = 4000
200
+ nro = wsfev1.CompUltimoAutorizado(tipo_cbte, punto_vta)
201
+ cbte_nro = int(nro) + 1
202
+ # obtengo CAE
203
+ wsfev1.Reprocesar = True
204
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
205
+ self.assertEqual(wsfev1.Reproceso, "")
206
+ # intento reprocesar:
207
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
208
+ self.assertEqual(wsfev1.Reproceso, "S")
209
+
210
+
211
+ if __name__ == '__main__':
212
+ unittest.main()
app/pyafipws/tests/wslpg.py ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ import pysimplesoap.client
14
+ from pyafipws.wslpg import WSLPG
15
+ from pyafipws.wsaa import WSAA
16
+ from pyafipws import utils
17
+ "Pruebas LiquidaciΓ³n Primaria ElectrΓ³nica de Granos web service WSLPG (AFIP)"
18
+
19
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
20
+ __copyright__ = "Copyright (C) 2013 Mariano Reingart"
21
+ __license__ = "GPL 3.0"
22
+
23
+
24
+ import unittest
25
+ import sys
26
+ from decimal import Decimal
27
+
28
+ sys.path.append("/home/reingart") # TODO: proper packaging
29
+
30
+
31
+ print(pysimplesoap.client.__version__)
32
+ #assert pysimplesoap.client.__version__ >= "1.08c"
33
+
34
+
35
+ WSDL = "https://fwshomo.afip.gov.ar/wslpg/LpgService?wsdl"
36
+ CUIT = 20267565393
37
+ CERT = "/home/reingart/pyafipws/reingart.crt"
38
+ PRIVATEKEY = "/home/reingart/pyafipws/reingart.key"
39
+ CACERT = "/home/reingart/pyafipws/afip_root_desa_ca.crt"
40
+ CACHE = "/home/reingart/pyafipws/cache"
41
+
42
+ # AutenticaciΓ³n:
43
+ wsaa = WSAA()
44
+ tra = wsaa.CreateTRA(service="wslpg")
45
+ cms = wsaa.SignTRA(tra, CERT, PRIVATEKEY)
46
+ wsaa.Conectar()
47
+ wsaa.LoginCMS(cms)
48
+
49
+
50
+ class TestIssues(unittest.TestCase):
51
+
52
+ def setUp(self):
53
+ sys.argv.append("--trace") # TODO: use logging
54
+ self.wslpg = wslpg = WSLPG()
55
+ wslpg.LanzarExcepciones = True
56
+ wslpg.Conectar(url=WSDL, cacert=None, cache=CACHE)
57
+ wslpg.Cuit = CUIT
58
+ wslpg.Token = wsaa.Token
59
+ wslpg.Sign = wsaa.Sign
60
+
61
+ def test_liquidacion(self):
62
+ "Prueba de autorizaciΓ³n (obtener COE) liquidaciΓ³n electrΓ³nica de granos"
63
+ wslpg = self.wslpg
64
+ pto_emision = 99
65
+ ok = wslpg.ConsultarUltNroOrden(pto_emision)
66
+ self.assertTrue(ok)
67
+ ok = wslpg.CrearLiquidacion(
68
+ pto_emision=pto_emision,
69
+ nro_orden=wslpg.NroOrden + 1,
70
+ cuit_comprador=wslpg.Cuit,
71
+ nro_act_comprador=29, nro_ing_bruto_comprador=wslpg.Cuit,
72
+ cod_tipo_operacion=1,
73
+ es_liquidacion_propia='N', es_canje='N',
74
+ cod_puerto=14, des_puerto_localidad="DETALLE PUERTO",
75
+ cod_grano=31,
76
+ cuit_vendedor=23000000019, nro_ing_bruto_vendedor=23000000019,
77
+ actua_corredor="N", liquida_corredor="N",
78
+ cuit_corredor=0,
79
+ comision_corredor=0, nro_ing_bruto_corredor=0,
80
+ fecha_precio_operacion="2013-02-07",
81
+ precio_ref_tn=2000,
82
+ cod_grado_ref="G1",
83
+ cod_grado_ent="FG",
84
+ factor_ent=98, val_grado_ent=1.02,
85
+ precio_flete_tn=10,
86
+ cont_proteico=20,
87
+ alic_iva_operacion=10.5,
88
+ campania_ppal=1213,
89
+ cod_localidad_procedencia=5544,
90
+ cod_prov_procedencia=12,
91
+ datos_adicionales="DATOS ADICIONALES",
92
+ peso_neto_sin_certificado=10000,
93
+ cod_prov_procedencia_sin_certificado=1,
94
+ cod_localidad_procedencia_sin_certificado=15124,
95
+ )
96
+
97
+ wslpg.AgregarRetencion(
98
+ codigo_concepto="RI",
99
+ detalle_aclaratorio="DETALLE DE IVA",
100
+ base_calculo=1000,
101
+ alicuota=10.5,
102
+ )
103
+ wslpg.AgregarRetencion(
104
+ codigo_concepto="RG",
105
+ detalle_aclaratorio="DETALLE DE GANANCIAS",
106
+ base_calculo=100,
107
+ alicuota=15,
108
+ )
109
+ ok = wslpg.AutorizarLiquidacion()
110
+ self.assertTrue(ok)
111
+ self.assertIsInstance(wslpg.COE, str)
112
+ self.assertEqual(len(wslpg.COE), len("330100013142"))
113
+
114
+ def test_liquidacion_contrato(self, nro_contrato=26):
115
+ "Prueba de obtener COE variante con contrato / corredor (WSLPGv1.4)"
116
+ wslpg = self.wslpg
117
+ pto_emision = 99
118
+ ok = wslpg.ConsultarUltNroOrden(pto_emision)
119
+ self.assertTrue(ok)
120
+ nro_orden = wslpg.NroOrden + 1
121
+
122
+ # probar todas las actividades en caso de que devuelva error AFIP:
123
+ # 1106: La actividad seleccionada no corresponde al comprador
124
+ actividades = (40, 41, 29, 33, 31, 30, 35, 44, 47, 46, 48, 49, 51, 50,
125
+ 45, 59, 57, 52, 34, 28, 36, 55, 39, 37)
126
+
127
+ for actid in actividades:
128
+ ok = wslpg.CrearLiquidacion(
129
+ pto_emision=pto_emision,
130
+ nro_orden=nro_orden,
131
+ nro_contrato=nro_contrato,
132
+ cuit_comprador=20400000000,
133
+ nro_act_comprador=actid, nro_ing_bruto_comprador=20400000000,
134
+ cod_tipo_operacion=1,
135
+ es_liquidacion_propia='N', es_canje='N',
136
+ cod_puerto=14, des_puerto_localidad="DETALLE PUERTO",
137
+ cod_grano=31,
138
+ cuit_vendedor=23000000019, nro_ing_bruto_vendedor=23000000019,
139
+ actua_corredor="S", liquida_corredor="S",
140
+ cuit_corredor=20267565393,
141
+ comision_corredor=1, nro_ing_bruto_corredor=20267565393,
142
+ fecha_precio_operacion="2013-02-07",
143
+ precio_ref_tn=2000,
144
+ cod_grado_ref="G1",
145
+ cod_grado_ent="FG",
146
+ factor_ent=98, val_grado_ent=1.02,
147
+ precio_flete_tn=10,
148
+ cont_proteico=20,
149
+ alic_iva_operacion=10.5,
150
+ campania_ppal=1213,
151
+ cod_localidad_procedencia=5544,
152
+ cod_prov_procedencia=12,
153
+ datos_adicionales="DATOS ADICIONALES",
154
+ peso_neto_sin_certificado=10000,
155
+ cod_prov_procedencia_sin_certificado=1,
156
+ cod_localidad_procedencia_sin_certificado=15124,
157
+ )
158
+
159
+ wslpg.AgregarRetencion(
160
+ codigo_concepto="RI",
161
+ detalle_aclaratorio="DETALLE DE IVA",
162
+ base_calculo=1000,
163
+ alicuota=10.5,
164
+ )
165
+ wslpg.AgregarRetencion(
166
+ codigo_concepto="RG",
167
+ detalle_aclaratorio="DETALLE DE GANANCIAS",
168
+ base_calculo=100,
169
+ alicuota=15,
170
+ )
171
+ ok = wslpg.AutorizarLiquidacion()
172
+ if wslpg.COE:
173
+ # print "Actividad OK", actid
174
+ break
175
+
176
+ self.assertTrue(ok)
177
+ self.assertIsInstance(wslpg.COE, str)
178
+ self.assertEqual(len(wslpg.COE), len("330100013142"))
179
+ self.assertEqual(wslpg.NroContrato, nro_contrato)
180
+
181
+ def test_anular(self, coe=None):
182
+ "Prueba de anulaciΓ³n de una liquidaciΓ³n electrΓ³nica de granos"
183
+ wslpg = self.wslpg
184
+ if not coe:
185
+ self.test_liquidacion() # autorizo una nueva liq.
186
+ coe = wslpg.COE
187
+ ok = wslpg.AnularLiquidacion(coe) # la anulo
188
+ self.assertTrue(ok)
189
+ self.assertEqual(wslpg.Resultado, "A")
190
+
191
+ def test_ajuste_unificado(self):
192
+ "Prueba de ajuste unificado de una liquidaciΓ³n de granos (WSLPGv1.4)"
193
+ wslpg = self.wslpg
194
+ # solicito una liquidaciΓ³n para tener el COE autorizado a ajustar:
195
+ self.test_liquidacion()
196
+ coe = wslpg.COE
197
+ # solicito el ΓΊltimo nro de orden para la nueva liquidaciΓ³n de ajuste:
198
+ pto_emision = 55
199
+ ok = wslpg.ConsultarUltNroOrden(pto_emision)
200
+ self.assertTrue(ok)
201
+ nro_orden = wslpg.NroOrden + 1
202
+ # creo el ajuste base y agrego los datos de certificado:
203
+ wslpg.CrearAjusteBase(pto_emision=pto_emision,
204
+ nro_orden=nro_orden,
205
+ coe_ajustado=coe,
206
+ cod_provincia=1,
207
+ cod_localidad=5,
208
+ )
209
+ wslpg.AgregarCertificado(tipo_certificado_deposito=5,
210
+ nro_certificado_deposito=555501200729,
211
+ peso_neto=10000,
212
+ cod_localidad_procedencia=3,
213
+ cod_prov_procedencia=1,
214
+ campania=1213,
215
+ fecha_cierre='2013-01-13',
216
+ peso_neto_total_certificado=10000)
217
+ # creo el ajuste de crΓ©dito (ver documentaciΓ³n AFIP)
218
+ wslpg.CrearAjusteCredito(
219
+ diferencia_peso_neto=1000, diferencia_precio_operacion=100,
220
+ cod_grado="G2", val_grado=1.0, factor=100,
221
+ diferencia_precio_flete_tn=10,
222
+ datos_adicionales='AJUSTE CRED UNIF',
223
+ concepto_importe_iva_0='Alicuota Cero',
224
+ importe_ajustar_Iva_0=900,
225
+ concepto_importe_iva_105='Alicuota Diez',
226
+ importe_ajustar_Iva_105=800,
227
+ concepto_importe_iva_21='Alicuota Veintiuno',
228
+ importe_ajustar_Iva_21=700,
229
+ )
230
+ wslpg.AgregarDeduccion(codigo_concepto="AL",
231
+ detalle_aclaratorio="Deduc Alm",
232
+ dias_almacenaje="1",
233
+ precio_pkg_diario=0.01,
234
+ comision_gastos_adm=1.0,
235
+ base_calculo=1000.0,
236
+ alicuota=10.5, )
237
+ wslpg.AgregarRetencion(codigo_concepto="RI",
238
+ detalle_aclaratorio="Ret IVA",
239
+ base_calculo=1000,
240
+ alicuota=10.5, )
241
+ # creo el ajuste de dΓ©bito (ver documentaciΓ³n AFIP)
242
+ wslpg.CrearAjusteDebito(
243
+ diferencia_peso_neto=500, diferencia_precio_operacion=100,
244
+ cod_grado="G2", val_grado=1.0, factor=100,
245
+ diferencia_precio_flete_tn=0.01,
246
+ datos_adicionales='AJUSTE DEB UNIF',
247
+ concepto_importe_iva_0='Alic 0',
248
+ importe_ajustar_Iva_0=250,
249
+ concepto_importe_iva_105='Alic 10.5',
250
+ importe_ajustar_Iva_105=200,
251
+ concepto_importe_iva_21='Alicuota 21',
252
+ importe_ajustar_Iva_21=50,
253
+ )
254
+ wslpg.AgregarDeduccion(codigo_concepto="AL",
255
+ detalle_aclaratorio="Deduc Alm",
256
+ dias_almacenaje="1",
257
+ precio_pkg_diario=0.01,
258
+ comision_gastos_adm=1.0,
259
+ base_calculo=500.0,
260
+ alicuota=10.5, )
261
+ wslpg.AgregarRetencion(codigo_concepto="RI",
262
+ detalle_aclaratorio="Ret IVA",
263
+ base_calculo=100,
264
+ alicuota=10.5, )
265
+ # autorizo el ajuste:
266
+ ok = wslpg.AjustarLiquidacionUnificado()
267
+ self.assertTrue(ok)
268
+ # verificar respuesta general:
269
+ self.assertIsInstance(wslpg.COE, str)
270
+ self.assertEqual(len(wslpg.COE), len("330100013133"))
271
+ coe_ajustado = coe
272
+ coe = wslpg.COE
273
+ try:
274
+ self.assertEqual(wslpg.Estado, "AC")
275
+ self.assertEqual(wslpg.Subtotal, Decimal("-734.10"))
276
+ self.assertEqual(wslpg.TotalIva105, Decimal("-77.61"))
277
+ self.assertEqual(wslpg.TotalIva21, Decimal("0"))
278
+ self.assertEqual(wslpg.TotalRetencionesGanancias, Decimal("0"))
279
+ self.assertEqual(wslpg.TotalRetencionesIVA, Decimal("-94.50"))
280
+ self.assertEqual(wslpg.TotalNetoAPagar, Decimal("-716.68"))
281
+ self.assertEqual(wslpg.TotalIvaRg2300_07, Decimal("16.89"))
282
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("-733.57"))
283
+ # verificar ajuste credito
284
+ ok = wslpg.AnalizarAjusteCredito()
285
+ self.assertTrue(ok)
286
+ self.assertEqual(wslpg.GetParametro("precio_operacion"), "1.900")
287
+ self.assertEqual(wslpg.GetParametro("total_peso_neto"), "1000")
288
+ self.assertEqual(wslpg.TotalDeduccion, Decimal("11.05"))
289
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("2780.95"))
290
+ self.assertEqual(wslpg.GetParametro("importe_iva"), "293.16")
291
+ self.assertEqual(wslpg.GetParametro("operacion_con_iva"), "3085.16")
292
+ self.assertEqual(wslpg.GetParametro("deducciones", 0, "importe_iva"), "1.05")
293
+ # verificar ajuste debito
294
+ ok = wslpg.AnalizarAjusteDebito()
295
+ self.assertTrue(ok)
296
+ self.assertEqual(wslpg.GetParametro("precio_operacion"), "2.090")
297
+ self.assertEqual(wslpg.GetParametro("total_peso_neto"), "500")
298
+ self.assertEqual(wslpg.TotalDeduccion, Decimal("5.52"))
299
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("2047.38"))
300
+ self.assertEqual(wslpg.GetParametro("importe_iva"), "215.55")
301
+ self.assertEqual(wslpg.GetParametro("operacion_con_iva"), "2268.45")
302
+ self.assertEqual(wslpg.GetParametro("retenciones", 0, "importe_retencion"), "10.50")
303
+
304
+ finally:
305
+ # anulo el ajuste para evitar subsiguiente validaciΓ³n AFIP:
306
+ if coe:
307
+ self.test_anular(coe)
308
+ if coe_ajustado:
309
+ self.test_anular(coe_ajustado) # anulo tambiΓ©n la liq. orig.
310
+
311
+ def test_ajuste_contrato(self, nro_contrato=27):
312
+ "Prueba de ajuste por contrato de una liquidaciΓ³n de granos (WSLPGv1.4)"
313
+ wslpg = self.wslpg
314
+ # solicito una liquidaciΓ³n para tener el COE autorizado a ajustar:
315
+ self.test_liquidacion_contrato(nro_contrato)
316
+ coe_ajustado = wslpg.COE
317
+ # solicito el ΓΊltimo nro de orden para la nueva liquidaciΓ³n de ajuste:
318
+ pto_emision = 55
319
+ ok = wslpg.ConsultarUltNroOrden(pto_emision)
320
+ self.assertTrue(ok)
321
+ nro_orden = wslpg.NroOrden + 1
322
+ wslpg.CrearAjusteBase(pto_emision=55, nro_orden=nro_orden,
323
+ nro_contrato=nro_contrato,
324
+ coe_ajustado=coe_ajustado,
325
+ nro_act_comprador=40,
326
+ cod_grano=31,
327
+ cuit_vendedor=23000000019,
328
+ cuit_comprador=20400000000,
329
+ cuit_corredor=20267565393,
330
+ precio_ref_tn=100,
331
+ cod_grado_ent="G1",
332
+ val_grado_ent=1.01,
333
+ precio_flete_tn=1000,
334
+ cod_puerto=14,
335
+ des_puerto_localidad="Desc Puerto",
336
+ cod_provincia=1,
337
+ cod_localidad=5,
338
+ )
339
+ wslpg.CrearAjusteCredito(
340
+ concepto_importe_iva_0='Ajuste IVA al 0%',
341
+ importe_ajustar_Iva_0=100,
342
+ )
343
+ wslpg.CrearAjusteDebito(
344
+ concepto_importe_iva_105='Ajuste IVA al 10.5%',
345
+ importe_ajustar_Iva_105=100,
346
+ )
347
+ wslpg.AgregarDeduccion(codigo_concepto="OD",
348
+ detalle_aclaratorio="Otras Deduc",
349
+ dias_almacenaje="1",
350
+ base_calculo=100.0,
351
+ alicuota=10.5, )
352
+
353
+ # autorizo el ajuste:
354
+ ok = wslpg.AjustarLiquidacionContrato()
355
+ self.assertTrue(ok)
356
+ # verificar respuesta general:
357
+ coe = wslpg.COE
358
+ self.assertIsInstance(wslpg.COE, str)
359
+ self.assertEqual(len(wslpg.COE), len("330100013133"))
360
+ try:
361
+ self.assertEqual(wslpg.Estado, "AC")
362
+ self.assertEqual(wslpg.Subtotal, Decimal("-100.00"))
363
+ self.assertEqual(wslpg.TotalIva105, Decimal("0"))
364
+ self.assertEqual(wslpg.TotalIva21, Decimal("0"))
365
+ self.assertEqual(wslpg.TotalRetencionesGanancias, Decimal("0"))
366
+ self.assertEqual(wslpg.TotalRetencionesIVA, Decimal("0"))
367
+ self.assertEqual(wslpg.TotalNetoAPagar, Decimal("-110.50"))
368
+ self.assertEqual(wslpg.TotalIvaRg2300_07, Decimal("0"))
369
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("-110.50"))
370
+ # self.assertEqual(wslpg.NroContrato, nro_contrato) # no devuelto AFIP
371
+ # verificar campos globales no documentados (directamente desde el XML):
372
+ wslpg.AnalizarXml()
373
+ v = wslpg.ObtenerTagXml("totalesUnificados", "subTotalDebCred")
374
+ self.assertEqual(v, "0")
375
+ v = wslpg.ObtenerTagXml("totalesUnificados", "totalBaseDeducciones")
376
+ self.assertEqual(v, "100.0")
377
+ v = wslpg.ObtenerTagXml("totalesUnificados", "ivaDeducciones")
378
+ self.assertEqual(v, "10.50")
379
+ # verificar ajuste credito
380
+ ok = wslpg.AnalizarAjusteCredito()
381
+ self.assertTrue(ok)
382
+ self.assertEqual(wslpg.GetParametro("precio_operacion"), "0.000")
383
+ self.assertEqual(wslpg.GetParametro("total_peso_neto"), "0")
384
+ self.assertEqual(wslpg.TotalDeduccion, Decimal("0.000"))
385
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("0.000"))
386
+ self.assertEqual(float(wslpg.GetParametro("importe_iva")), 0.00)
387
+ self.assertEqual(float(wslpg.GetParametro("operacion_con_iva")), 0.00)
388
+ # verificar ajuste debito
389
+ ok = wslpg.AnalizarAjusteDebito()
390
+ self.assertTrue(ok)
391
+ self.assertEqual(float(wslpg.GetParametro("precio_operacion")), 0.00)
392
+ self.assertEqual(float(wslpg.GetParametro("total_peso_neto")), 0)
393
+ self.assertEqual(wslpg.TotalDeduccion, Decimal("110.50"))
394
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("-110.50"))
395
+ self.assertEqual(float(wslpg.GetParametro("importe_iva")), 0.00)
396
+ self.assertEqual(float(wslpg.GetParametro("operacion_con_iva")), 0.00)
397
+ self.assertEqual(float(wslpg.GetParametro("deducciones", 0, "importe_iva")), 10.50)
398
+ self.assertEqual(float(wslpg.GetParametro("deducciones", 0, "importe_deduccion")), 110.50)
399
+
400
+ finally:
401
+ # anulo el ajuste para evitar subsiguiente validaciΓ³n AFIP:
402
+ # 2105: No puede relacionar la liquidacion con el contrato, porque el contrato tiene un Ajuste realizado.
403
+ # 2106: No puede ajustar el contrato, porque tiene liquidaciones relacionadas con ajuste.
404
+ # anular primero el ajuste para evitar la validaciΓ³n de AFIP:
405
+ # 2108: No puede anular la liquidaciΓ³n porque estΓ‘ relacionada a un contrato con ajuste vigente.
406
+ if coe:
407
+ self.test_anular(coe)
408
+ if coe_ajustado:
409
+ self.test_anular(coe_ajustado) # anulo tambiΓ©n el COE ajustado
410
+
411
+ def atest_ajuste_papel(self):
412
+ # deshabilitado ya que el mΓ©todo esta "en estudio" por parte de AFIP
413
+ wslpg = self.wslpg
414
+ wslpg.CrearAjusteBase(pto_emision=50,
415
+ nro_orden=1,
416
+ tipo_formulario=6,
417
+ nro_formulario="000101800999",
418
+ actividad=46,
419
+ cuit_comprador=99999999999,
420
+ nro_ing_bruto_comprador=99999999999,
421
+ tipo_operacion=1,
422
+ cod_grano=31,
423
+ cuit_vendedor=30000000007,
424
+ nro_ing_bruto_vendedor=30000000007,
425
+ cod_provincia=1,
426
+ cod_localidad=5)
427
+ wslpg.AgregarCertificado(tipo_certificado_deposito=5,
428
+ nro_certificado_deposito=555501200802,
429
+ peso_neto=10000,
430
+ cod_localidad_procedencia=5,
431
+ cod_prov_procedencia=1,
432
+ campania=1213,
433
+ fecha_cierre='2013-07-12')
434
+ wslpg.CrearAjusteCredito(
435
+ concepto_importe_iva_21='IVA al 21%',
436
+ importe_ajustar_Iva_21=1500,
437
+ )
438
+ wslpg.AgregarRetencion(codigo_concepto="RI",
439
+ detalle_aclaratorio="Ret IVA",
440
+ base_calculo=1500,
441
+ alicuota=8, )
442
+ wslpg.CrearAjusteDebito(
443
+ concepto_importe_iva_105='IVA al 0%',
444
+ importe_ajustar_Iva_105=100,
445
+ )
446
+
447
+ ret = wslpg.AjustarLiquidacionUnificadoPapel()
448
+
449
+ def test_asociaciar_coe_contrato(self, nro_contrato=27):
450
+ wslpg = self.wslpg
451
+ # solicito una liquidaciΓ³n para tener el COE autorizado a asociar:
452
+ self.test_liquidacion()
453
+ coe = wslpg.COE
454
+ try:
455
+ # Asocio la liquidaciΓ³n con el contrato:
456
+ wslpg.AsociarLiquidacionAContrato(coe=coe,
457
+ nro_contrato=nro_contrato,
458
+ cuit_comprador="20400000000",
459
+ cuit_vendedor="23000000019",
460
+ cuit_corredor="20267565393",
461
+ cod_grano=31)
462
+ self.assertEqual(wslpg.Errores, [])
463
+ self.assertIsInstance(wslpg.COE, str)
464
+ self.assertEqual(len(wslpg.COE), len("330100013133"))
465
+ self.assertEqual(wslpg.Estado, "AC")
466
+ finally:
467
+ # anulo el ajuste para evitar subsiguiente validaciΓ³n AFIP:
468
+ # 2105: No puede relacionar la liquidacion con el contrato, porque el contrato tiene un Ajuste realizado.
469
+ # 2112: La liquidacion ya esta relacionada al contrato.
470
+ try:
471
+ self.test_anular(coe)
472
+ except BaseException:
473
+ # ignorar error de AFIP (aparentemente problema interno):
474
+ self.assertEqual(wslpg.Errores[0], "2100: El contrato ingresado no se encuentra registrado.")
475
+ pass
476
+
477
+ def test_consultar_liquidaciones_por_contrato(self, nro_contrato=26):
478
+ wslpg = self.wslpg
479
+ # obtener las liquidaciones relacionadas al contrato:
480
+ wslpg.ConsultarLiquidacionesPorContrato(
481
+ nro_contrato=nro_contrato,
482
+ cuit_comprador="20400000000",
483
+ cuit_vendedor="23000000019",
484
+ cuit_corredor="20267565393",
485
+ cod_grano=31,
486
+ )
487
+ self.assertEqual(wslpg.Errores, [])
488
+ # verifico COEs previamente relacionados al contrato:
489
+ for coe in sorted([330100014020, 330100014022, 330100014023,
490
+ 330100014025, 330100014028, 330100014029,
491
+ 330100014040, 330100014043, 330100014057,
492
+ 330100014061, 330100014450, 330100014454,
493
+ 330100014455, 330100014459, 330100014467,
494
+ 330100014472, 330100004664]):
495
+ self.assertIsInstance(wslpg.COE, str)
496
+ self.assertEqual(wslpg.COE, str(coe))
497
+ self.assertEqual(wslpg.Estado, "") # por el momento no lo devuelve
498
+ # leo el prΓ³ximo numero
499
+ wslpg.LeerDatosLiquidacion()
500
+
501
+ def test_consultar_ajuste_unificado(self):
502
+ "Prueba de consulta de un ajuste unificado (WSLPGv1.4)"
503
+ wslpg = self.wslpg
504
+ # uso datos de un ajuste generado con test_ajuste_unificado:
505
+ pto_emision = 55
506
+ nro_orden = 78
507
+ # consulto el ajuste:
508
+ ok = wslpg.ConsultarAjuste(pto_emision, nro_orden)
509
+ self.assertTrue(ok)
510
+ # verificar respuesta general:
511
+ self.assertEqual(wslpg.COE, "330100014501")
512
+ self.assertEqual(wslpg.Estado, "AN") # anulado!
513
+ self.assertEqual(wslpg.Subtotal, Decimal("-734.10"))
514
+ self.assertEqual(wslpg.TotalIva105, Decimal("-77.61"))
515
+ self.assertEqual(wslpg.TotalIva21, Decimal("0"))
516
+ self.assertEqual(wslpg.TotalRetencionesGanancias, Decimal("0"))
517
+ self.assertEqual(wslpg.TotalRetencionesIVA, Decimal("-94.50"))
518
+ self.assertEqual(wslpg.TotalNetoAPagar, Decimal("-716.68"))
519
+ self.assertEqual(wslpg.TotalIvaRg2300_07, Decimal("16.89"))
520
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("-733.57"))
521
+ # verificar ajuste credito
522
+ ok = wslpg.AnalizarAjusteCredito()
523
+ self.assertTrue(ok)
524
+ self.assertEqual(float(wslpg.GetParametro("precio_operacion")), 1.9)
525
+ self.assertEqual(wslpg.GetParametro("total_peso_neto"), "1000")
526
+ self.assertEqual(wslpg.TotalDeduccion, Decimal("11.05"))
527
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("2780.95"))
528
+ self.assertEqual(float(wslpg.GetParametro("importe_iva")), 293.16)
529
+ self.assertEqual(float(wslpg.GetParametro("operacion_con_iva")), 3085.16)
530
+ self.assertEqual(float(wslpg.GetParametro("deducciones", 0, "importe_iva")), 1.05)
531
+ # verificar ajuste debito
532
+ ok = wslpg.AnalizarAjusteDebito()
533
+ self.assertTrue(ok)
534
+ self.assertEqual(float(wslpg.GetParametro("precio_operacion")), 2.09)
535
+ self.assertEqual(wslpg.GetParametro("total_peso_neto"), "500")
536
+ self.assertEqual(wslpg.TotalDeduccion, Decimal("5.52"))
537
+ self.assertEqual(wslpg.TotalPagoSegunCondicion, Decimal("2047.38"))
538
+ self.assertEqual(float(wslpg.GetParametro("importe_iva")), 215.55)
539
+ self.assertEqual(float(wslpg.GetParametro("operacion_con_iva")), 2268.45)
540
+ self.assertEqual(float(wslpg.GetParametro("retenciones", 0, "importe_retencion")), 10.50)
541
+
542
+
543
+ if __name__ == '__main__':
544
+ unittest.main()
app/pyafipws/tests/wslpg_cert_autorizar_resp.xml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
3
+ <S:Body>
4
+ <ns2:cgAutorizarResp xmlns:ns2="http://serviciosjava.afip.gob.ar/wslpg/">
5
+ <oReturn>
6
+ <autorizacion>
7
+ <ptoEmision>21</ptoEmision>
8
+ <nroOrden>2</nroOrden>
9
+ <coe>332000000189</coe>
10
+ <estado>AC</estado>
11
+ <fechaCertificacion>2015-02-24</fechaCertificacion>
12
+ <pesosResumen>
13
+ <pesoBrutoCertificado>1500.00</pesoBrutoCertificado>
14
+ <pesoMermaVolatil>0.00</pesoMermaVolatil>
15
+ <pesoMermaSecado>0.00</pesoMermaSecado>
16
+ <pesoMermaZarandeo>0.00</pesoMermaZarandeo>
17
+ <pesoNetoCertificado>1500.00</pesoNetoCertificado>
18
+ </pesosResumen>
19
+ <serviciosResumen>
20
+ <servicioSecado>0.00</servicioSecado>
21
+ <servicioZarandeo>0.00</servicioZarandeo>
22
+ <servicioOtros>0.00</servicioOtros>
23
+ <servicioGastosGenerales>0.00</servicioGastosGenerales>
24
+ <importeIVA>0.00</importeIVA>
25
+ <servicioTotal>0.00</servicioTotal>
26
+ </serviciosResumen>
27
+ </autorizacion>
28
+ </oReturn>
29
+ </ns2:cgAutorizarResp>
30
+ </S:Body>
31
+ </S:Envelope>
app/pyafipws/tests/wsmtx.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ from pyafipws.wsaa import WSAA
14
+ from pyafipws.wsmtx import WSMTXCA
15
+ "Pruebas para WSMTX de AFIP (Factura ElectrοΏ½nica Mercado Interno con detalle)"
16
+
17
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
18
+ __copyright__ = "Copyright (C) 2010 Mariano Reingart"
19
+ __license__ = "GPL 3.0"
20
+
21
+ import unittest
22
+ import os
23
+ import time
24
+ import sys
25
+ from decimal import Decimal
26
+ import datetime
27
+
28
+ sys.path.append("/home/reingart") # TODO: proper packaging
29
+
30
+
31
+ WSDL = "https://fwshomo.afip.gov.ar/wsmtxca/services/MTXCAService?wsdl"
32
+ CUIT = 20267565393
33
+ CERT = "/home/reingart/pyafipws/reingart.crt"
34
+ PRIVATEKEY = "/home/reingart/pyafipws/reingart.key"
35
+ CACERT = "/home/reingart/pyafipws/afip_root_desa_ca.crt"
36
+ CACHE = "/home/reingart/pyafipws/cache"
37
+
38
+ # AutenticaciοΏ½n:
39
+ wsaa = WSAA()
40
+ tra = wsaa.CreateTRA(service="wsmtxca")
41
+ cms = wsaa.SignTRA(tra, CERT, PRIVATEKEY)
42
+ wsaa.Conectar()
43
+ wsaa.LoginCMS(cms)
44
+
45
+
46
+ class TestMTX(unittest.TestCase):
47
+
48
+ def setUp(self):
49
+ sys.argv.append("--trace") # TODO: use logging
50
+ self.wsmtxca = wsmtxca = WSMTXCA()
51
+ wsmtxca.Cuit = CUIT
52
+ wsmtxca.Token = wsaa.Token
53
+ wsmtxca.Sign = wsaa.Sign
54
+ wsmtxca.Conectar(CACHE, WSDL)
55
+
56
+ def atest_dummy(self):
57
+ print(wsmtxca.client.help("dummy"))
58
+ wsmtxca.Dummy()
59
+ print("AppServerStatus", wsmtxca.AppServerStatus)
60
+ print("DbServerStatus", wsmtxca.DbServerStatus)
61
+ print("AuthServerStatus", wsmtxca.AuthServerStatus)
62
+
63
+ def test_autorizar_comprobante(self, tipo_cbte=1, cbte_nro=None, servicios=True, tributos=True):
64
+ "Prueba de autorizaciοΏ½n de un comprobante (obtenciοΏ½n de CAE)"
65
+ wsmtxca = self.wsmtxca
66
+
67
+ # datos generales del comprobante:
68
+ punto_vta = 4000
69
+ if not cbte_nro:
70
+ # si no me especifοΏ½can nro de comprobante, busco el prοΏ½ximo
71
+ cbte_nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
72
+ cbte_nro = int(cbte_nro) + 1
73
+ fecha = datetime.datetime.now().strftime("%Y-%m-%d")
74
+ tipo_doc = 80
75
+ nro_doc = "30000000007"
76
+ cbt_desde = cbte_nro
77
+ cbt_hasta = cbt_desde
78
+ imp_tot_conc = "0.00"
79
+ imp_neto = "100.00"
80
+ if tributos:
81
+ imp_total = "123.00"
82
+ imp_trib = "2.00"
83
+ else:
84
+ imp_total = "121.00"
85
+ imp_trib = "0.00"
86
+ imp_op_ex = "0.00"
87
+ imp_subtotal = "100.00"
88
+ fecha_cbte = fecha
89
+ # Fechas del perοΏ½odo del servicio facturado (solo si concepto = 1?)
90
+ if servicios:
91
+ concepto = 3
92
+ fecha_venc_pago = fecha
93
+ fecha_serv_desde = fecha
94
+ fecha_serv_hasta = fecha
95
+ else:
96
+ concepto = 1
97
+ fecha_venc_pago = fecha_serv_desde = fecha_serv_hasta = None
98
+ moneda_id = 'PES'
99
+ moneda_ctz = '1.000'
100
+ obs = "Observaciones Comerciales, libre"
101
+
102
+ wsmtxca.CrearFactura(concepto, tipo_doc, nro_doc, tipo_cbte, punto_vta,
103
+ cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto,
104
+ imp_subtotal, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
105
+ fecha_serv_desde, fecha_serv_hasta, # --
106
+ moneda_id, moneda_ctz, obs)
107
+
108
+ # agrego un comprobante asociado (solo notas de crοΏ½dito / dοΏ½bito)
109
+ if tipo_cbte in (2, 3):
110
+ tipo = 1
111
+ pv = 2
112
+ nro = 1234
113
+ wsmtxca.AgregarCmpAsoc(tipo, pv, nro)
114
+
115
+ if tributos:
116
+ # agrego otros tributos:
117
+ tributo_id = 99
118
+ desc = 'Impuesto Municipal Matanza'
119
+ base_imp = "100.00"
120
+ alic = "1.00"
121
+ importe = "1.00"
122
+ wsmtxca.AgregarTributo(tributo_id, desc, base_imp, alic, importe)
123
+
124
+ # agrego otros tributos:
125
+ tributo_id = 1
126
+ desc = 'Impuestos Internos'
127
+ base_imp = "100.00"
128
+ alic = "1.00"
129
+ importe = "1.00"
130
+ wsmtxca.AgregarTributo(tributo_id, desc, base_imp, alic, importe)
131
+
132
+ # agrego el subtotal por tasa de IVA:
133
+ iva_id = 5 # 21%
134
+ base_imp = "100.00"
135
+ importe = 21
136
+ wsmtxca.AgregarIva(iva_id, base_imp, importe)
137
+
138
+ # agrego un artοΏ½culo:
139
+ u_mtx = 123456
140
+ cod_mtx = 1234567890123
141
+ codigo = "P0001"
142
+ ds = "Descripcion del producto P0001"
143
+ qty = 2.00
144
+ umed = 7
145
+ precio = 100.00
146
+ bonif = 0.00
147
+ iva_id = 5
148
+ imp_iva = 42.00
149
+ imp_subtotal = 242.00
150
+ wsmtxca.AgregarItem(u_mtx, cod_mtx, codigo, ds, qty, umed, precio, bonif,
151
+ iva_id, imp_iva, imp_subtotal)
152
+
153
+ # agrego bonificaciοΏ½n general
154
+ wsmtxca.AgregarItem(None, None, None, 'bonificacion', 0, 99, 1, None,
155
+ 5, -21, -121)
156
+
157
+ # llamo al websevice para obtener el CAE:
158
+ wsmtxca.AutorizarComprobante()
159
+
160
+ self.assertEqual(wsmtxca.Resultado, "A") # Aprobado!
161
+ self.assertIsInstance(wsmtxca.CAE, str)
162
+ self.assertEqual(len(wsmtxca.CAE), len("63363178822329"))
163
+ self.assertEqual(len(wsmtxca.Vencimiento), len("2013-09-07"))
164
+
165
+ cae = wsmtxca.CAE
166
+
167
+ # llamo al webservice para consultar y validar manualmente el CAE:
168
+ wsmtxca.ConsultarComprobante(tipo_cbte, punto_vta, cbte_nro)
169
+
170
+ self.assertEqual(wsmtxca.CAE, cae)
171
+ self.assertEqual(wsmtxca.CbteNro, cbte_nro)
172
+ self.assertEqual(wsmtxca.ImpTotal, imp_total)
173
+
174
+ wsmtxca.AnalizarXml("XmlResponse")
175
+ self.assertEqual(wsmtxca.ObtenerTagXml('codigoAutorizacion'), str(wsmtxca.CAE))
176
+ self.assertEqual(wsmtxca.ObtenerTagXml('codigoConcepto'), str(concepto))
177
+ self.assertEqual(wsmtxca.ObtenerTagXml('arrayItems', 0, 'item', 'unidadesMtx'), '123456')
178
+
179
+ def test_reproceso_servicios(self):
180
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
181
+ wsmtxca = self.wsmtxca
182
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
183
+ tipo_cbte = 1
184
+ punto_vta = 4000
185
+ nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
186
+ cbte_nro = int(nro) + 1
187
+ # obtengo CAE
188
+ wsmtxca.Reprocesar = True
189
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro)
190
+ self.assertEqual(wsmtxca.Reproceso, "")
191
+ # intento reprocesar:
192
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro)
193
+ self.assertEqual(wsmtxca.Reproceso, "S")
194
+
195
+ def test_reproceso_productos(self):
196
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
197
+ wsmtxca = self.wsmtxca
198
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
199
+ tipo_cbte = 1
200
+ punto_vta = 4000
201
+ nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
202
+ cbte_nro = int(nro) + 1
203
+ # obtengo CAE
204
+ wsmtxca.Reprocesar = True
205
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
206
+ self.assertEqual(wsmtxca.Reproceso, "")
207
+ # intento reprocesar:
208
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
209
+ self.assertEqual(wsmtxca.Reproceso, "S")
210
+
211
+ def test_reproceso_nota_debito(self):
212
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
213
+ # N/D con comprobantes asociados
214
+ wsmtxca = self.wsmtxca
215
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
216
+ tipo_cbte = 2
217
+ punto_vta = 4000
218
+ nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
219
+ cbte_nro = int(nro) + 1
220
+ # obtengo CAE
221
+ wsmtxca.Reprocesar = True
222
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
223
+ self.assertEqual(wsmtxca.Reproceso, "")
224
+ # intento reprocesar:
225
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, servicios=False)
226
+ self.assertEqual(wsmtxca.Reproceso, "S")
227
+
228
+ def test_reproceso_sin_tributos(self):
229
+ "Prueba de reproceso de un comprobante (recupero de CAE por consulta)"
230
+ wsmtxca = self.wsmtxca
231
+ # obtengo el prοΏ½ximo nοΏ½mero de comprobante
232
+ tipo_cbte = 1
233
+ punto_vta = 4000
234
+ nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
235
+ cbte_nro = int(nro) + 1
236
+ # obtengo CAE (sin tributos)
237
+ wsmtxca.Reprocesar = True
238
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, tributos=False)
239
+ self.assertEqual(wsmtxca.Reproceso, "")
240
+ # intento reprocesar:
241
+ self.test_autorizar_comprobante(tipo_cbte, cbte_nro, tributos=False)
242
+ self.assertEqual(wsmtxca.Reproceso, "S")
243
+
244
+
245
+ if __name__ == '__main__':
246
+ unittest.main()
app/pyafipws/trazafito.py ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MοΏ½dulo Trazabilidad de Productos Fitosanitarios SENASA ResoluciοΏ½n 369/2013"
14
+
15
+ # InformaciοΏ½n adicional y documentaciοΏ½n:
16
+ # http://www.sistemasagiles.com.ar/trac/wiki/TrazabilidadProductosFitosanitarios
17
+
18
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
19
+ __copyright__ = "Copyright (C) 2014 Mariano Reingart"
20
+ __license__ = "GPL 3.0+"
21
+ __version__ = "1.11d"
22
+
23
+ # http://senasa.servicios.pami.org.ar/
24
+
25
+ import os
26
+ import socket
27
+ import sys
28
+ import datetime
29
+ import time
30
+ import traceback
31
+ import pysimplesoap.client
32
+ from pysimplesoap.client import SoapClient, SoapFault, parse_proxy, \
33
+ set_http_wrapper
34
+ from pysimplesoap.simplexml import SimpleXMLElement
35
+ from io import StringIO
36
+
37
+ # importo funciones compartidas:
38
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, \
39
+ dar_nombre_campo_dbf, get_install_dir, BaseWS, \
40
+ inicializar_y_capturar_excepciones
41
+
42
+ HOMO = False
43
+ TYPELIB = False
44
+
45
+ WSDL = "https://servicios.pami.org.ar/trazaenagr.WebService?wsdl"
46
+ LOCATION = "https://servicios.pami.org.ar/trazaenagr.WebService"
47
+
48
+ # Formato de TransaccionSenasaDTO (SaveTransaccion)
49
+ TRANSACCION_DTO = [
50
+ ('gln_origen', 13, A),
51
+ ('gln_destino', 13, A),
52
+ ('f_operacion', 10, A),
53
+ ('f_elaboracion', 10, A),
54
+ ('f_vto', 10, A),
55
+ ('id_evento', 15, N),
56
+ ('cod_producto', 14, A),
57
+ ('n_cantidad', 30, N),
58
+ ('n_serie', 20, A),
59
+ ('n_lote', 50, A),
60
+ ('n_cai', 15, A),
61
+ ('n_cae', 15, A),
62
+ ('id_motivo_destruccion', 5, A),
63
+ ('n_manifiesto', 15, N),
64
+ ('en_transporte', 1, A), # boolean
65
+ ('n_remito', 15, A),
66
+ ('motivo_devolucion', 100, A),
67
+ ('observaciones', 1000, A),
68
+ ('n_vale_compra', 15, A),
69
+ ('apellidoNombres', 255, A),
70
+ ('direccion', 200, A),
71
+ ('numero', 6, N),
72
+ ('localidad', 15, A),
73
+ ('provincia', 15, A),
74
+ ('n_postal', 8, A),
75
+ ('cuit', 11, A),
76
+ ('codigo_transaccion', 14, A),
77
+ ]
78
+
79
+ # Formato para TransaccionSenasa (getTransacciones)
80
+ TRANSACCIONES = [
81
+ ('id_transaccion_global', 15, N),
82
+ ('id_transaccion', 15, N),
83
+ ('f_transaccion', 10, A),
84
+ ('f_operacion', 10, A),
85
+ ('f_vencimiento', 10, A),
86
+ ('f_elaboracion', 10, A),
87
+ ('d_evento', 100, A),
88
+ ('cantidad', 30, N),
89
+ ('id_unidad', 15, N),
90
+ ('d_unidad', 100, A),
91
+ ('cod_producto', 14, A),
92
+ ('id_unidad', 15, N),
93
+ ('n_serie', 20, A),
94
+ ('n_lote', 50, A),
95
+ ('n_cai', 15, A),
96
+ ('n_cae', 15, A),
97
+ ('d_motivo_destruccion', 50, A),
98
+ ('d_manifiesto', 15, A),
99
+ ('en_transporte', 1, A),
100
+ ('n_remito', 30, A),
101
+ ('motivo_devolucion', 200, A),
102
+ ('observaciones', 1000, A),
103
+ ('n_vale_compra', 15, A),
104
+ ('apellidoNombres', 255, A),
105
+ ('direccion', 200, A),
106
+ ('numero', 6, A),
107
+ ('localidad', 250, A),
108
+ ('provincia', 250, A),
109
+ ('n_postal', 8, A),
110
+ ('cuit', 11, A),
111
+ ('d_agente_informador', 255, A),
112
+ ('d_agente_origen', 255, A),
113
+ ('d_agente_destino', 255, A),
114
+ ('d_producto', 250, A),
115
+ ('d_estado_transaccion', 30, A),
116
+ ('d_tipo_transaccion', 30, A),
117
+ ]
118
+
119
+ # Formato para Errores
120
+ ERRORES = [
121
+ ('_c_error', 4, A), # cοΏ½digo
122
+ ('_d_error', 250, A), # descripciοΏ½n
123
+ ]
124
+
125
+
126
+ class TrazaFito(BaseWS):
127
+ "Interfaz para el WebService de Trazabilidad de Fitosanitarios SENASA"
128
+
129
+ _public_methods_ = ['SaveTransaccion', 'SendCancelaTransac',
130
+ 'SendConfirmaTransacc', 'SendAlertaTransacc',
131
+ 'GetTransacciones',
132
+ 'Conectar', 'LeerError', 'LeerTransaccion',
133
+ 'SetUsername',
134
+ 'SetParametro', 'GetParametro',
135
+ 'GetCodigoTransaccion', 'GetResultado', 'LoadTestXML']
136
+
137
+ _public_attrs_ = [
138
+ 'Username', 'Password',
139
+ 'CodigoTransaccion', 'Errores', 'Resultado',
140
+ 'XmlRequest', 'XmlResponse',
141
+ 'Version', 'InstallDir',
142
+ 'Traceback', 'Excepcion', 'LanzarExcepciones',
143
+ 'CantPaginas', 'HayError', 'TransaccionSenasa',
144
+ ]
145
+
146
+ _reg_progid_ = "TrazaFito"
147
+ _reg_clsid_ = "{39793931-450A-4F66-9324-D4D981FC5319}"
148
+
149
+ # Variables globales para BaseWS:
150
+ HOMO = HOMO
151
+ WSDL = WSDL
152
+ Version = "%s %s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '',
153
+ pysimplesoap.client.__version__)
154
+
155
+ def __init__(self, reintentos=1):
156
+ self.Username = self.Password = None
157
+ self.TransaccionSenasa = []
158
+ BaseWS.__init__(self, reintentos)
159
+
160
+ def inicializar(self):
161
+ BaseWS.inicializar(self)
162
+ self.CodigoTransaccion = self.Errores = self.Resultado = None
163
+ self.Resultado = ''
164
+ self.Errores = [] # lista de strings para la interfaz
165
+ self.errores = [] # lista de diccionarios (uso interno)
166
+ self.CantPaginas = self.HayError = None
167
+
168
+ def __analizar_errores(self, ret):
169
+ "Comprueba y extrae errores si existen en la respuesta XML"
170
+ self.errores = ret.get('errores', [])
171
+ self.Errores = ["%s: %s" % (it['c_error'], it['d_error'])
172
+ for it in ret.get('errores', [])]
173
+ self.Resultado = ret.get('resultado')
174
+
175
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30):
176
+ # Conecto usando el mοΏ½todo estandard:
177
+ ok = BaseWS.Conectar(self, cache, wsdl, proxy, wrapper, cacert, timeout,
178
+ soap_server="jetty")
179
+ if ok:
180
+ # si el archivo es local, asumo que ya esta corregido:
181
+ if not self.wsdl.startswith("file"):
182
+ # corrijo ubicaciοΏ½n del servidor (localhost:9050 en el WSDL)
183
+ location = self.wsdl[:-5]
184
+ ws = self.client.services['IWebServiceSenasa']
185
+ ws['ports']['IWebServiceSenasaPort']['location'] = location
186
+
187
+ # Establecer credenciales de seguridad:
188
+ self.client['wsse:Security'] = {
189
+ 'wsse:UsernameToken': {
190
+ 'wsse:Username': self.Username,
191
+ 'wsse:Password': self.Password,
192
+ }
193
+ }
194
+ return ok
195
+
196
+ @inicializar_y_capturar_excepciones
197
+ def SaveTransaccion(self, usuario, password,
198
+ gln_origen=None, gln_destino=None,
199
+ f_operacion=None, f_elaboracion=None, f_vto=None,
200
+ id_evento=None, cod_producto=None, n_cantidad=None,
201
+ n_serie=None, n_lote=None, n_cai=None, n_cae=None,
202
+ id_motivo_destruccion=None, n_manifiesto=None,
203
+ en_transporte=None, n_remito=None,
204
+ motivo_devolucion=None, observaciones=None,
205
+ n_vale_compra=None, apellidoNombres=None,
206
+ direccion=None, numero=None, localidad=None,
207
+ provincia=None, n_postal=None, cuit=None
208
+ ):
209
+ "Realiza el registro de una transacciοΏ½n de productos fitosanitarios. "
210
+ # creo los parοΏ½metros para esta llamada
211
+ params = {'gln_origen': gln_origen,
212
+ 'gln_destino': gln_destino,
213
+ 'f_operacion': f_operacion,
214
+ 'f_elaboracion': f_elaboracion,
215
+ 'f_vto': f_vto,
216
+ 'id_evento': id_evento,
217
+ 'cod_producto': cod_producto,
218
+ 'n_cantidad': n_cantidad,
219
+ 'n_serie': n_serie,
220
+ 'n_lote': n_lote,
221
+ 'n_cai': n_cai or None,
222
+ 'n_cae': n_cae or None,
223
+ 'id_motivo_destruccion': id_motivo_destruccion or None,
224
+ 'n_manifiesto': n_manifiesto or None,
225
+ 'en_transporte': en_transporte or None,
226
+ 'n_remito': n_remito or None,
227
+ 'motivo_devolucion': motivo_devolucion or None,
228
+ 'observaciones': observaciones or None,
229
+ 'n_vale_compra': n_vale_compra or None,
230
+ 'apellidoNombres': apellidoNombres or None,
231
+ 'direccion': direccion or None,
232
+ 'numero': numero or None,
233
+ 'localidad': localidad or None,
234
+ 'provincia': provincia or None,
235
+ 'n_postal': n_postal or None,
236
+ 'cuit': cuit or None,
237
+ }
238
+ res = self.client.saveTransacciones(
239
+ arg0=params,
240
+ arg1=usuario,
241
+ arg2=password,
242
+ )
243
+ ret = res['return']
244
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
245
+ self.__analizar_errores(ret)
246
+ return True
247
+
248
+ @inicializar_y_capturar_excepciones
249
+ def SendCancelaTransac(self, usuario, password, codigo_transaccion):
250
+ " Realiza la cancelaciοΏ½n de una transacciοΏ½n"
251
+ res = self.client.sendCancelaTransac(
252
+ arg0=codigo_transaccion,
253
+ arg1=usuario,
254
+ arg2=password,
255
+ )
256
+ ret = res['return']
257
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
258
+ self.__analizar_errores(ret)
259
+ return True
260
+
261
+ @inicializar_y_capturar_excepciones
262
+ def SendConfirmaTransacc(self, usuario, password, p_ids_transac, f_operacion, n_cantidad=None):
263
+ "Confirma la recepciοΏ½n de un medicamento"
264
+ res = self.client.sendConfirmaTransacc(
265
+ arg0=usuario,
266
+ arg1=password,
267
+ arg2={'p_ids_transac': p_ids_transac, 'f_operacion': f_operacion,
268
+ 'n_cantidad': n_cantidad,
269
+ },
270
+ )
271
+ ret = res['return']
272
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
273
+ self.__analizar_errores(ret)
274
+ return True
275
+
276
+ @inicializar_y_capturar_excepciones
277
+ def SendAlertaTransacc(self, usuario, password, p_ids_transac_ws):
278
+ "Alerta un medicamento, acciοΏ½n contraria a οΏ½confirmar la transacciοΏ½nοΏ½."
279
+ res = self.client.sendAlertaTransacc(
280
+ arg0=usuario,
281
+ arg1=password,
282
+ arg2=p_ids_transac_ws,
283
+ )
284
+ ret = res['return']
285
+ self.CodigoTransaccion = ret.get('id_transac_asociada')
286
+ self.__analizar_errores(ret)
287
+ return True
288
+
289
+ @inicializar_y_capturar_excepciones
290
+ def GetTransacciones(self, usuario, password,
291
+ id_transaccion=None, id_evento=None, gln_origen=None,
292
+ fecha_desde_t=None, fecha_hasta_t=None,
293
+ fecha_desde_v=None, fecha_hasta_v=None,
294
+ gln_informador=None, id_tipo_transaccion=None,
295
+ gtin_elemento=None, n_lote=None, n_serie=None,
296
+ n_remito_factura=None,
297
+ ):
298
+ "Trae un listado de las transacciones que no estοΏ½n confirmadas"
299
+
300
+ # preparo los parametros de entrada opcionales:
301
+ kwargs = {}
302
+ if id_transaccion is not None:
303
+ kwargs['arg2'] = id_transaccion
304
+ if id_evento is not None:
305
+ kwargs['arg3'] = id_evento
306
+ if gln_origen is not None:
307
+ kwargs['arg4'] = gln_origen
308
+ if fecha_desde_t is not None:
309
+ kwargs['arg5'] = fecha_desde_t
310
+ if fecha_hasta_t is not None:
311
+ kwargs['arg6'] = fecha_hasta_t
312
+ if fecha_desde_v is not None:
313
+ kwargs['arg7'] = fecha_desde_v
314
+ if fecha_hasta_v is not None:
315
+ kwargs['arg8'] = fecha_hasta_v
316
+ if gln_informador is not None:
317
+ kwargs['arg9'] = gln_informador
318
+ if id_tipo_transaccion is not None:
319
+ kwargs['arg10'] = id_tipo_transaccion
320
+ if gtin_elemento is not None:
321
+ kwargs['arg11'] = gtin_elemento
322
+ if n_lote is not None:
323
+ kwargs['arg12'] = n_lote
324
+ if n_serie is not None:
325
+ kwargs['arg13'] = n_serie
326
+ if n_remito_factura is not None:
327
+ kwargs['arg14'] = n_remito_factura
328
+
329
+ # llamo al webservice
330
+ res = self.client.getTransacciones(
331
+ arg0=usuario,
332
+ arg1=password,
333
+ **kwargs
334
+ )
335
+ ret = res['return']
336
+ if ret:
337
+ self.__analizar_errores(ret)
338
+ self.CantPaginas = ret.get('cantPaginas')
339
+ self.HayError = ret.get('hay_error')
340
+ self.TransaccionSenasa = [it for it in ret.get('list', [])]
341
+ return True
342
+
343
+ def LeerTransaccion(self):
344
+ "Recorro TransaccionSenasa devuelto por GetTransacciones"
345
+ # usar GetParametro para consultar el valor retornado por el webservice
346
+
347
+ if self.TransaccionSenasa:
348
+ # extraigo el primer item
349
+ self.params_out = self.TransaccionSenasa.pop(0)
350
+ return True
351
+ else:
352
+ # limpio los parοΏ½metros
353
+ self.params_out = {}
354
+ return False
355
+
356
+ def LeerError(self):
357
+ "Recorro los errores devueltos y devuelvo el primero si existe"
358
+
359
+ if self.Errores:
360
+ # extraigo el primer item
361
+ er = self.Errores.pop(0)
362
+ return er
363
+ else:
364
+ return ""
365
+
366
+ def SetUsername(self, username):
367
+ "Establezco el nombre de usuario"
368
+ self.Username = username
369
+
370
+ def SetPassword(self, password):
371
+ "Establezco la contraseοΏ½a"
372
+ self.Password = password
373
+
374
+ def GetCodigoTransaccion(self):
375
+ "Devuelvo el cοΏ½digo de transacciοΏ½n"
376
+ return self.CodigoTransaccion
377
+
378
+ def GetResultado(self):
379
+ "Devuelvo el resultado"
380
+ return self.Resultado
381
+
382
+
383
+ def main():
384
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
385
+ import os
386
+ import time
387
+ import sys
388
+ global WSDL, LOCATION
389
+
390
+ DEBUG = '--debug' in sys.argv
391
+
392
+ ws = TrazaFito()
393
+
394
+ ws.Username = 'testwservice'
395
+ ws.Password = 'testwservicepsw'
396
+
397
+ if '--prod' in sys.argv and not HOMO:
398
+ WSDL = "https://servicios.pami.org.ar/trazaagr.WebService?wsdl"
399
+ print("Usando WSDL:", WSDL)
400
+ sys.argv.pop(sys.argv.index("--prod"))
401
+
402
+ # Inicializo las variables y estructuras para el archivo de intercambio:
403
+ transaccion_dto = []
404
+ transacciones = []
405
+ errores = []
406
+ formatos = [('TransaccionDTO', TRANSACCION_DTO, transaccion_dto),
407
+ ('Transacciones', TRANSACCIONES, transacciones),
408
+ ('Errores', ERRORES, errores),
409
+ ]
410
+
411
+ if '--formato' in sys.argv:
412
+ print("Formato:")
413
+ for msg, formato, lista in formatos:
414
+ comienzo = 1
415
+ print("=== %s ===" % msg)
416
+ print("|| %-25s || %-12s || %-5s || %-4s || %-10s ||" % (
417
+ "Nombre", "Tipo", "Long.", "Pos(txt)", "Campo(dbf)"))
418
+ claves = []
419
+ for fmt in formato:
420
+ clave, longitud, tipo = fmt[0:3]
421
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
422
+ claves.append(clave_dbf)
423
+ print("|| %-25s || %-12s || %5d || %4d || %-10s ||" % (
424
+ clave, tipo, longitud, comienzo, clave_dbf))
425
+ comienzo += longitud
426
+ sys.exit(0)
427
+
428
+ if '--cargar' in sys.argv:
429
+ if '--dbf' in sys.argv:
430
+ leer_dbf(formatos[:1], {})
431
+ elif '--json' in sys.argv:
432
+ for formato in formatos[:1]:
433
+ archivo = open(formato[0].lower() + ".json", "r")
434
+ d = json.load(archivo)
435
+ formato[2].extend(d)
436
+ archivo.close()
437
+ else:
438
+ for formato in formatos[:1]:
439
+ archivo = open(formato[0].lower() + ".txt", "r")
440
+ for linea in archivo:
441
+ d = leer(linea, formato[1])
442
+ formato[2].append(d)
443
+ archivo.close()
444
+
445
+ ws.Conectar("", WSDL)
446
+
447
+ if ws.Excepcion:
448
+ print(ws.Excepcion)
449
+ print(ws.Traceback)
450
+ sys.exit(-1)
451
+
452
+ # Datos de pruebas:
453
+
454
+ if '--test' in sys.argv:
455
+ transaccion_dto.append(dict(
456
+ gln_origen="9876543210982", gln_destino="3692581473693",
457
+ f_operacion=datetime.datetime.now().strftime("%d/%m/%Y"),
458
+ f_elaboracion=datetime.datetime.now().strftime("%d/%m/%Y"),
459
+ f_vto=(datetime.datetime.now() + datetime.timedelta(30)).strftime("%d/%m/%Y"),
460
+ id_evento=11,
461
+ cod_producto="88900000000001",
462
+ n_cantidad=1,
463
+ n_serie=int(time.time() * 10),
464
+ n_lote=datetime.datetime.now().strftime("%Y"),
465
+ n_cai="123456789012345",
466
+ n_cae="",
467
+ id_motivo_destruccion=0,
468
+ n_manifiesto="",
469
+ en_transporte="N",
470
+ n_remito="1234",
471
+ motivo_devolucion="",
472
+ observaciones="prueba",
473
+ n_vale_compra="",
474
+ apellidoNombres="Juan Peres",
475
+ direccion="Saraza", numero="1234",
476
+ localidad="Hurlingham", provincia="Buenos Aires",
477
+ n_postal="1688",
478
+ cuit="20267565393",
479
+ codigo_transaccion=None,
480
+ ))
481
+
482
+ # Opciones principales:
483
+
484
+ if '--confirma' in sys.argv:
485
+ if '--loadxml' in sys.argv:
486
+ ws.LoadTestXML("trazamed_confirma.xml") # cargo respuesta
487
+ ok = ws.SendConfirmaTransacc(usuario="pruebasws", password="pruebasws",
488
+ p_ids_transac="1", f_operacion="31-12-2013")
489
+ if not ok:
490
+ raise RuntimeError(ws.Excepcion)
491
+ ws.SendConfirmaTransacc(*sys.argv[sys.argv.index("--confirma") + 1:])
492
+ elif '--alerta' in sys.argv:
493
+ ws.SendAlertaTransacc(*sys.argv[sys.argv.index("--alerta") + 1:])
494
+ elif '--cancela' in sys.argv:
495
+ ws.SendCancelaTransac(*sys.argv[sys.argv.index("--cancela") + 1:])
496
+ elif '--consulta' in sys.argv:
497
+ ws.GetTransacciones(
498
+ *sys.argv[sys.argv.index("--consulta") + 1:]
499
+ )
500
+ print("CantPaginas", ws.CantPaginas)
501
+ print("HayError", ws.HayError)
502
+ # print "TransaccionSenasa", ws.TransaccionSenasa
503
+ # parametros comunes de salida (columnas de la tabla):
504
+ claves = [k for k, v, l in TRANSACCIONES]
505
+ # extiendo la lista de resultado para el archivo de intercambio:
506
+ transacciones.extend(ws.TransaccionSenasa)
507
+ # encabezado de la tabla:
508
+ print("||", "||".join(["%s" % clave for clave in claves]), "||")
509
+ # recorro los datos devueltos (TransaccionSenasa):
510
+ while ws.LeerTransaccion():
511
+ for clave in claves:
512
+ print("||", ws.GetParametro(clave), end=' ') # imprimo cada fila
513
+ print("||")
514
+ else:
515
+ argv = [argv for argv in sys.argv if not argv.startswith("--")]
516
+ if not transaccion_dto:
517
+ if len(argv) > 10:
518
+ ws.SaveTransaccion(*argv[1:])
519
+ else:
520
+ print("ERROR: no se indicaron todos los parοΏ½metros requeridos")
521
+ elif transaccion_dto:
522
+ try:
523
+ usuario, password = argv[-2:]
524
+ except BaseException:
525
+ print("ADVERTENCIA: no se indico parοΏ½metros usuario y passoword")
526
+ usuario, password = "senasaws", "Clave2013"
527
+ for i, dto in enumerate(transaccion_dto):
528
+ print("Procesando registro", i)
529
+ del dto['codigo_transaccion']
530
+ ws.SaveTransaccion(usuario, password, **dto)
531
+ dto['codigo_transaccion'] = ws.CodigoTransaccion
532
+ errores.extend(ws.errores)
533
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
534
+ ws.Resultado,
535
+ ws.CodigoTransaccion,
536
+ '|'.join(ws.Errores or []),
537
+ ))
538
+ else:
539
+ print("ERROR: no se especificaron productos a informar")
540
+
541
+ if not transaccion_dto:
542
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
543
+ ws.Resultado,
544
+ ws.CodigoTransaccion,
545
+ '|'.join(ws.Errores or []),
546
+ ))
547
+
548
+ if ws.Excepcion:
549
+ print(ws.Traceback)
550
+
551
+ if '--grabar' in sys.argv:
552
+ if '--dbf' in sys.argv:
553
+ guardar_dbf(formatos, True, {})
554
+ elif '--json' in sys.argv:
555
+ for formato in formatos:
556
+ archivo = open(formato[0].lower() + ".json", "w")
557
+ json.dump(formato[2], archivo, sort_keys=True, indent=4)
558
+ archivo.close()
559
+ else:
560
+ for formato in formatos:
561
+ archivo = open(formato[0].lower() + ".txt", "w")
562
+ for it in formato[2]:
563
+ archivo.write(escribir(it, formato[1]))
564
+ archivo.close()
565
+
566
+
567
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
568
+ INSTALL_DIR = TrazaFito.InstallDir = get_install_dir()
569
+
570
+
571
+ if __name__ == '__main__':
572
+
573
+ # ajusto el encoding por defecto (si se redirije la salida)
574
+ if not hasattr(sys.stdout, "encoding") or sys.stdout.encoding is None:
575
+ import codecs
576
+ import locale
577
+ sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout, "replace")
578
+ sys.stderr = codecs.getwriter(locale.getpreferredencoding())(sys.stderr, "replace")
579
+
580
+ if '--register' in sys.argv or '--unregister' in sys.argv:
581
+ import pythoncom
582
+ import win32com.server.register
583
+ win32com.server.register.UseCommandLine(TrazaFito)
584
+ elif "/Automate" in sys.argv:
585
+ # MS seems to like /automate to run the class factories.
586
+ import win32com.server.localserver
587
+ # win32com.server.localserver.main()
588
+ # start the server.
589
+ win32com.server.localserver.serve([TrazaFito._reg_clsid_])
590
+ else:
591
+ main()
app/pyafipws/trazamed.py ADDED
@@ -0,0 +1,1041 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para Trazabilidad de Medicamentos ANMAT - PAMI - INSSJP Disp. 3683/11
14
+ segοΏ½n EspecificaciοΏ½n TοΏ½cnica para Pruebas de Servicios v2 (2013)"""
15
+
16
+ # InformaciοΏ½n adicional y documentaciοΏ½n:
17
+ # http://www.sistemasagiles.com.ar/trac/wiki/TrazabilidadMedicamentos
18
+
19
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
20
+ __copyright__ = "Copyright (C) 2011 Mariano Reingart"
21
+ __license__ = "GPL 3.0"
22
+ __version__ = "1.16c"
23
+
24
+ import os
25
+ import socket
26
+ import sys
27
+ import datetime
28
+ import time
29
+ import traceback
30
+ import pysimplesoap.client
31
+ from pysimplesoap.client import SoapClient, SoapFault, parse_proxy, \
32
+ set_http_wrapper
33
+ from pysimplesoap.simplexml import SimpleXMLElement
34
+ from io import StringIO
35
+
36
+ # importo funciones compartidas:
37
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, dar_nombre_campo_dbf, get_install_dir, BaseWS, inicializar_y_capturar_excepciones
38
+
39
+ HOMO = False
40
+ TYPELIB = False
41
+
42
+ WSDL = "https://servicios.pami.org.ar/trazamed.WebService?wsdl"
43
+ LOCATION = "https://servicios.pami.org.ar/trazamed.WebService"
44
+ #WSDL = "https://trazabilidad.pami.org.ar:9050/trazamed.WebService?wsdl"
45
+
46
+ # Formato de MedicamentosDTO, MedicamentosDTODHSerie, MedicamentosDTOFraccion
47
+ MEDICAMENTOS = [
48
+ ('f_evento', 10, A), # formato DD/MM/AAAA
49
+ ('h_evento', 5, A), # formato HH:MM
50
+ ('gln_origen', 13, A),
51
+ ('gln_destino', 13, A),
52
+ ('n_remito', 20, A),
53
+ ('n_factura', 20, A),
54
+ ('vencimiento', 10, A),
55
+ ('gtin', 14, A),
56
+ ('lote', 20, A),
57
+ ('numero_serial', 20, A),
58
+ ('desde_numero_serial', 20, A), # sendMedicamentosDHSerie
59
+ ('hasta_numero_serial', 20, A), # sendMedicamentosDHSerie
60
+ ('id_obra_social', 9, N),
61
+ ('id_evento', 3, N),
62
+ ('cuit_origen', 11, A),
63
+ ('cuit_destino', 11, A),
64
+ ('apellido', 50, A),
65
+ ('nombres', 100, A),
66
+ ('tipo_documento', 2, N), # 96: DNI,80: CUIT
67
+ ('n_documento', 10, A),
68
+ ('sexo', 1, A), # M o F
69
+ ('direccion', 100, A),
70
+ ('numero', 10, A),
71
+ ('piso', 5, A),
72
+ ('depto', 5, A),
73
+ ('localidad', 50, A),
74
+ ('provincia', 100, A),
75
+ ('n_postal', 8, A),
76
+ ('fecha_nacimiento', 100, A),
77
+ ('telefono', 30, A),
78
+ ('nro_asociado', 30, A),
79
+ ('cantidad', 3, N), # sendMedicamentosFraccion
80
+ ('codigo_transaccion', 14, A),
81
+ ]
82
+
83
+ # Formato para TransaccionPlainWS (getTransaccionesNoConfirmadas)
84
+ TRANSACCIONES = [
85
+ ('_id_transaccion', 14, A),
86
+ ('_id_transaccion_global', 14, A),
87
+ ('_f_evento', 10, A),
88
+ ('_f_transaccion', 16, A), # formato DD/MM/AAAA HH:MM
89
+ ('_gtin', 14, A),
90
+ ('_lote', 20, A),
91
+ ('_numero_serial', 20, A),
92
+ ('_nombre', 200, A),
93
+ ('_d_evento', 100, A),
94
+ ('_gln_origen', 13, A),
95
+ ('_razon_social_origen', 200, A),
96
+ ('_gln_destino', 13, A),
97
+ ('_razon_social_destino', 200, A),
98
+ ('_n_remito', 20, A),
99
+ ('_n_factura', 20, A),
100
+ ('_vencimiento', 10, A),
101
+ ('_id_evento', 3, N), # agregado el 30/01/2014
102
+ ]
103
+
104
+ # Formato para Errores
105
+ ERRORES = [
106
+ ('c_error', 4, A), # cοΏ½digo
107
+ ('d_error', 250, A), # descripciοΏ½n
108
+ ]
109
+
110
+
111
+ class TrazaMed(BaseWS):
112
+ "Interfaz para el WebService de Trazabilidad de Medicamentos ANMAT - PAMI - INSSJP"
113
+ _public_methods_ = ['SendMedicamentos',
114
+ 'SendCancelacTransacc', 'SendCancelacTransaccParcial',
115
+ 'SendMedicamentosDHSerie',
116
+ 'SendMedicamentosFraccion',
117
+ 'SendConfirmaTransacc', 'SendAlertaTransacc',
118
+ 'GetTransaccionesNoConfirmadas',
119
+ 'GetEnviosPropiosAlertados', 'GetConsultaStock',
120
+ 'GetTransaccionesWS', 'GetCatalogoElectronicoByGTIN',
121
+ 'Conectar', 'LeerError', 'LeerTransaccion',
122
+ 'SetUsername', 'SetPassword',
123
+ 'SetParametro', 'GetParametro',
124
+ 'GetCodigoTransaccion', 'GetResultado', 'LoadTestXML']
125
+
126
+ _public_attrs_ = [
127
+ 'Username', 'Password',
128
+ 'CodigoTransaccion', 'Errores', 'Resultado',
129
+ 'XmlRequest', 'XmlResponse',
130
+ 'Version', 'InstallDir',
131
+ 'Traceback', 'Excepcion', 'LanzarExcepciones',
132
+ 'CantPaginas', 'HayError', 'TransaccionPlainWS',
133
+ ]
134
+
135
+ _reg_progid_ = "TrazaMed"
136
+ _reg_clsid_ = "{8472867A-AE6F-487F-8554-C2C896CFFC3E}"
137
+
138
+ if TYPELIB:
139
+ _typelib_guid_ = '{F992EB7E-AFBD-41BB-B717-5693D3A2BADB}'
140
+ _typelib_version_ = 1, 4
141
+ _com_interfaces_ = ['ITrazaMed']
142
+
143
+ # Variables globales para BaseWS:
144
+ HOMO = HOMO
145
+ WSDL = WSDL
146
+ Version = "%s %s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '',
147
+ pysimplesoap.client.__version__)
148
+
149
+ def __init__(self, reintentos=1):
150
+ self.Username = self.Password = None
151
+ self.TransaccionPlainWS = []
152
+ BaseWS.__init__(self, reintentos)
153
+
154
+ def inicializar(self):
155
+ BaseWS.inicializar(self)
156
+ self.CodigoTransaccion = self.Errores = self.Resultado = None
157
+ self.Resultado = ''
158
+ self.Errores = [] # lista de strings para la interfaz
159
+ self.errores = [] # lista de diccionarios (uso interno)
160
+ self.CantPaginas = self.HayError = None
161
+
162
+ def __analizar_errores(self, ret):
163
+ "Comprueba y extrae errores si existen en la respuesta XML"
164
+ self.errores = ret.get('errores', [])
165
+ self.Errores = ["%s: %s" % (it['_c_error'], it['_d_error'])
166
+ for it in ret.get('errores', [])]
167
+ self.Resultado = ret.get('resultado')
168
+
169
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30):
170
+ # Conecto usando el mοΏ½todo estandard:
171
+ ok = BaseWS.Conectar(self, cache, wsdl, proxy, wrapper, cacert, timeout,
172
+ soap_server="jetty")
173
+
174
+ if ok:
175
+ # si el archivo es local, asumo que ya esta corregido:
176
+ if not self.wsdl.startswith("file"):
177
+ # corrijo ubicaciοΏ½n del servidor (localhost:9050 en el WSDL)
178
+ location = self.wsdl[:-5]
179
+ if 'IWebServiceService' in self.client.services:
180
+ ws = self.client.services['IWebServiceService'] # version 1
181
+ else:
182
+ ws = self.client.services['IWebService'] # version 2
183
+ ws['ports']['IWebServicePort']['location'] = location
184
+
185
+ # Establecer credenciales de seguridad:
186
+ self.client['wsse:Security'] = {
187
+ 'wsse:UsernameToken': {
188
+ 'wsse:Username': self.Username,
189
+ 'wsse:Password': self.Password,
190
+ }
191
+ }
192
+ return ok
193
+
194
+ @inicializar_y_capturar_excepciones
195
+ def SendMedicamentos(self, usuario, password,
196
+ f_evento, h_evento, gln_origen, gln_destino,
197
+ n_remito, n_factura, vencimiento, gtin, lote,
198
+ numero_serial, id_obra_social, id_evento,
199
+ cuit_origen='', cuit_destino='', apellido='', nombres='',
200
+ tipo_documento='', n_documento='', sexo='',
201
+ direccion='', numero='', piso='', depto='', localidad='', provincia='',
202
+ n_postal='', fecha_nacimiento='', telefono='',
203
+ nro_asociado=None,
204
+ ):
205
+ "Realiza el registro de una transacciοΏ½n de medicamentos. "
206
+ # creo los parοΏ½metros para esta llamada
207
+ params = {'f_evento': f_evento,
208
+ 'h_evento': h_evento,
209
+ 'gln_origen': gln_origen,
210
+ 'gln_destino': gln_destino,
211
+ 'n_remito': n_remito,
212
+ 'n_factura': n_factura,
213
+ 'vencimiento': vencimiento,
214
+ 'gtin': gtin,
215
+ 'lote': lote,
216
+ 'numero_serial': numero_serial,
217
+ 'id_obra_social': id_obra_social or None,
218
+ 'id_evento': id_evento,
219
+ 'cuit_origen': cuit_origen,
220
+ 'cuit_destino': cuit_destino,
221
+ 'apellido': apellido,
222
+ 'nombres': nombres,
223
+ 'tipo_documento': tipo_documento,
224
+ 'n_documento': n_documento,
225
+ 'sexo': sexo,
226
+ 'direccion': direccion,
227
+ 'numero': numero,
228
+ 'piso': piso,
229
+ 'depto': depto,
230
+ 'localidad': localidad,
231
+ 'provincia': provincia,
232
+ 'n_postal': n_postal,
233
+ 'fecha_nacimiento': fecha_nacimiento,
234
+ 'telefono': telefono,
235
+ 'nro_asociado': nro_asociado,
236
+ }
237
+ res = self.client.sendMedicamentos(
238
+ arg0=params,
239
+ arg1=usuario,
240
+ arg2=password,
241
+ )
242
+
243
+ ret = res['return']
244
+
245
+ self.CodigoTransaccion = ret['codigoTransaccion']
246
+ self.__analizar_errores(ret)
247
+
248
+ return True
249
+
250
+ @inicializar_y_capturar_excepciones
251
+ def SendMedicamentosFraccion(self, usuario, password,
252
+ f_evento, h_evento, gln_origen, gln_destino,
253
+ n_remito, n_factura, vencimiento, gtin, lote,
254
+ numero_serial, id_obra_social, id_evento,
255
+ cuit_origen='', cuit_destino='', apellido='', nombres='',
256
+ tipo_documento='', n_documento='', sexo='',
257
+ direccion='', numero='', piso='', depto='', localidad='', provincia='',
258
+ n_postal='', fecha_nacimiento='', telefono='',
259
+ nro_asociado=None, cantidad=None,
260
+ ):
261
+ "Realiza el registro de una transacciοΏ½n de medicamentos fraccionados"
262
+ # creo los parοΏ½metros para esta llamada
263
+ params = {'f_evento': f_evento,
264
+ 'h_evento': h_evento,
265
+ 'gln_origen': gln_origen,
266
+ 'gln_destino': gln_destino,
267
+ 'n_remito': n_remito,
268
+ 'n_factura': n_factura,
269
+ 'vencimiento': vencimiento,
270
+ 'gtin': gtin,
271
+ 'lote': lote,
272
+ 'numero_serial': numero_serial,
273
+ 'id_obra_social': id_obra_social or None,
274
+ 'id_evento': id_evento,
275
+ 'cuit_origen': cuit_origen,
276
+ 'cuit_destino': cuit_destino,
277
+ 'apellido': apellido,
278
+ 'nombres': nombres,
279
+ 'tipo_documento': tipo_documento,
280
+ 'n_documento': n_documento,
281
+ 'sexo': sexo,
282
+ 'direccion': direccion,
283
+ 'numero': numero,
284
+ 'piso': piso,
285
+ 'depto': depto,
286
+ 'localidad': localidad,
287
+ 'provincia': provincia,
288
+ 'n_postal': n_postal,
289
+ 'fecha_nacimiento': fecha_nacimiento,
290
+ 'telefono': telefono,
291
+ 'nro_asociado': nro_asociado,
292
+ 'cantidad': cantidad,
293
+ }
294
+ res = self.client.sendMedicamentosFraccion(
295
+ arg0=params,
296
+ arg1=usuario,
297
+ arg2=password,
298
+ )
299
+
300
+ ret = res['return']
301
+
302
+ self.CodigoTransaccion = ret['codigoTransaccion']
303
+ self.__analizar_errores(ret)
304
+
305
+ return True
306
+
307
+ @inicializar_y_capturar_excepciones
308
+ def SendMedicamentosDHSerie(self, usuario, password,
309
+ f_evento, h_evento, gln_origen, gln_destino,
310
+ n_remito, n_factura, vencimiento, gtin, lote,
311
+ desde_numero_serial, hasta_numero_serial,
312
+ id_obra_social, id_evento,
313
+ cuit_origen='', cuit_destino='', apellido='', nombres='',
314
+ tipo_documento='', n_documento='', sexo='',
315
+ direccion='', numero='', piso='', depto='', localidad='', provincia='',
316
+ n_postal='', fecha_nacimiento='', telefono='',
317
+ nro_asociado=None,
318
+ ):
319
+ "EnvοΏ½a un lote de medicamentos informando el desde-hasta nοΏ½mero de serie"
320
+ # creo los parοΏ½metros para esta llamada
321
+ params = {'f_evento': f_evento,
322
+ 'h_evento': h_evento,
323
+ 'gln_origen': gln_origen,
324
+ 'gln_destino': gln_destino,
325
+ 'n_remito': n_remito,
326
+ 'n_factura': n_factura,
327
+ 'vencimiento': vencimiento,
328
+ 'gtin': gtin,
329
+ 'lote': lote,
330
+ 'desde_numero_serial': desde_numero_serial,
331
+ 'hasta_numero_serial': hasta_numero_serial,
332
+ 'id_obra_social': id_obra_social or None,
333
+ 'id_evento': id_evento,
334
+ 'cuit_origen': cuit_origen,
335
+ 'cuit_destino': cuit_destino,
336
+ 'apellido': apellido,
337
+ 'nombres': nombres,
338
+ 'tipo_documento': tipo_documento,
339
+ 'n_documento': n_documento,
340
+ 'sexo': sexo,
341
+ 'direccion': direccion,
342
+ 'numero': numero,
343
+ 'piso': piso,
344
+ 'depto': depto,
345
+ 'localidad': localidad,
346
+ 'provincia': provincia,
347
+ 'n_postal': n_postal,
348
+ 'fecha_nacimiento': fecha_nacimiento,
349
+ 'telefono': telefono,
350
+ 'nro_asociado': nro_asociado,
351
+ }
352
+ res = self.client.sendMedicamentosDHSerie(
353
+ arg0=params,
354
+ arg1=usuario,
355
+ arg2=password,
356
+ )
357
+
358
+ ret = res['return']
359
+
360
+ self.CodigoTransaccion = ret['codigoTransaccion']
361
+ self.__analizar_errores(ret)
362
+
363
+ return True
364
+
365
+ @inicializar_y_capturar_excepciones
366
+ def SendCancelacTransacc(self, usuario, password, codigo_transaccion):
367
+ " Realiza la cancelaciοΏ½n de una transacciοΏ½n"
368
+ res = self.client.sendCancelacTransacc(
369
+ arg0=codigo_transaccion,
370
+ arg1=usuario,
371
+ arg2=password,
372
+ )
373
+
374
+ ret = res['return']
375
+
376
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
377
+ self.__analizar_errores(ret)
378
+
379
+ return True
380
+
381
+ @inicializar_y_capturar_excepciones
382
+ def SendCancelacTransaccParcial(self, usuario, password, codigo_transaccion,
383
+ gtin_medicamento=None, numero_serial=None):
384
+ " Realiza la cancelaciοΏ½n parcial de una transacciοΏ½n"
385
+ res = self.client.sendCancelacTransaccParcial(
386
+ arg0=codigo_transaccion,
387
+ arg1=usuario,
388
+ arg2=password,
389
+ arg3=gtin_medicamento,
390
+ arg4=numero_serial,
391
+ )
392
+ ret = res['return']
393
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
394
+ self.__analizar_errores(ret)
395
+ return True
396
+
397
+ @inicializar_y_capturar_excepciones
398
+ def SendConfirmaTransacc(self, usuario, password, p_ids_transac, f_operacion):
399
+ "Confirma la recepciοΏ½n de un medicamento"
400
+ res = self.client.sendConfirmaTransacc(
401
+ arg0=usuario,
402
+ arg1=password,
403
+ arg2={'p_ids_transac': p_ids_transac, 'f_operacion': f_operacion},
404
+ )
405
+ ret = res['return']
406
+ self.CodigoTransaccion = ret.get('id_transac_asociada')
407
+ self.__analizar_errores(ret)
408
+ return True
409
+
410
+ @inicializar_y_capturar_excepciones
411
+ def SendAlertaTransacc(self, usuario, password, p_ids_transac_ws):
412
+ "Alerta un medicamento, acciοΏ½n contraria a οΏ½confirmar la transacciοΏ½nοΏ½."
413
+ res = self.client.sendAlertaTransacc(
414
+ arg0=usuario,
415
+ arg1=password,
416
+ arg2=p_ids_transac_ws,
417
+ )
418
+ ret = res['return']
419
+ self.CodigoTransaccion = ret.get('id_transac_asociada')
420
+ self.__analizar_errores(ret)
421
+ return True
422
+
423
+ @inicializar_y_capturar_excepciones
424
+ def GetTransaccionesNoConfirmadas(self, usuario, password,
425
+ p_id_transaccion_global=None, id_agente_informador=None,
426
+ id_agente_origen=None, id_agente_destino=None,
427
+ id_medicamento=None, id_evento=None,
428
+ fecha_desde_op=None, fecha_hasta_op=None,
429
+ fecha_desde_t=None, fecha_hasta_t=None,
430
+ fecha_desde_v=None, fecha_hasta_v=None,
431
+ n_remito=None, n_factura=None,
432
+ estado=None, lote=None, numero_serial=None,
433
+ ):
434
+ "Trae un listado de las transacciones que no estοΏ½n confirmadas"
435
+
436
+ # preparo los parametros de entrada opcionales:
437
+ kwargs = {}
438
+ if p_id_transaccion_global is not None:
439
+ kwargs['arg2'] = p_id_transaccion_global
440
+ if id_agente_informador is not None:
441
+ kwargs['arg3'] = id_agente_informador
442
+ if id_agente_origen is not None:
443
+ kwargs['arg4'] = id_agente_origen
444
+ if id_agente_destino is not None:
445
+ kwargs['arg5'] = id_agente_destino
446
+ if id_medicamento is not None:
447
+ kwargs['arg6'] = id_medicamento
448
+ if id_evento is not None:
449
+ kwargs['arg7'] = id_evento
450
+ if fecha_desde_op is not None:
451
+ kwargs['arg8'] = fecha_desde_op
452
+ if fecha_hasta_op is not None:
453
+ kwargs['arg9'] = fecha_hasta_op
454
+ if fecha_desde_t is not None:
455
+ kwargs['arg10'] = fecha_desde_t
456
+ if fecha_hasta_t is not None:
457
+ kwargs['arg11'] = fecha_hasta_t
458
+ if fecha_desde_v is not None:
459
+ kwargs['arg12'] = fecha_desde_v
460
+ if fecha_hasta_v is not None:
461
+ kwargs['arg13'] = fecha_hasta_v
462
+ if n_remito is not None:
463
+ kwargs['arg14'] = n_remito
464
+ if n_factura is not None:
465
+ kwargs['arg15'] = n_factura
466
+ if estado is not None:
467
+ kwargs['arg16'] = estado
468
+ if lote is not None:
469
+ kwargs['arg17'] = lote
470
+ if numero_serial is not None:
471
+ kwargs['arg18'] = numero_serial
472
+
473
+ # llamo al webservice
474
+ res = self.client.getTransaccionesNoConfirmadas(
475
+ arg0=usuario,
476
+ arg1=password,
477
+ **kwargs
478
+ )
479
+ ret = res['return']
480
+ if ret:
481
+ self.__analizar_errores(ret)
482
+ self.CantPaginas = ret.get('cantPaginas')
483
+ self.HayError = ret.get('hay_error')
484
+ self.TransaccionPlainWS = [it for it in ret.get('list', [])]
485
+ return True
486
+
487
+ def LeerTransaccion(self):
488
+ "Recorro TransaccionPlainWS devuelto por GetTransaccionesNoConfirmadas"
489
+ # usar GetParametro para consultar el valor retornado por el webservice
490
+
491
+ if self.TransaccionPlainWS:
492
+ # extraigo el primer item
493
+ self.params_out = self.TransaccionPlainWS.pop(0)
494
+ return True
495
+ else:
496
+ # limpio los parοΏ½metros
497
+ self.params_out = {}
498
+ return False
499
+
500
+ def LeerError(self):
501
+ "Recorro los errores devueltos y devuelvo el primero si existe"
502
+
503
+ if self.Errores:
504
+ # extraigo el primer item
505
+ er = self.Errores.pop(0)
506
+ return er
507
+ else:
508
+ return ""
509
+
510
+ @inicializar_y_capturar_excepciones
511
+ def GetEnviosPropiosAlertados(self, usuario, password,
512
+ p_id_transaccion_global=None, id_agente_informador=None,
513
+ id_agente_origen=None, id_agente_destino=None,
514
+ id_medicamento=None, id_evento=None,
515
+ fecha_desde_op=None, fecha_hasta_op=None,
516
+ fecha_desde_t=None, fecha_hasta_t=None,
517
+ fecha_desde_v=None, fecha_hasta_v=None,
518
+ n_remito=None, n_factura=None,
519
+ ):
520
+ "Obtiene las distribuciones y envοΏ½os propios que han sido alertados"
521
+
522
+ # preparo los parametros de entrada opcionales:
523
+ kwargs = {}
524
+ if p_id_transaccion_global is not None:
525
+ kwargs['arg2'] = p_id_transaccion_global
526
+ if id_agente_informador is not None:
527
+ kwargs['arg3'] = id_agente_informador
528
+ if id_agente_origen is not None:
529
+ kwargs['arg4'] = id_agente_origen
530
+ if id_agente_destino is not None:
531
+ kwargs['arg5'] = id_agente_destino
532
+ if id_medicamento is not None:
533
+ kwargs['arg6'] = id_medicamento
534
+ if id_evento is not None:
535
+ kwargs['arg7'] = id_evento
536
+ if fecha_desde_op is not None:
537
+ kwargs['arg8'] = fecha_desde_op
538
+ if fecha_hasta_op is not None:
539
+ kwargs['arg9'] = fecha_hasta_op
540
+ if fecha_desde_t is not None:
541
+ kwargs['arg10'] = fecha_desde_t
542
+ if fecha_hasta_t is not None:
543
+ kwargs['arg11'] = fecha_hasta_t
544
+ if fecha_desde_v is not None:
545
+ kwargs['arg12'] = fecha_desde_v
546
+ if fecha_hasta_v is not None:
547
+ kwargs['arg13'] = fecha_hasta_v
548
+ if n_remito is not None:
549
+ kwargs['arg14'] = n_remito
550
+ if n_factura is not None:
551
+ kwargs['arg15'] = n_factura
552
+
553
+ # llamo al webservice
554
+ res = self.client.getEnviosPropiosAlertados(
555
+ arg0=usuario,
556
+ arg1=password,
557
+ **kwargs
558
+ )
559
+ ret = res['return']
560
+ if ret:
561
+ self.__analizar_errores(ret)
562
+ self.CantPaginas = ret.get('cantPaginas')
563
+ self.HayError = ret.get('hay_error')
564
+ self.TransaccionPlainWS = [it for it in ret.get('list', [])]
565
+ return True
566
+
567
+ @inicializar_y_capturar_excepciones
568
+ def GetTransaccionesWS(self, usuario, password,
569
+ p_id_transaccion_global=None,
570
+ id_agente_origen=None, id_agente_destino=None,
571
+ id_medicamento=None, id_evento=None,
572
+ fecha_desde_op=None, fecha_hasta_op=None,
573
+ fecha_desde_t=None, fecha_hasta_t=None,
574
+ fecha_desde_v=None, fecha_hasta_v=None,
575
+ n_remito=None, n_factura=None,
576
+ id_estado=None, nro_pag=None,
577
+ ):
578
+ "Obtiene los movimientos realizados y permite filtros de bοΏ½squeda"
579
+
580
+ # preparo los parametros de entrada opcionales:
581
+ kwargs = {}
582
+ if p_id_transaccion_global is not None:
583
+ kwargs['arg2'] = p_id_transaccion_global
584
+ if id_agente_origen is not None:
585
+ kwargs['arg3'] = id_agente_origen
586
+ if id_agente_destino is not None:
587
+ kwargs['arg4'] = id_agente_destino
588
+ if id_medicamento is not None:
589
+ kwargs['arg5'] = id_medicamento
590
+ if id_evento is not None:
591
+ kwargs['arg6'] = id_evento
592
+ if fecha_desde_op is not None:
593
+ kwargs['arg7'] = fecha_desde_op
594
+ if fecha_hasta_op is not None:
595
+ kwargs['arg8'] = fecha_hasta_op
596
+ if fecha_desde_t is not None:
597
+ kwargs['arg9'] = fecha_desde_t
598
+ if fecha_hasta_t is not None:
599
+ kwargs['arg10'] = fecha_hasta_t
600
+ if fecha_desde_v is not None:
601
+ kwargs['arg11'] = fecha_desde_v
602
+ if fecha_hasta_v is not None:
603
+ kwargs['arg12'] = fecha_hasta_v
604
+ if n_remito is not None:
605
+ kwargs['arg13'] = n_remito
606
+ if n_factura is not None:
607
+ kwargs['arg14'] = n_factura
608
+ if id_estado is not None:
609
+ kwargs['arg15'] = id_estado
610
+ if nro_pag is not None:
611
+ kwargs['arg16'] = nro_pag
612
+
613
+ # llamo al webservice
614
+ res = self.client.getTransaccionesWS(
615
+ arg0=usuario,
616
+ arg1=password,
617
+ **kwargs
618
+ )
619
+ ret = res['return']
620
+ if ret:
621
+ self.__analizar_errores(ret)
622
+ self.CantPaginas = ret.get('cantPaginas')
623
+ self.HayError = ret.get('hay_error')
624
+ self.TransaccionPlainWS = [it for it in ret.get('list', [])]
625
+ return True
626
+
627
+ @inicializar_y_capturar_excepciones
628
+ def GetCatalogoElectronicoByGTIN(self, usuario, password,
629
+ cuit_fabricante=None, gtin=None, descripcion=None,
630
+ id_monodroga=None,
631
+ ):
632
+ "Obtiene el CatοΏ½logo ElectrοΏ½nico de Medicamentos"
633
+
634
+ # preparo los parametros de entrada opcionales:
635
+ kwargs = {}
636
+ if cuit_fabricante is not None:
637
+ kwargs['arg2'] = cuit_fabricante
638
+ if gtin is not None:
639
+ kwargs['arg3'] = gtin
640
+ if descripcion is not None:
641
+ kwargs['arg4'] = descripcion
642
+ if id_monodroga is not None:
643
+ kwargs['arg5'] = id_monodroga
644
+
645
+ # llamo al webservice
646
+ res = self.client.getCatalogoElectronicoByGTIN(
647
+ arg0=usuario,
648
+ arg1=password,
649
+ **kwargs
650
+ )
651
+ ret = res['return']
652
+ if ret:
653
+ self.__analizar_errores(ret)
654
+ self.CantPaginas = ret.get('cantPaginas')
655
+ self.HayError = ret.get('hay_error')
656
+ self.params_out = dict([(i, it) for i, it
657
+ in enumerate(ret.get('list', []))])
658
+ return len(self.params_out)
659
+ else:
660
+ return 0
661
+
662
+ @inicializar_y_capturar_excepciones
663
+ def GetConsultaStock(self, usuario, password,
664
+ id_medicamento=None, id_agente=None, descripcion=None,
665
+ cantidad=None, presentacion=None,
666
+ lote=None, numero_serial=None,
667
+ nro_pag=1, cant_reg=100,
668
+ ):
669
+ "Permite consultar el stock actual del agente."
670
+
671
+ # preparo los parametros de entrada opcionales:
672
+ kwargs = {}
673
+ if id_medicamento is not None:
674
+ kwargs['arg2'] = id_medicamento
675
+ if id_agente is not None:
676
+ kwargs['arg3'] = id_agente
677
+ if descripcion is not None:
678
+ kwargs['arg4'] = descripcion
679
+ if cantidad is not None:
680
+ kwargs['arg5'] = cantidad
681
+ if presentacion is not None:
682
+ kwargs['arg6'] = presentacion
683
+ if lote is not None:
684
+ kwargs['arg7'] = lote
685
+ if numero_serial is not None:
686
+ kwargs['arg8'] = numero_serial
687
+ if nro_pag is not None:
688
+ kwargs['arg9'] = nro_pag
689
+ if cant_reg is not None:
690
+ kwargs['arg10'] = cant_reg
691
+
692
+ # llamo al webservice
693
+ res = self.client.getConsultaStock(
694
+ arg0=usuario,
695
+ arg1=password,
696
+ **kwargs
697
+ )
698
+ ret = res['return']
699
+ if ret:
700
+ self.__analizar_errores(ret)
701
+ self.CantPaginas = ret.get('cantPaginas')
702
+ self.HayError = ret.get('hay_error')
703
+ self.params_out = dict([(i, it) for i, it
704
+ in enumerate(ret.get('list', []))])
705
+ return len(self.params_out)
706
+ else:
707
+ return 0
708
+
709
+ def SetUsername(self, username):
710
+ "Establezco el nombre de usuario"
711
+ self.Username = username
712
+
713
+ def SetPassword(self, password):
714
+ "Establezco la contraseοΏ½a"
715
+ self.Password = password
716
+
717
+ def GetCodigoTransaccion(self):
718
+ "Devuelvo el cοΏ½digo de transacciοΏ½n"
719
+ return self.CodigoTransaccion
720
+
721
+ def GetResultado(self):
722
+ "Devuelvo el resultado"
723
+ return self.Resultado
724
+
725
+
726
+ def main():
727
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
728
+ import os
729
+ import time
730
+ import sys
731
+ global WSDL, LOCATION
732
+
733
+ DEBUG = '--debug' in sys.argv
734
+
735
+ ws = TrazaMed()
736
+
737
+ ws.Username = 'testwservice'
738
+ ws.Password = 'testwservicepsw'
739
+
740
+ if '--prod' in sys.argv and not HOMO:
741
+ WSDL = "https://trazabilidad.pami.org.ar:9050/trazamed.WebService"
742
+ print("Usando WSDL:", WSDL)
743
+ sys.argv.pop(sys.argv.index("--prod"))
744
+
745
+ # Inicializo las variables y estructuras para el archivo de intercambio:
746
+ medicamentos = []
747
+ transacciones = []
748
+ errores = []
749
+ formatos = [('Medicamentos', MEDICAMENTOS, medicamentos),
750
+ ('Transacciones', TRANSACCIONES, transacciones),
751
+ ('Errores', ERRORES, errores),
752
+ ]
753
+
754
+ if '--formato' in sys.argv:
755
+ print("Formato:")
756
+ for msg, formato, lista in formatos:
757
+ comienzo = 1
758
+ print("=== %s ===" % msg)
759
+ print("|| %-25s || %-12s || %-5s || %-4s || %-10s ||" % (
760
+ "Nombre", "Tipo", "Long.", "Pos(txt)", "Campo(dbf)"))
761
+ claves = []
762
+ for fmt in formato:
763
+ clave, longitud, tipo = fmt[0:3]
764
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
765
+ claves.append(clave_dbf)
766
+ print("|| %-25s || %-12s || %5d || %4d || %-10s ||" % (
767
+ clave, tipo, longitud, comienzo, clave_dbf))
768
+ comienzo += longitud
769
+ sys.exit(0)
770
+
771
+ if '--cargar' in sys.argv:
772
+ if '--dbf' in sys.argv:
773
+ leer_dbf(formatos[:1], {})
774
+ elif '--json' in sys.argv:
775
+ for formato in formatos[:1]:
776
+ archivo = open(formato[0].lower() + ".json", "r")
777
+ d = json.load(archivo)
778
+ formato[2].extend(d)
779
+ archivo.close()
780
+ else:
781
+ for formato, campos, lista in formatos[:1]:
782
+ archivo = open(formato.lower() + ".txt", "r")
783
+ for linea in archivo:
784
+ d = leer(linea, campos)
785
+ lista.append(d)
786
+ archivo.close()
787
+ if DEBUG:
788
+ for campo in campos:
789
+ print(campo[0], "=", lista[0][campo[0]])
790
+
791
+ ws.Conectar("", WSDL)
792
+
793
+ if ws.Excepcion:
794
+ print(ws.Excepcion)
795
+ print(ws.Traceback)
796
+ sys.exit(-1)
797
+
798
+ # Datos de pruebas:
799
+
800
+ if '--test' in sys.argv:
801
+ medicamentos.append(dict(
802
+ f_evento=datetime.datetime.now().strftime("%d/%m/%Y"),
803
+ h_evento=datetime.datetime.now().strftime("%H:%M"),
804
+ gln_origen="9999999999918", gln_destino="glnws",
805
+ n_remito="R000100001234", n_factura="A000100001234",
806
+ vencimiento=(datetime.datetime.now() + datetime.timedelta(30)).strftime("%d/%m/%Y"),
807
+ gtin="GTIN1", lote=datetime.datetime.now().strftime("%Y"),
808
+ numero_serial=int(time.time() * 10),
809
+ id_obra_social=None, id_evento=134,
810
+ cuit_origen="20267565393", cuit_destino="20267565393",
811
+ apellido="Reingart", nombres="Mariano",
812
+ tipo_documento="96", n_documento="26756539", sexo="M",
813
+ direccion="Saraza", numero="1234", piso="", depto="",
814
+ localidad="Hurlingham", provincia="Buenos Aires",
815
+ n_postal="1688", fecha_nacimiento="01/01/2000",
816
+ telefono="5555-5555",
817
+ nro_asociado="9999999999999",
818
+ cantidad=None,
819
+ desde_numero_serial=None, hasta_numero_serial=None,
820
+ codigo_transaccion=None,
821
+ ))
822
+ if '--testfraccion' in sys.argv:
823
+ medicamentos.append(dict(
824
+ f_evento=datetime.datetime.now().strftime("%d/%m/%Y"),
825
+ h_evento=datetime.datetime.now().strftime("%H:%M"),
826
+ gln_origen="9999999999918", gln_destino="glnws",
827
+ n_remito="1234", n_factura="1234",
828
+ vencimiento=(datetime.datetime.now() + datetime.timedelta(30)).strftime("%d/%m/%Y"),
829
+ gtin="GTIN1", lote=datetime.datetime.now().strftime("%Y"),
830
+ numero_serial=int(time.time() * 10),
831
+ id_obra_social=None, id_evento=134,
832
+ cuit_origen="20267565393", cuit_destino="20267565393",
833
+ apellido="Reingart", nombres="Mariano",
834
+ tipo_documento="96", n_documento="26756539", sexo="M",
835
+ direccion="Saraza", numero="1234", piso="", depto="",
836
+ localidad="Hurlingham", provincia="Buenos Aires",
837
+ n_postal="1688", fecha_nacimiento="01/01/2000",
838
+ telefono="5555-5555",
839
+ nro_asociado="9999999999999",
840
+ cantidad=5,
841
+ desde_numero_serial=None, hasta_numero_serial=None,
842
+ codigo_transaccion=None,
843
+ ))
844
+ if '--testdh' in sys.argv:
845
+ medicamentos.append(dict(
846
+ f_evento=datetime.datetime.now().strftime("%d/%m/%Y"),
847
+ h_evento=datetime.datetime.now().strftime("%H:%M"),
848
+ gln_origen="9999999999918", gln_destino="glnws",
849
+ n_remito="1234", n_factura="1234",
850
+ vencimiento=(datetime.datetime.now() + datetime.timedelta(30)).strftime("%d/%m/%Y"),
851
+ gtin="GTIN1", lote=datetime.datetime.now().strftime("%Y"),
852
+ desde_numero_serial=int(time.time() * 10) - 1,
853
+ hasta_numero_serial=int(time.time() * 10) + 1,
854
+ id_obra_social=None, id_evento=134,
855
+ nro_asociado="1234",
856
+ cantidad=None, numero_serial=None,
857
+ codigo_transaccion=None,
858
+ ))
859
+
860
+ # Opciones principales:
861
+
862
+ if '--cancela' in sys.argv:
863
+ if '--loadxml' in sys.argv:
864
+ ws.LoadTestXML("trazamed_cancela_err.xml") # cargo respuesta
865
+ ws.SendCancelacTransacc(*sys.argv[sys.argv.index("--cancela") + 1:])
866
+ elif '--cancela_parcial' in sys.argv:
867
+ ws.SendCancelacTransaccParcial(*sys.argv[sys.argv.index("--cancela_parcial") + 1:])
868
+ elif '--confirma' in sys.argv:
869
+ if '--loadxml' in sys.argv:
870
+ ws.LoadTestXML("trazamed_confirma.xml") # cargo respuesta
871
+ ok = ws.SendConfirmaTransacc(usuario="pruebasws", password="pruebasws",
872
+ p_ids_transac="1", f_operacion="31-12-2013")
873
+ if not ok:
874
+ raise RuntimeError(ws.Excepcion)
875
+ ws.SendConfirmaTransacc(*sys.argv[sys.argv.index("--confirma") + 1:])
876
+ elif '--alerta' in sys.argv:
877
+ ws.SendAlertaTransacc(*sys.argv[sys.argv.index("--alerta") + 1:])
878
+ elif '--consulta' in sys.argv:
879
+ if '--alertados' in sys.argv:
880
+ ws.GetEnviosPropiosAlertados(
881
+ *sys.argv[sys.argv.index("--alertados") + 1:]
882
+ )
883
+ elif '--movimientos' in sys.argv:
884
+ ws.GetTransaccionesWS(
885
+ *sys.argv[sys.argv.index("--movimientos") + 1:]
886
+ )
887
+ else:
888
+ ws.GetTransaccionesNoConfirmadas(
889
+ *sys.argv[sys.argv.index("--consulta") + 1:]
890
+ # usuario="pruebasws", password="pruebasws",
891
+ # p_id_transaccion_global="1234",
892
+ # id_agente_informador="1",
893
+ # id_agente_origen="1",
894
+ # id_agente_destino="1",
895
+ # id_medicamento="1",
896
+ # id_evento="1",
897
+ # fecha_desde_op="01/01/2015",
898
+ # fecha_hasta_op="31/12/2013",
899
+ # fecha_desde_t="01/01/2013",
900
+ # fecha_hasta_t="31/12/2013",
901
+ # fecha_desde_v="01/04/2013",
902
+ # fecha_hasta_v="30/04/2013",
903
+ # n_factura=5, n_remito=6,
904
+ # estado=1,
905
+ # lote=88745,
906
+ # numero_serial=894124788,
907
+ )
908
+ print("CantPaginas", ws.CantPaginas)
909
+ print("HayError", ws.HayError)
910
+ # print "TransaccionPlainWS", ws.TransaccionPlainWS
911
+ # parametros comunes de salida (columnas de la tabla):
912
+ claves = [k for k, v, l in TRANSACCIONES]
913
+ # extiendo la lista de resultado para el archivo de intercambio:
914
+ transacciones.extend(ws.TransaccionPlainWS)
915
+ # encabezado de la tabla:
916
+ print("||", "||".join(["%s" % clave for clave in claves]), "||")
917
+ # recorro los datos devueltos (TransaccionPlainWS):
918
+ while ws.LeerTransaccion():
919
+ for clave in claves:
920
+ print("||", ws.GetParametro(clave), end=' ') # imprimo cada fila
921
+ print("||")
922
+ elif '--catalogo' in sys.argv:
923
+ ret = ws.GetCatalogoElectronicoByGTIN(
924
+ *sys.argv[sys.argv.index("--catalogo") + 1:]
925
+ )
926
+ for catalogo in list(ws.params_out.values()):
927
+ print(catalogo) # imprimo cada fila
928
+ elif '--stock' in sys.argv:
929
+ ret = ws.GetConsultaStock(
930
+ *sys.argv[sys.argv.index("--stock") + 1:]
931
+ )
932
+ print("\n".join([str(s) for s in list(ws.params_out.values())]))
933
+ else:
934
+ argv = [argv for argv in sys.argv if not argv.startswith("--")]
935
+ if not medicamentos:
936
+ if len(argv) > 16:
937
+ if '--dh' in sys.argv:
938
+ ws.SendMedicamentosDHSerie(*argv[1:])
939
+ elif '--fraccion' in sys.argv:
940
+ ws.SendMedicamentosFraccion(*argv[1:])
941
+ else:
942
+ ws.SendMedicamentos(*argv[1:])
943
+ else:
944
+ print("ERROR: no se indicaron todos los parοΏ½metros requeridos")
945
+ elif medicamentos:
946
+ try:
947
+ usuario, password = argv[1:3]
948
+ except BaseException:
949
+ print("ADVERTENCIA: no se indico parοΏ½metros usuario y passoword")
950
+ usuario = password = "pruebasws"
951
+ for i, med in enumerate(medicamentos):
952
+ print("Procesando registro", i)
953
+ del med['codigo_transaccion']
954
+ if med.get("cantidad"):
955
+ del med["desde_numero_serial"]
956
+ del med["hasta_numero_serial"]
957
+ ws.SendMedicamentosFraccion(usuario, password, **med)
958
+ elif med.get("desde_numero_serial"):
959
+ del med["cantidad"]
960
+ del med["numero_serial"]
961
+ ws.SendMedicamentosDHSerie(usuario, password, **med)
962
+ else:
963
+ del med["cantidad"]
964
+ del med["desde_numero_serial"]
965
+ del med["hasta_numero_serial"]
966
+ ws.SendMedicamentos(usuario, password, **med)
967
+ med['codigo_transaccion'] = ws.CodigoTransaccion
968
+ errores.extend(ws.errores)
969
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
970
+ ws.Resultado,
971
+ ws.CodigoTransaccion,
972
+ '|'.join(ws.Errores or []),
973
+ ))
974
+ else:
975
+ print("ERROR: no se especificaron medicamentos a informar")
976
+
977
+ if not medicamentos:
978
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
979
+ ws.Resultado,
980
+ ws.CodigoTransaccion,
981
+ '|'.join(ws.Errores or []),
982
+ ))
983
+
984
+ if ws.Excepcion:
985
+ print(ws.Traceback)
986
+
987
+ if '--grabar' in sys.argv:
988
+ if '--dbf' in sys.argv:
989
+ guardar_dbf(formatos, True, {})
990
+ elif '--json' in sys.argv:
991
+ for formato in formatos:
992
+ archivo = open(formato[0].lower() + ".json", "w")
993
+ json.dump(formato[2], archivo, sort_keys=True, indent=4)
994
+ archivo.close()
995
+ else:
996
+ for formato, campos, lista in formatos:
997
+ archivo = open(formato.lower() + ".txt", "w")
998
+ for it in lista:
999
+ archivo.write(escribir(it, campos))
1000
+ archivo.close()
1001
+
1002
+
1003
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
1004
+ INSTALL_DIR = TrazaMed.InstallDir = get_install_dir()
1005
+
1006
+
1007
+ if __name__ == '__main__':
1008
+
1009
+ # ajusto el encoding por defecto (si se redirije la salida)
1010
+ if not hasattr(sys.stdout, "encoding") or sys.stdout.encoding is None:
1011
+ import codecs
1012
+ import locale
1013
+ sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout, "replace")
1014
+ sys.stderr = codecs.getwriter(locale.getpreferredencoding())(sys.stderr, "replace")
1015
+
1016
+ if '--register' in sys.argv or '--unregister' in sys.argv:
1017
+ import pythoncom
1018
+ if TYPELIB:
1019
+ if '--register' in sys.argv:
1020
+ tlb = os.path.abspath(os.path.join(INSTALL_DIR, "typelib", "trazamed.tlb"))
1021
+ print("Registering %s" % (tlb,))
1022
+ tli = pythoncom.LoadTypeLib(tlb)
1023
+ pythoncom.RegisterTypeLib(tli, tlb)
1024
+ elif '--unregister' in sys.argv:
1025
+ k = TrazaMed
1026
+ pythoncom.UnRegisterTypeLib(k._typelib_guid_,
1027
+ k._typelib_version_[0],
1028
+ k._typelib_version_[1],
1029
+ 0,
1030
+ pythoncom.SYS_WIN32)
1031
+ print("Unregistered typelib")
1032
+ import win32com.server.register
1033
+ win32com.server.register.UseCommandLine(TrazaMed)
1034
+ elif "/Automate" in sys.argv:
1035
+ # MS seems to like /automate to run the class factories.
1036
+ import win32com.server.localserver
1037
+ # win32com.server.localserver.main()
1038
+ # start the server.
1039
+ win32com.server.localserver.serve([TrazaMed._reg_clsid_])
1040
+ else:
1041
+ main()
app/pyafipws/trazaprodmed.py ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para Trazabilidad de Productos MοΏ½dicos ANMAT - Disp. 2303/2014
14
+ segοΏ½n EspecificaciοΏ½n TοΏ½cnica para Pruebas de Servicios (17/09/2015)"""
15
+
16
+ # InformaciοΏ½n adicional y documentaciοΏ½n:
17
+ # http://www.sistemasagiles.com.ar/trac/wiki/TrazabilidadProductosMedicos
18
+
19
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
20
+ __copyright__ = "Copyright (C) 2016 Mariano Reingart"
21
+ __license__ = "GPL 3.0"
22
+ __version__ = "1.01b"
23
+
24
+ import os
25
+ import socket
26
+ import sys
27
+ import datetime
28
+ import time
29
+ import traceback
30
+ import pysimplesoap.client
31
+ from pysimplesoap.client import SoapClient, SoapFault, parse_proxy, \
32
+ set_http_wrapper
33
+ from pysimplesoap.simplexml import SimpleXMLElement
34
+ from io import StringIO
35
+
36
+ # importo funciones compartidas:
37
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, dar_nombre_campo_dbf, get_install_dir, BaseWS, inicializar_y_capturar_excepciones
38
+
39
+ HOMO = False
40
+ TYPELIB = False
41
+
42
+ WSDL = "https://servicios.pami.org.ar/trazaenprodmed.WebService?wsdl"
43
+ WSDL_PROD = "https://servicios.pami.org.ar/trazaprodmed.WebService?wsdl"
44
+
45
+
46
+ class TrazaProdMed(BaseWS):
47
+ "Interfaz para el WebService de Trazabilidad de Productos MοΏ½dicos ANMAT - PAMI - INSSJP"
48
+ _public_methods_ = ['InformarProducto', 'CrearTransaccion',
49
+ 'SendCancelacTransacc', 'SendCancelacTransaccParcial',
50
+ 'GetTransaccionesWS', 'GetCatalogoElectronicoByGTIN',
51
+ 'GetCatalogoElectronicoByGLN', 'GetMedico',
52
+ 'Conectar', 'LeerError', 'LeerTransaccion',
53
+ 'SetUsername', 'SetPassword',
54
+ 'SetParametro', 'GetParametro',
55
+ 'GetCodigoTransaccion', 'GetResultado', 'LoadTestXML']
56
+
57
+ _public_attrs_ = [
58
+ 'Username', 'Password',
59
+ 'CodigoTransaccion', 'Errores', 'Resultado',
60
+ 'XmlRequest', 'XmlResponse',
61
+ 'Version', 'InstallDir',
62
+ 'Traceback', 'Excepcion', 'LanzarExcepciones',
63
+ 'CantPaginas', 'HayError', 'TransaccionesWS',
64
+ ]
65
+
66
+ _reg_progid_ = "TrazaProdMed"
67
+ _reg_clsid_ = "{D4112556-EF2E-45D3-A2A2-7A2849A364D9}"
68
+
69
+ # Variables globales para BaseWS:
70
+ HOMO = HOMO
71
+ WSDL = WSDL
72
+ Version = "%s %s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '',
73
+ pysimplesoap.client.__version__)
74
+
75
+ def __init__(self, reintentos=1):
76
+ self.Username = self.Password = None
77
+ self.Transacciones = []
78
+ BaseWS.__init__(self, reintentos)
79
+
80
+ def inicializar(self):
81
+ BaseWS.inicializar(self)
82
+ self.CodigoTransaccion = self.Errores = self.Resultado = None
83
+ self.Resultado = ''
84
+ self.Errores = [] # lista de strings para la interfaz
85
+ self.errores = [] # lista de diccionarios (uso interno)
86
+ self.CantPaginas = self.HayError = None
87
+
88
+ def __analizar_errores(self, ret):
89
+ "Comprueba y extrae errores si existen en la respuesta XML"
90
+ self.errores = ret.get('errores', [])
91
+ self.Errores = ["%s: %s" % (it['c_error'], it['d_error'])
92
+ for it in ret.get('errores', [])]
93
+ self.Resultado = ret.get('resultado')
94
+
95
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30):
96
+ # Conecto usando el mοΏ½todo estandard:
97
+ print("timeout", timeout)
98
+ ok = BaseWS.Conectar(self, cache, wsdl, proxy, wrapper, cacert, timeout,
99
+ soap_server="jetty")
100
+
101
+ if ok:
102
+ # Establecer credenciales de seguridad:
103
+ self.client['wsse:Security'] = {
104
+ 'wsse:UsernameToken': {
105
+ 'wsse:Username': self.Username,
106
+ 'wsse:Password': self.Password,
107
+ }
108
+ }
109
+ return ok
110
+
111
+ @inicializar_y_capturar_excepciones
112
+ def CrearTransaccion(self,
113
+ f_evento, h_evento, gln_origen, gln_destino,
114
+ n_remito, n_factura, vencimiento, gtin, lote,
115
+ numero_serial, id_evento,
116
+ cuit_medico=None, id_obra_social=None, apellido='', nombres='',
117
+ tipo_documento='', n_documento='', sexo='',
118
+ calle='', numero='', piso='', depto='', localidad='',
119
+ provincia='', n_postal='', fecha_nacimiento='', telefono='',
120
+ nro_afiliado=None, cod_diagnostico=None, cod_hiv=None,
121
+ id_motivo_devolucion=None, otro_motivo_devolucion=None):
122
+ "Inicializa internamente una estructura TransaccionDTO para informar"
123
+ # creo la transacciοΏ½n con los parοΏ½metros para llamar a InformarProducto
124
+ tx = {
125
+ 'fEvento': f_evento, 'hEvento': h_evento,
126
+ 'glnOrigen': gln_origen, 'glnDestino': gln_destino,
127
+ 'nroFactura': n_factura, 'nroRemito': n_remito,
128
+ 'vencimiento': vencimiento,
129
+ 'gtin': gtin, 'idEvento': id_evento,
130
+ 'nroSerial': numero_serial, 'lote': lote,
131
+ 'cuitMedico': cuit_medico,
132
+ 'apellidos': apellido, 'nombres': nombres, 'telefono': telefono,
133
+ 'calle': calle, 'nroCalle': numero, 'departamento': depto,
134
+ 'piso': piso, 'localidad': localidad, 'provincia': provincia,
135
+ 'codPostal': n_postal,
136
+ 'fechaNacimiento': fecha_nacimiento, 'sexo': sexo,
137
+ 'idTipoDocumento': tipo_documento, 'nroDocumento': n_documento,
138
+ 'codDiagnostico': cod_diagnostico, 'codHiv': cod_hiv,
139
+ 'idObraSocial': id_obra_social, 'nroAfiliado': nro_afiliado,
140
+ 'idMotivoDevolucion': id_motivo_devolucion,
141
+ 'otroMotivoDevolucion': otro_motivo_devolucion,
142
+ }
143
+ self.Transacciones.append(tx)
144
+ return True
145
+
146
+ @inicializar_y_capturar_excepciones
147
+ def InformarProducto(self, usuario, password):
148
+ "Realiza el registro de una transacciοΏ½n de producto. "
149
+ # El usuario (titular del registro/distribuidor/mοΏ½dico/establecimiento
150
+ # asistencial) informa el evento ocurrido para cada uno de los productos.
151
+ res = self.client.informarProducto(
152
+ transacciones=self.Transacciones,
153
+ usuario=usuario,
154
+ password=password,
155
+ )
156
+
157
+ ret = res['return']
158
+
159
+ self.CodigoTransaccion = ret['codigoTransaccion']
160
+ self.__analizar_errores(ret)
161
+
162
+ return True
163
+
164
+ @inicializar_y_capturar_excepciones
165
+ def SendCancelacTransacc(self, usuario, password, codigo_transaccion):
166
+ " Realiza la cancelaciοΏ½n de una transacciοΏ½n"
167
+ res = self.client.sendCancelacTransacc(
168
+ transaccion=codigo_transaccion,
169
+ usuario=usuario,
170
+ password=password,
171
+ )
172
+
173
+ ret = res['return']
174
+
175
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
176
+ self.__analizar_errores(ret)
177
+
178
+ return True
179
+
180
+ @inicializar_y_capturar_excepciones
181
+ def SendCancelacTransaccParcial(self, usuario, password, codigo_transaccion,
182
+ gtin=None, numero_serial=None):
183
+ " Realiza la cancelaciοΏ½n parcial de una transacciοΏ½n"
184
+ res = self.client.sendCancelacTransaccParcial(
185
+ transaccion=codigo_transaccion,
186
+ usuario=usuario,
187
+ password=password,
188
+ gtin=gtin,
189
+ serie=numero_serial,
190
+ )
191
+ ret = res['return']
192
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
193
+ self.__analizar_errores(ret)
194
+ return True
195
+
196
+ def LeerTransaccion(self):
197
+ "Recorro Transacciones devueltas por GetTransaccionesWS"
198
+ # usar GetParametro para consultar el valor retornado por el webservice
199
+
200
+ if self.Transacciones:
201
+ # extraigo el primer item
202
+ self.params_out = self.Transacciones.pop(0)
203
+ return True
204
+ else:
205
+ # limpio los parοΏ½metros
206
+ self.params_out = {}
207
+ return False
208
+
209
+ def LeerError(self):
210
+ "Recorro los errores devueltos y devuelvo el primero si existe"
211
+
212
+ if self.Errores:
213
+ # extraigo el primer item
214
+ er = self.Errores.pop(0)
215
+ return er
216
+ else:
217
+ return ""
218
+
219
+ @inicializar_y_capturar_excepciones
220
+ def GetTransaccionesWS(self, usuario, password,
221
+ id_transaccion=None,
222
+ gln_agente_origen=None, gln_agente_destino=None,
223
+ gtin=None, lote=None, serie=None, id_evento=None,
224
+ fecha_desde_op=None, fecha_hasta_op=None,
225
+ fecha_desde_t=None, fecha_hasta_t=None,
226
+ fecha_desde_v=None, fecha_hasta_v=None,
227
+ n_remito=None, n_factura=None,
228
+ id_provincia=None, id_estado=None, nro_pag=1, offset=100,
229
+ ):
230
+ "Obtiene los movimientos realizados y permite filtros de bοΏ½squeda"
231
+
232
+ # preparo los parametros de entrada opcionales:
233
+ kwargs = {}
234
+ if id_transaccion is not None:
235
+ kwargs['idTransaccion'] = id_transaccion
236
+ if gln_agente_origen is not None:
237
+ kwargs['glnAgenteOrigen'] = gln_agente_origen
238
+ if gln_agente_destino is not None:
239
+ kwargs['glnAgenteDestino'] = gln_agente_destino
240
+ if gtin is not None:
241
+ kwargs['gtin'] = gtin
242
+ if lote is not None:
243
+ kwargs['lote'] = lote
244
+ if serie is not None:
245
+ kwargs['serie'] = serie
246
+ if id_evento is not None:
247
+ kwargs['idEvento'] = id_evento
248
+ if fecha_desde_op is not None:
249
+ kwargs['fechaOperacionDesde'] = fecha_desde_op
250
+ if fecha_hasta_op is not None:
251
+ kwargs['fechaOperacionHasta'] = fecha_hasta_op
252
+ if fecha_desde_t is not None:
253
+ kwargs['fechaTransaccionDesde'] = fecha_desde_t
254
+ if fecha_hasta_t is not None:
255
+ kwargs['fechaTransaccionHasta'] = fecha_hasta_t
256
+ if fecha_desde_v is not None:
257
+ kwargs['fechaVencimientoDesde'] = fecha_desde_v
258
+ if fecha_hasta_v is not None:
259
+ kwargs['fechaVencimientoHasta'] = fecha_hasta_v
260
+ if n_remito is not None:
261
+ kwargs['remito'] = n_remito
262
+ if n_factura is not None:
263
+ kwargs['factura'] = n_factura
264
+ if id_provincia is not None:
265
+ kwargs['idProvincia'] = id_provincia
266
+ if id_estado is not None:
267
+ kwargs['idEstadoTransaccion'] = id_estado
268
+ if nro_pag is not None:
269
+ kwargs['pagina'] = nro_pag
270
+ if offset is not None:
271
+ kwargs['offset'] = offset
272
+
273
+ # llamo al webservice
274
+ res = self.client.getTransaccionesWS(
275
+ usuario=usuario,
276
+ password=password,
277
+ **kwargs
278
+ )
279
+ ret = res['return']
280
+ if ret:
281
+ self.__analizar_errores(ret)
282
+ self.CantPaginas = ret.get('cantPaginas')
283
+ self.HayError = ret.get('hay_error')
284
+ self.Transacciones = [it for it in ret.get('list', [])]
285
+ return True
286
+
287
+ @inicializar_y_capturar_excepciones
288
+ def GetCatalogoElectronicoByGTIN(self, usuario, password,
289
+ gtin=None, gln=None, marca=None, modelo=None,
290
+ cuit=None, id_nombre_generico=None, nro_pag=1, offset=100,
291
+ ):
292
+ "Obtiene el CatοΏ½logo ElectrοΏ½nico de Medicamentos"
293
+
294
+ # preparo los parametros de entrada opcionales:
295
+ kwargs = {}
296
+ if cuit is not None:
297
+ kwargs['cuit'] = cuit
298
+ if gtin is not None:
299
+ kwargs['gtin'] = gtin
300
+ if gln is not None:
301
+ kwargs['gln'] = gln
302
+ if marca is not None:
303
+ kwargs['marca'] = marca
304
+ if modelo is not None:
305
+ kwargs['modelo'] = modelo
306
+ if id_nombre_generico is not None:
307
+ kwargs['id_nombre_generico'] = id_nombre_generico
308
+ if nro_pag is not None:
309
+ kwargs['pagina'] = nro_pag
310
+ if offset is not None:
311
+ kwargs['offset'] = offset
312
+
313
+ # llamo al webservice
314
+ res = self.client.getCatalogoElectronicoByGTIN(
315
+ usuario=usuario,
316
+ password=password,
317
+ **kwargs
318
+ )
319
+
320
+ ret = res['return']
321
+ if ret:
322
+ self.__analizar_errores(ret)
323
+ self.CantPaginas = ret.get('cantPaginas')
324
+ self.HayError = ret.get('hay_error')
325
+ self.params_out = dict([(i, it) for i, it
326
+ in enumerate(ret.get('lstProductos', []))])
327
+ return len(self.params_out)
328
+ else:
329
+ return 0
330
+
331
+ def SetUsername(self, username):
332
+ "Establezco el nombre de usuario"
333
+ self.Username = username
334
+
335
+ def SetPassword(self, password):
336
+ "Establezco la contraseοΏ½a"
337
+ self.Password = password
338
+
339
+ def GetCodigoTransaccion(self):
340
+ "Devuelvo el cοΏ½digo de transacciοΏ½n"
341
+ return self.CodigoTransaccion
342
+
343
+ def GetResultado(self):
344
+ "Devuelvo el resultado"
345
+ return self.Resultado
346
+
347
+
348
+ def main():
349
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
350
+ import os
351
+ import time
352
+ import sys
353
+ global WSDL, LOCATION
354
+
355
+ DEBUG = '--debug' in sys.argv
356
+
357
+ ws = TrazaProdMed()
358
+
359
+ ws.Username = 'testwservice'
360
+ ws.Password = 'testwservicepsw'
361
+
362
+ if '--prod' in sys.argv and not HOMO:
363
+ WSDL = WSDL_PROD
364
+ print("Usando WSDL:", WSDL)
365
+ sys.argv.pop(sys.argv.index("--prod"))
366
+
367
+ # Inicializo las variables y estructuras para el archivo de intercambio:
368
+ transacciones = []
369
+ errores = []
370
+ formatos = []
371
+
372
+ if '--formato' in sys.argv:
373
+ print("Formato:")
374
+ for msg, formato, lista in formatos:
375
+ comienzo = 1
376
+ print("=== %s ===" % msg)
377
+ print("|| %-25s || %-12s || %-5s || %-4s || %-10s ||" % (
378
+ "Nombre", "Tipo", "Long.", "Pos(txt)", "Campo(dbf)"))
379
+ claves = []
380
+ for fmt in formato:
381
+ clave, longitud, tipo = fmt[0:3]
382
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
383
+ claves.append(clave_dbf)
384
+ print("|| %-25s || %-12s || %5d || %4d || %-10s ||" % (
385
+ clave, tipo, longitud, comienzo, clave_dbf))
386
+ comienzo += longitud
387
+ sys.exit(0)
388
+
389
+ if '--cargar' in sys.argv:
390
+ if '--dbf' in sys.argv:
391
+ leer_dbf(formatos[:1], {})
392
+ elif '--json' in sys.argv:
393
+ for formato in formatos[:1]:
394
+ archivo = open(formato[0].lower() + ".json", "r")
395
+ d = json.load(archivo)
396
+ formato[2].extend(d)
397
+ archivo.close()
398
+ else:
399
+ for formato in formatos[:1]:
400
+ archivo = open(formato[0].lower() + ".txt", "r")
401
+ for linea in archivo:
402
+ d = leer(linea, formato[1])
403
+ formato[2].append(d)
404
+ archivo.close()
405
+
406
+ ws.Conectar("", WSDL)
407
+
408
+ if ws.Excepcion:
409
+ print(ws.Excepcion)
410
+ print(ws.Traceback)
411
+ sys.exit(-1)
412
+
413
+ # Datos de pruebas:
414
+
415
+ if '--test' in sys.argv:
416
+ ws.CrearTransaccion(
417
+ f_evento=datetime.datetime.now().strftime("%d/%m/%Y"),
418
+ h_evento=datetime.datetime.now().strftime("%H:%M"),
419
+ gln_origen="7791234567801", gln_destino="7791234567801",
420
+ n_remito="R0001-12341234", n_factura="A0001-12341234",
421
+ vencimiento=(datetime.datetime.now() + datetime.timedelta(30)).strftime("%d/%m/%Y"),
422
+ gtin="07791234567810", lote=datetime.datetime.now().strftime("%Y"), # R4556567
423
+ numero_serial=int(time.time() * 10), # A23434
424
+ id_evento=1,
425
+ cuit_medico="30711622507", id_obra_social=465667,
426
+ apellido="Reingart", nombres="Mariano",
427
+ tipo_documento="96", n_documento="28510785", sexo="M",
428
+ calle="San Martin", numero="5656", piso="", depto="1",
429
+ localidad="Berazategui", provincia="Buenos Aires",
430
+ n_postal="1700", fecha_nacimiento="20/12/1972",
431
+ telefono="5555-5555",
432
+ nro_afiliado="9999999999999",
433
+ cod_diagnostico="B30",
434
+ cod_hiv="NOAP31121970",
435
+ id_motivo_devolucion=1,
436
+ otro_motivo_devolucion="producto fallado",
437
+ )
438
+
439
+ # Opciones principales:
440
+
441
+ if '--cancela' in sys.argv:
442
+ if '--loadxml' in sys.argv:
443
+ ws.LoadTestXML("tests/xml/trazaprodmed_cancela_err.xml") # cargo respuesta
444
+ ws.SendCancelacTransacc(*sys.argv[sys.argv.index("--cancela") + 1:])
445
+ elif '--cancela_parcial' in sys.argv:
446
+ ws.SendCancelacTransaccParcial(*sys.argv[sys.argv.index("--cancela_parcial") + 1:])
447
+ elif '--consulta' in sys.argv:
448
+ ws.GetTransaccionesWS(
449
+ *sys.argv[sys.argv.index("--consulta") + 1:]
450
+ )
451
+ print("CantPaginas", ws.CantPaginas)
452
+ print("HayError", ws.HayError)
453
+ # print "TransaccionPlainWS", ws.TransaccionPlainWS
454
+ # parametros comunes de salida (columnas de la tabla):
455
+ TRANSACCIONES = list(ws.Transacciones[0].keys()) if ws.Transacciones else []
456
+ claves = [k for k in TRANSACCIONES]
457
+ # extiendo la lista de resultado para el archivo de intercambio:
458
+ transacciones.extend(ws.Transacciones)
459
+ # encabezado de la tabla:
460
+ print("||", "||".join(["%s" % clave for clave in claves]), "||")
461
+ # recorro los datos devueltos (TransaccionPlainWS):
462
+ while ws.LeerTransaccion():
463
+ for clave in claves:
464
+ print("||", ws.GetParametro(clave), end=' ') # imprimo cada fila
465
+ print("||")
466
+ elif '--catalogo' in sys.argv:
467
+ ret = ws.GetCatalogoElectronicoByGTIN(
468
+ *sys.argv[sys.argv.index("--catalogo") + 1:]
469
+ )
470
+ for catalogo in list(ws.params_out.values()):
471
+ print(catalogo) # imprimo cada fila
472
+ else:
473
+ argv = [argv for argv in sys.argv if not argv.startswith("--")]
474
+ if not transacciones:
475
+ if len(argv) > 16:
476
+ ws.CrearTransaccion(*argv[3:])
477
+ else:
478
+ print("ERROR: no se indicaron todos los parοΏ½metros requeridos")
479
+ if ws.Transacciones:
480
+ try:
481
+ usuario, password = argv[1:3]
482
+ except BaseException:
483
+ print("ADVERTENCIA: no se indico parοΏ½metros usuario y passoword")
484
+ usuario = password = "pruebasws"
485
+ ws.InformarProducto(usuario, password)
486
+ for i, tx in enumerate(transacciones):
487
+ print("Procesando registro", i)
488
+ tx['codigo_transaccion'] = ws.CodigoTransaccion
489
+ errores.extend(ws.errores)
490
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
491
+ ws.Resultado,
492
+ ws.CodigoTransaccion,
493
+ '|'.join(ws.Errores or []),
494
+ ))
495
+ else:
496
+ print("ERROR: no se especificaron productos a informar")
497
+
498
+ if ws.Excepcion:
499
+ print(ws.Traceback)
500
+
501
+ if '--grabar' in sys.argv:
502
+ if '--dbf' in sys.argv:
503
+ guardar_dbf(formatos, True, {})
504
+ elif '--json' in sys.argv:
505
+ for formato in formatos:
506
+ archivo = open(formato[0].lower() + ".json", "w")
507
+ json.dump(formato[2], archivo, sort_keys=True, indent=4)
508
+ archivo.close()
509
+ else:
510
+ for formato in formatos:
511
+ archivo = open(formato[0].lower() + ".txt", "w")
512
+ for it in formato[2]:
513
+ archivo.write(escribir(it, formato[1]))
514
+ archivo.close()
515
+
516
+
517
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
518
+ INSTALL_DIR = TrazaProdMed.InstallDir = get_install_dir()
519
+
520
+
521
+ if __name__ == '__main__':
522
+
523
+ # ajusto el encoding por defecto (si se redirije la salida)
524
+ if not hasattr(sys.stdout, "encoding") or sys.stdout.encoding is None:
525
+ import codecs
526
+ import locale
527
+ sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout, "replace")
528
+ sys.stderr = codecs.getwriter(locale.getpreferredencoding())(sys.stderr, "replace")
529
+
530
+ if '--register' in sys.argv or '--unregister' in sys.argv:
531
+ import pythoncom
532
+ if TYPELIB:
533
+ if '--register' in sys.argv:
534
+ tlb = os.path.abspath(os.path.join(INSTALL_DIR, "typelib", "trazaprodmed.tlb"))
535
+ print("Registering %s" % (tlb,))
536
+ tli = pythoncom.LoadTypeLib(tlb)
537
+ pythoncom.RegisterTypeLib(tli, tlb)
538
+ elif '--unregister' in sys.argv:
539
+ k = TrazaProdMed
540
+ pythoncom.UnRegisterTypeLib(k._typelib_guid_,
541
+ k._typelib_version_[0],
542
+ k._typelib_version_[1],
543
+ 0,
544
+ pythoncom.SYS_WIN32)
545
+ print("Unregistered typelib")
546
+ import win32com.server.register
547
+ win32com.server.register.UseCommandLine(TrazaProdMed)
548
+ elif "/Automate" in sys.argv:
549
+ # MS seems to like /automate to run the class factories.
550
+ import win32com.server.localserver
551
+ # win32com.server.localserver.main()
552
+ # start the server.
553
+ win32com.server.localserver.serve([TrazaProdMed._reg_clsid_])
554
+ else:
555
+ main()
app/pyafipws/trazarenpre.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MΓ³dulo para Trazabilidad de Precursores QuΓ­micos RENPRE ResoluciΓ³n 900/12"
14
+
15
+ # InformaciΓ³n adicional y documentaciΓ³n:
16
+ # http://www.sistemasagiles.com.ar/trac/wiki/TrazabilidadPrecursoresQuimicos
17
+
18
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
19
+ __copyright__ = "Copyright (C) 2011 Mariano Reingart"
20
+ __license__ = "GPL 3.0+"
21
+ __version__ = "1.12a"
22
+
23
+ # http://renpre.servicios.pami.org.ar/portal_traza_renpre/paso5.html
24
+
25
+ import os
26
+ import socket
27
+ import sys
28
+ import datetime
29
+ import time
30
+ import pysimplesoap.client
31
+ from pysimplesoap.client import SoapFault
32
+ from .utils import BaseWS, inicializar_y_capturar_excepciones, get_install_dir
33
+
34
+ HOMO = False
35
+ TYPELIB = False
36
+
37
+ WSDL = "https://servicios.pami.org.ar/trazamed.WebServiceSDRN?wsdl"
38
+ LOCATION = "https://servicios.pami.org.ar/trazamed.WebServiceSDRN?wsdl"
39
+ # WSDL = "https://trazabilidad.pami.org.ar:59050/trazamed.WebServiceSDRN?wsdl" # prod.
40
+
41
+
42
+ class TrazaRenpre(BaseWS):
43
+ "Interfaz para el WebService de Trazabilidad de Precursores Quimicos SEDRONAR SNT"
44
+ _public_methods_ = ['SaveTransacciones',
45
+ 'SendCancelacTransacc', 'GetTransaccionesWS',
46
+ 'Conectar', 'LeerError', 'LeerTransaccion',
47
+ 'SetUsername',
48
+ 'SetParametro', 'GetParametro',
49
+ 'GetCodigoTransaccion', 'GetResultado', 'LoadTestXML']
50
+
51
+ _public_attrs_ = [
52
+ 'Username', 'Password',
53
+ 'CodigoTransaccion', 'Errores', 'Resultado',
54
+ 'XmlRequest', 'XmlResponse',
55
+ 'Version', 'InstallDir',
56
+ 'Traceback', 'Excepcion', 'LanzarExcepciones',
57
+ ]
58
+
59
+ _reg_progid_ = "TrazaRenpre"
60
+ _reg_clsid_ = "{461298DB-0531-47CA-B3D9-B36FE6967209}"
61
+
62
+ # Variables globales para BaseWS:
63
+ HOMO = HOMO
64
+ WSDL = WSDL
65
+ Version = "%s %s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '',
66
+ pysimplesoap.client.__version__)
67
+
68
+ def __init__(self, reintentos=1):
69
+ self.Username = self.Password = None
70
+ BaseWS.__init__(self, reintentos)
71
+
72
+ def inicializar(self):
73
+ BaseWS.inicializar(self)
74
+ self.CodigoTransaccion = self.Errores = self.Resultado = None
75
+
76
+ def __analizar_errores(self, ret):
77
+ "Comprueba y extrae errores si existen en la respuesta XML"
78
+ self.Errores = ["%s: %s" % (it.get('_c_error', ""), it.get('_d_error', ""))
79
+ for it in ret.get('errores', [])]
80
+ self.Resultado = ret.get('resultado')
81
+
82
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30):
83
+ # Conecto usando el mΓ©todo estandard:
84
+ ok = BaseWS.Conectar(self, cache, wsdl, proxy, wrapper, cacert, timeout,
85
+ soap_server="jetty")
86
+ if ok:
87
+ # si el archivo es local, asumo que ya esta corregido:
88
+ if not self.wsdl.startswith("file"):
89
+ # corrijo ubicaciΓ³n del servidor (localhost:9050 en el WSDL)
90
+ location = self.wsdl[:-5]
91
+ if 'IWebServiceSDRNService' in self.client.services:
92
+ ws = self.client.services['IWebServiceSDRNService']
93
+ else:
94
+ ws = self.client.services['IWebServiceSDRN']
95
+ ws['ports']['IWebServiceSDRNPort']['location'] = location
96
+ # Establecer credenciales de seguridad:
97
+ self.client['wsse:Security'] = {
98
+ 'wsse:UsernameToken': {
99
+ 'wsse:Username': self.Username,
100
+ 'wsse:Password': self.Password,
101
+ }
102
+ }
103
+ return ok
104
+
105
+ @inicializar_y_capturar_excepciones
106
+ def SaveTransacciones(self, usuario, password,
107
+ gln_origen=None, gln_destino=None, f_operacion=None,
108
+ id_evento=None, cod_producto=None, n_cantidad=None,
109
+ n_documento_operacion=None, n_remito=None,
110
+ id_tipo_transporte=None,
111
+ id_paso_frontera=None,
112
+ id_tipo_documento_operacion=None,
113
+ d_dominio_tractor=None,
114
+ d_dominio_semi=None,
115
+ n_serie=None, n_lote=None, doc_despacho_plaza=None,
116
+ djai=None, n_cert_impo_expo=None,
117
+ id_tipo_documento=None, n_documento=None,
118
+ m_calidad_analitica=None, m_entrega_parcial=None,
119
+ doc_permiso_embarque=None, gln_transportista=None,
120
+ operacion_excento_djai=None, control_duplicidad=None,
121
+ ):
122
+ "Permite informe por parte de un agente de una o varias transacciones"
123
+ # creo los parΓ‘metros para esta llamada
124
+ params = {'gln_origen': gln_origen, 'gln_destino': gln_destino,
125
+ 'f_operacion': f_operacion, 'id_evento': id_evento,
126
+ 'cod_producto': cod_producto, 'n_cantidad': n_cantidad,
127
+ 'n_documento_operacion': n_documento_operacion,
128
+ 'n_remito': n_remito,
129
+ 'id_tipo_transporte': id_tipo_transporte,
130
+ 'id_paso_frontera': id_paso_frontera,
131
+ 'id_tipo_documento_operacion': id_tipo_documento_operacion,
132
+ 'd_dominio_tractor': d_dominio_tractor,
133
+ 'd_dominio_semi': d_dominio_semi,
134
+ 'n_serie': n_serie, 'n_lote': n_lote,
135
+ 'doc_despacho_plaza': doc_despacho_plaza,
136
+ 'djai': djai, 'n_cert_impo_expo': n_cert_impo_expo,
137
+ 'id_tipo_documento': id_tipo_documento,
138
+ 'n_documento': n_documento,
139
+ 'm_calidad_analitica': m_calidad_analitica,
140
+ 'm_entrega_parcial': m_entrega_parcial,
141
+ 'doc_permiso_embarque': doc_permiso_embarque,
142
+ 'gln_transportista': gln_transportista,
143
+ 'operacion_excento_djai': operacion_excento_djai,
144
+ 'control_duplicidad': control_duplicidad,
145
+ }
146
+ # actualizo con parΓ‘metros generales:
147
+ params.update(self.params_in)
148
+ res = self.client.saveTransacciones(
149
+ arg0=params,
150
+ arg1=usuario,
151
+ arg2=password,
152
+ )
153
+ ret = res['return']
154
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
155
+ self.__analizar_errores(ret)
156
+ return True
157
+
158
+ @inicializar_y_capturar_excepciones
159
+ def SendCancelacTransacc(self, usuario, password, codigo_transaccion):
160
+ " Realiza la cancelaciΓ³n de una transacciΓ³n"
161
+ res = self.client.sendCancelaTransac(
162
+ arg0=codigo_transaccion,
163
+ arg1=usuario,
164
+ arg2=password,
165
+ )
166
+
167
+ ret = res['return']
168
+
169
+ self.CodigoTransaccion = ret['codigoTransaccion']
170
+ self.__analizar_errores(ret)
171
+
172
+ return True
173
+
174
+ @inicializar_y_capturar_excepciones
175
+ def SendConfirmaTransacc(self, usuario, password, p_ids_transac, f_operacion):
176
+ "Confirma la recepciΓ³n de un medicamento"
177
+ res = self.client.sendConfirmaTransacc(
178
+ arg0=usuario,
179
+ arg1=password,
180
+ arg2={'p_ids_transac': p_ids_transac, 'f_operacion': f_operacion},
181
+ )
182
+ ret = res['return']
183
+ self.CodigoTransaccion = ret.get('id_transac_asociada')
184
+ self.__analizar_errores(ret)
185
+ return True
186
+
187
+ @inicializar_y_capturar_excepciones
188
+ def SendAlertaTransacc(self, usuario, password, p_ids_transac_ws):
189
+ "Alerta un medicamento, acciΓ³n contraria a β€œconfirmar la transacciΓ³n”."
190
+ res = self.client.sendAlertaTransacc(
191
+ arg0=usuario,
192
+ arg1=password,
193
+ arg2=p_ids_transac_ws,
194
+ )
195
+ ret = res['return']
196
+ self.CodigoTransaccion = ret.get('id_transac_asociada')
197
+ self.__analizar_errores(ret)
198
+ return True
199
+
200
+ @inicializar_y_capturar_excepciones
201
+ def GetTransaccionesWS(self, usuario, password,
202
+ p_id_transaccion_global=None,
203
+ id_agente_origen=None, id_agente_destino=None,
204
+ id_agente_informador=None,
205
+ gtin=None, id_evento=None, cant_analitica=None,
206
+ fecha_desde_op=None, fecha_hasta_op=None,
207
+ fecha_desde_t=None, fecha_hasta_t=None,
208
+ id_tipo=None,
209
+ id_estado=None, nro_pag=1, cant_reg=100,
210
+ ):
211
+ "Obtiene los movimientos realizados y permite filtros de bΓΊsqueda"
212
+
213
+ # preparo los parametros de entrada opcionales:
214
+ kwargs = {}
215
+ if p_id_transaccion_global is not None:
216
+ kwargs['arg2'] = p_id_transaccion_global
217
+ if id_agente_origen is not None:
218
+ kwargs['arg3'] = id_agente_origen
219
+ if id_agente_destino is not None:
220
+ kwargs['arg4'] = id_agente_destino
221
+ if id_agente_destino is not None:
222
+ kwargs['arg5'] = id_agente_informador
223
+ if gtin is not None:
224
+ kwargs['arg6'] = gtin
225
+ if id_evento is not None:
226
+ kwargs['arg7'] = id_evento
227
+ if cant_analitica is not None:
228
+ kwargs['arg8'] = cant_analitica
229
+ if fecha_desde_op is not None:
230
+ kwargs['arg9'] = fecha_desde_op
231
+ if fecha_hasta_op is not None:
232
+ kwargs['arg10'] = fecha_hasta_op
233
+ if fecha_desde_t is not None:
234
+ kwargs['arg11'] = fecha_desde_t
235
+ if fecha_hasta_t is not None:
236
+ kwargs['arg12'] = fecha_hasta_t
237
+ if id_tipo is not None:
238
+ kwargs['arg13'] = id_tipo
239
+ if id_estado is not None:
240
+ kwargs['arg14'] = id_estado
241
+ if nro_pag is not None:
242
+ kwargs['arg15'] = nro_pag
243
+ if cant_reg is not None:
244
+ kwargs['arg16'] = cant_reg
245
+
246
+ # llamo al webservice
247
+ res = self.client.getTransaccionesWs(
248
+ arg0=usuario,
249
+ arg1=password,
250
+ **kwargs
251
+ )
252
+ ret = res['return']
253
+ if ret:
254
+ self.__analizar_errores(ret)
255
+ self.CantPaginas = ret.get('cantPaginas')
256
+ self.HayError = ret.get('hay_error')
257
+ self.TransaccionPlainWS = [it for it in ret.get('list', [])]
258
+ return True
259
+
260
+ def SetUsername(self, username):
261
+ "Establezco el nombre de usuario"
262
+ self.Username = username
263
+
264
+ def SetPassword(self, password):
265
+ "Establezco la contraseΓ±a"
266
+ self.Password = password
267
+
268
+ def GetCodigoTransaccion(self):
269
+ "Devuelvo el cΓ³digo de transacciΓ³n"
270
+ return self.CodigoTransaccion
271
+
272
+ def GetResultado(self):
273
+ "Devuelvo el resultado"
274
+ return self.Resultado
275
+
276
+
277
+ def main():
278
+ "FunciΓ³n principal de pruebas (transaccionar!)"
279
+ import os
280
+ import time
281
+ import sys
282
+ global WSDL, LOCATION
283
+
284
+ DEBUG = '--debug' in sys.argv
285
+
286
+ ws = TrazaRenpre()
287
+
288
+ ws.Username = 'testwservice'
289
+ ws.Password = 'testwservicepsw'
290
+
291
+ if '--prod' in sys.argv and not HOMO:
292
+ WSDL = "https://trazabilidad.pami.org.ar:59050/trazamed.WebServiceSDRN?wsdl"
293
+ print("Usando WSDL:", WSDL)
294
+ sys.argv.pop(0)
295
+
296
+ ws.Conectar("", WSDL)
297
+
298
+ if ws.Excepcion:
299
+ print(ws.Excepcion)
300
+ print(ws.Traceback)
301
+ sys.exit(-1)
302
+
303
+ # print ws.client.services
304
+ #op = ws.client.get_operation("sendMedicamentos")
305
+ #import pdb;pdb.set_trace()
306
+ if '--test' in sys.argv:
307
+ ws.SaveTransacciones(
308
+ usuario='pruebasws', password='pruebasws',
309
+ gln_origen=8888888888888,
310
+ gln_destino=8888888888888,
311
+ f_operacion="20/05/2014",
312
+ id_evento=44,
313
+ cod_producto=88800000000035, # acido sulfΓΊrico
314
+ n_cantidad=1,
315
+ n_documento_operacion=1,
316
+ # m_entrega_parcial="",
317
+ n_remito=123,
318
+ n_serie=112,
319
+ )
320
+ print("Resultado", ws.Resultado)
321
+ print("CodigoTransaccion", ws.CodigoTransaccion)
322
+ print("Excepciones", ws.Excepcion)
323
+ print("Erroes", ws.Errores)
324
+ elif '--cancela' in sys.argv:
325
+ ws.SendCancelacTransacc(*sys.argv[sys.argv.index("--cancela") + 1:])
326
+ elif '--consulta' in sys.argv:
327
+ if '--movimientos' in sys.argv:
328
+ ws.GetTransaccionesWS(
329
+ *sys.argv[sys.argv.index("--movimientos") + 1:]
330
+ )
331
+ else:
332
+ ws.SaveTransacciones(*sys.argv[1:])
333
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
334
+ ws.Resultado,
335
+ ws.CodigoTransaccion,
336
+ '|'.join(ws.Errores),
337
+ ))
338
+ if ws.Excepcion:
339
+ print(ws.Traceback)
340
+
341
+
342
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
343
+ INSTALL_DIR = TrazaRenpre.InstallDir = get_install_dir()
344
+
345
+
346
+ if __name__ == '__main__':
347
+
348
+ # ajusto el encoding por defecto (si se redirije la salida)
349
+ if sys.stdout.encoding is None:
350
+ import codecs
351
+ import locale
352
+ sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout, "replace")
353
+ sys.stderr = codecs.getwriter(locale.getpreferredencoding())(sys.stderr, "replace")
354
+
355
+ if '--register' in sys.argv or '--unregister' in sys.argv:
356
+ import pythoncom
357
+ import win32com.server.register
358
+ win32com.server.register.UseCommandLine(TrazaRenpre)
359
+ elif "/Automate" in sys.argv:
360
+ # MS seems to like /automate to run the class factories.
361
+ import win32com.server.localserver
362
+ # win32com.server.localserver.main()
363
+ # start the server.
364
+ win32com.server.localserver.serve([TrazaRenpre._reg_clsid_])
365
+ else:
366
+ main()
app/pyafipws/trazavet.py ADDED
@@ -0,0 +1,598 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MοΏ½dulo Trazabilidad de Productos Veterinarios SENASA ResoluciοΏ½n 369/2013"
14
+
15
+ # InformaciοΏ½n adicional y documentaciοΏ½n:
16
+ # http://www.sistemasagiles.com.ar/trac/wiki/TrazabilidadProductosVeterinarios
17
+
18
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
19
+ __copyright__ = "Copyright (C) 2014 Mariano Reingart"
20
+ __license__ = "GPL 3.0+"
21
+ __version__ = "1.11d"
22
+
23
+ # http://senasa.servicios.pami.org.ar/
24
+
25
+ import os
26
+ import socket
27
+ import sys
28
+ import datetime
29
+ import time
30
+ import traceback
31
+ import pysimplesoap.client
32
+ from pysimplesoap.client import SoapClient, SoapFault, parse_proxy, \
33
+ set_http_wrapper
34
+ from pysimplesoap.simplexml import SimpleXMLElement
35
+ from io import StringIO
36
+
37
+ # importo funciones compartidas:
38
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, \
39
+ dar_nombre_campo_dbf, get_install_dir, BaseWS, \
40
+ inicializar_y_capturar_excepciones
41
+
42
+ HOMO = True
43
+ TYPELIB = False
44
+
45
+ WSDL = "https://servicios.pami.org.ar/trazaenvet.WebService?wsdl"
46
+ LOCATION = "https://servicios.pami.org.ar/trazaenvet.WebService?wsdl"
47
+
48
+ # Formato de TransaccionSenasaDTO (SaveTransaccion)
49
+ TRANSACCION_DTO = [
50
+ ('gln_origen', 13, A),
51
+ ('gln_destino', 13, A),
52
+ ('f_operacion', 10, A),
53
+ ('f_elaboracion', 10, A),
54
+ ('f_vto', 10, A),
55
+ ('id_evento', 15, N),
56
+ ('cod_producto', 14, A),
57
+ ('n_cantidad', 30, N),
58
+ ('n_serie', 20, A),
59
+ ('n_lote', 50, A),
60
+ ('n_cai', 15, A),
61
+ ('n_cae', 15, A),
62
+ ('id_motivo_destruccion', 5, A),
63
+ ('n_manifiesto', 15, N),
64
+ ('en_transporte', 1, A), # boolean
65
+ ('n_remito', 15, A),
66
+ ('motivo_devolucion', 100, A),
67
+ ('observaciones', 1000, A),
68
+ ('n_vale_compra', 15, A),
69
+ ('apellidoNombres', 255, A),
70
+ ('direccion', 200, A),
71
+ ('numero', 6, N),
72
+ ('localidad', 15, A),
73
+ ('provincia', 15, A),
74
+ ('n_postal', 8, A),
75
+ ('cuit', 11, A),
76
+ ('codigo_transaccion', 14, A),
77
+ ]
78
+
79
+ # Formato para TransaccionSenasa (getTransacciones)
80
+ TRANSACCIONES = [
81
+ ('id_transaccion_global', 15, N),
82
+ ('id_transaccion', 15, N),
83
+ ('f_transaccion', 10, A),
84
+ ('f_operacion', 10, A),
85
+ ('f_vencimiento', 10, A),
86
+ ('f_elaboracion', 10, A),
87
+ ('d_evento', 100, A),
88
+ ('n_cantidad', 30, N),
89
+ ('id_unidad', 15, N),
90
+ ('d_unidad', 100, A),
91
+ ('cod_producto', 14, A),
92
+ ('id_unidad', 15, N),
93
+ ('n_serie', 20, A),
94
+ ('n_lote', 50, A),
95
+ ('n_cai', 15, A),
96
+ ('n_cae', 15, A),
97
+ ('d_motivo_destruccion', 50, A),
98
+ ('d_manifiesto', 15, A),
99
+ ('en_transporte', 1, A),
100
+ ('n_remito', 30, A),
101
+ ('motivo_devolucion', 200, A),
102
+ ('observaciones', 1000, A),
103
+ ('n_vale_compra', 15, A),
104
+ ('apellidoNombres', 255, A),
105
+ ('direccion', 200, A),
106
+ ('numero', 6, A),
107
+ ('localidad', 250, A),
108
+ ('provincia', 250, A),
109
+ ('n_postal', 8, A),
110
+ ('cuit', 11, A),
111
+ ('d_agente_informador', 255, A),
112
+ ('d_agente_origen', 255, A),
113
+ ('d_agente_destino', 255, A),
114
+ ('d_producto', 250, A),
115
+ ('d_estado_transaccion', 30, A),
116
+ ('d_tipo_transaccion', 30, A),
117
+ ]
118
+
119
+ # Formato para Errores
120
+ ERRORES = [
121
+ ('_c_error', 4, A), # cοΏ½digo
122
+ ('_d_error', 250, A), # descripciοΏ½n
123
+ ]
124
+
125
+
126
+ class TrazaVet(BaseWS):
127
+ "Interfaz para el WebService de Trazabilidad de Veterinarios SENASA"
128
+
129
+ _public_methods_ = ['SaveTransaccion', 'SendCancelaTransac',
130
+ 'SendConfirmaTransacc', 'SendAlertaTransacc',
131
+ 'GetTransacciones',
132
+ 'Conectar', 'LeerError', 'LeerTransaccion',
133
+ 'SetUsername',
134
+ 'SetParametro', 'GetParametro',
135
+ 'GetCodigoTransaccion', 'GetResultado', 'LoadTestXML']
136
+
137
+ _public_attrs_ = [
138
+ 'Username', 'Password',
139
+ 'CodigoTransaccion', 'Errores', 'Resultado',
140
+ 'XmlRequest', 'XmlResponse',
141
+ 'Version', 'InstallDir',
142
+ 'Traceback', 'Excepcion',
143
+ 'CantPaginas', 'HayError', 'TransaccionSenasa',
144
+ ]
145
+
146
+ _reg_progid_ = "TrazaVet"
147
+ _reg_clsid_ = "{2E28D79D-23B0-4C1E-85B9-64BDC41B8FCF}"
148
+
149
+ # Variables globales para BaseWS:
150
+ HOMO = HOMO
151
+ WSDL = WSDL
152
+ Version = "%s %s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '',
153
+ pysimplesoap.client.__version__)
154
+
155
+ def __init__(self, reintentos=1):
156
+ self.Username = self.Password = None
157
+ self.TransaccionSenasa = []
158
+ BaseWS.__init__(self, reintentos)
159
+
160
+ def inicializar(self):
161
+ BaseWS.inicializar(self)
162
+ self.CodigoTransaccion = self.Errores = self.Resultado = None
163
+ self.Resultado = ''
164
+ self.Errores = [] # lista de strings para la interfaz
165
+ self.errores = [] # lista de diccionarios (uso interno)
166
+ self.CantPaginas = self.HayError = None
167
+
168
+ def __analizar_errores(self, ret):
169
+ "Comprueba y extrae errores si existen en la respuesta XML"
170
+ self.errores = ret.get('errores', [])
171
+ self.Errores = ["%s: %s" % (it['c_error'], it['d_error'])
172
+ for it in ret.get('errores', [])]
173
+ self.Resultado = ret.get('resultado')
174
+
175
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30):
176
+ # Conecto usando el mοΏ½todo estandard:
177
+ ok = BaseWS.Conectar(self, cache, wsdl, proxy, wrapper, cacert, timeout,
178
+ soap_server="jetty")
179
+ if ok:
180
+ # si el archivo es local, asumo que ya esta corregido:
181
+ if not self.wsdl.startswith("file"):
182
+ # corrijo ubicaciοΏ½n del servidor (localhost:59050 en el WSDL)
183
+ location = self.wsdl[:-5]
184
+ ws = self.client.services['IWebServiceSenasa']
185
+ ws['ports']['IWebServiceSenasaPort']['location'] = location
186
+
187
+ # Establecer credenciales de seguridad:
188
+ self.client['wsse:Security'] = {
189
+ 'wsse:UsernameToken': {
190
+ 'wsse:Username': self.Username,
191
+ 'wsse:Password': self.Password,
192
+ }
193
+ }
194
+ return ok
195
+
196
+ @inicializar_y_capturar_excepciones
197
+ def SaveTransaccion(self, usuario, password,
198
+ gln_origen=None, gln_destino=None,
199
+ f_operacion=None, f_elaboracion=None, f_vto=None,
200
+ id_evento=None, cod_producto=None, n_cantidad=None,
201
+ n_serie=None, n_lote=None, n_cai=None, n_cae=None,
202
+ id_motivo_destruccion=None, n_manifiesto=None,
203
+ en_transporte=None, n_remito=None,
204
+ motivo_devolucion=None, observaciones=None,
205
+ n_vale_compra=None, apellidoNombres=None,
206
+ direccion=None, numero=None, localidad=None,
207
+ provincia=None, n_postal=None, cuit=None
208
+ ):
209
+ "Realiza el registro de una transacciοΏ½n de productos fitosanitarios. "
210
+ # creo los parοΏ½metros para esta llamada
211
+ params = {'gln_origen': gln_origen,
212
+ 'gln_destino': gln_destino,
213
+ 'f_operacion': f_operacion,
214
+ 'f_elaboracion': f_elaboracion,
215
+ 'f_vto': f_vto,
216
+ 'id_evento': id_evento,
217
+ 'cod_producto': cod_producto,
218
+ 'n_cantidad': n_cantidad,
219
+ 'n_serie': n_serie,
220
+ 'n_lote': n_lote,
221
+ 'n_cai': n_cai or None,
222
+ 'n_cae': n_cae or None,
223
+ 'id_motivo_destruccion': id_motivo_destruccion or None,
224
+ 'n_manifiesto': n_manifiesto or None,
225
+ 'en_transporte': en_transporte or None,
226
+ 'n_remito': n_remito or None,
227
+ 'motivo_devolucion': motivo_devolucion or None,
228
+ 'observaciones': observaciones or None,
229
+ 'n_vale_compra': n_vale_compra or None,
230
+ 'apellidoNombres': apellidoNombres or None,
231
+ 'direccion': direccion or None,
232
+ 'numero': numero or None,
233
+ 'localidad': localidad or None,
234
+ 'provincia': provincia or None,
235
+ 'n_postal': n_postal or None,
236
+ 'cuit': cuit or None,
237
+ }
238
+ res = self.client.saveTransacciones(
239
+ arg0=params,
240
+ arg1=usuario,
241
+ arg2=password,
242
+ )
243
+ ret = res['return']
244
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
245
+ self.__analizar_errores(ret)
246
+ return True
247
+
248
+ @inicializar_y_capturar_excepciones
249
+ def SendCancelaTransac(self, usuario, password, codigo_transaccion):
250
+ " Realiza la cancelaciοΏ½n de una transacciοΏ½n"
251
+ res = self.client.sendCancelaTransac(
252
+ arg0=codigo_transaccion,
253
+ arg1=usuario,
254
+ arg2=password,
255
+ )
256
+ ret = res['return']
257
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
258
+ self.__analizar_errores(ret)
259
+ return True
260
+
261
+ @inicializar_y_capturar_excepciones
262
+ def SendConfirmaTransacc(self, usuario, password, p_ids_transac, f_operacion, n_cantidad=None):
263
+ "Confirma la recepciοΏ½n de un medicamento"
264
+ res = self.client.sendConfirmaTransacc(
265
+ arg0=usuario,
266
+ arg1=password,
267
+ arg2={'p_ids_transac': p_ids_transac, 'f_operacion': f_operacion,
268
+ 'n_cantidad': n_cantidad,
269
+ },
270
+ )
271
+ ret = res['return']
272
+ self.CodigoTransaccion = ret.get('codigoTransaccion')
273
+ self.__analizar_errores(ret)
274
+ return True
275
+
276
+ @inicializar_y_capturar_excepciones
277
+ def SendAlertaTransacc(self, usuario, password, p_ids_transac_ws):
278
+ "Alerta un medicamento, acciοΏ½n contraria a οΏ½confirmar la transacciοΏ½nοΏ½."
279
+ res = self.client.sendAlertaTransacc(
280
+ arg0=usuario,
281
+ arg1=password,
282
+ arg2=p_ids_transac_ws,
283
+ )
284
+ ret = res['return']
285
+ self.CodigoTransaccion = ret.get('id_transac_asociada')
286
+ self.__analizar_errores(ret)
287
+ return True
288
+
289
+ @inicializar_y_capturar_excepciones
290
+ def GetTransacciones(self, usuario, password,
291
+ id_transaccion=None, id_evento=None, gln_origen=None,
292
+ fecha_desde_t=None, fecha_hasta_t=None,
293
+ fecha_desde_v=None, fecha_hasta_v=None,
294
+ gln_informador=None, id_tipo_transaccion=None,
295
+ gtin_elemento=None, n_lote=None, n_serie=None,
296
+ n_remito_factura=None,
297
+ ):
298
+ "Trae un listado de las transacciones que no estοΏ½n confirmadas"
299
+
300
+ # preparo los parametros de entrada opcionales:
301
+ kwargs = {}
302
+ if id_transaccion is not None:
303
+ kwargs['arg2'] = id_transaccion
304
+ if id_evento is not None:
305
+ kwargs['arg3'] = id_evento
306
+ if gln_origen is not None:
307
+ kwargs['arg4'] = gln_origen
308
+ if fecha_desde_t is not None:
309
+ kwargs['arg5'] = fecha_desde_t
310
+ if fecha_hasta_t is not None:
311
+ kwargs['arg6'] = fecha_hasta_t
312
+ if fecha_desde_v is not None:
313
+ kwargs['arg7'] = fecha_desde_v
314
+ if fecha_hasta_v is not None:
315
+ kwargs['arg8'] = fecha_hasta_v
316
+ if gln_informador is not None:
317
+ kwargs['arg9'] = gln_informador
318
+ if id_tipo_transaccion is not None:
319
+ kwargs['arg10'] = id_tipo_transaccion
320
+ if gtin_elemento is not None:
321
+ kwargs['arg11'] = gtin_elemento
322
+ if n_lote is not None:
323
+ kwargs['arg12'] = n_lote
324
+ if n_serie is not None:
325
+ kwargs['arg13'] = n_serie
326
+ if n_remito_factura is not None:
327
+ kwargs['arg14'] = n_remito_factura
328
+
329
+ # llamo al webservice
330
+ res = self.client.getTransacciones(
331
+ arg0=usuario,
332
+ arg1=password,
333
+ **kwargs
334
+ )
335
+ ret = res['return']
336
+ if ret:
337
+ self.__analizar_errores(ret)
338
+ self.CantPaginas = ret.get('cantPaginas')
339
+ self.HayError = ret.get('hay_error')
340
+ self.TransaccionSenasa = [it for it in ret.get('list', [])]
341
+ return True
342
+
343
+ def LeerTransaccion(self):
344
+ "Recorro TransaccionSenasa devuelto por GetTransacciones"
345
+ # usar GetParametro para consultar el valor retornado por el webservice
346
+
347
+ if self.TransaccionSenasa:
348
+ # extraigo el primer item
349
+ self.params_out = self.TransaccionSenasa.pop(0)
350
+ return True
351
+ else:
352
+ # limpio los parοΏ½metros
353
+ self.params_out = {}
354
+ return False
355
+
356
+ def LeerError(self):
357
+ "Recorro los errores devueltos y devuelvo el primero si existe"
358
+
359
+ if self.Errores:
360
+ # extraigo el primer item
361
+ er = self.Errores.pop(0)
362
+ return er
363
+ else:
364
+ return ""
365
+
366
+ def SetUsername(self, username):
367
+ "Establezco el nombre de usuario"
368
+ self.Username = username
369
+
370
+ def SetPassword(self, password):
371
+ "Establezco la contraseοΏ½a"
372
+ self.Password = password
373
+
374
+ def GetCodigoTransaccion(self):
375
+ "Devuelvo el cοΏ½digo de transacciοΏ½n"
376
+ return self.CodigoTransaccion
377
+
378
+ def GetResultado(self):
379
+ "Devuelvo el resultado"
380
+ return self.Resultado
381
+
382
+
383
+ def main():
384
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
385
+ import os
386
+ import time
387
+ import sys
388
+ global WSDL, LOCATION
389
+
390
+ DEBUG = '--debug' in sys.argv
391
+
392
+ ws = TrazaVet()
393
+
394
+ ws.Username = 'testwservice'
395
+ ws.Password = 'testwservicepsw'
396
+
397
+ if '--prod' in sys.argv and not HOMO:
398
+ WSDL = "https://servicios.pami.org.ar/trazavet.WebService?wsdl"
399
+ print("Usando WSDL:", WSDL)
400
+ sys.argv.pop(sys.argv.index("--prod"))
401
+
402
+ if '--proxy' in sys.argv and not HOMO:
403
+ proxy = sys.argv.pop(sys.argv.index("--proxy") + 1)
404
+ print("Usando proxy:", proxy)
405
+ sys.argv.pop(sys.argv.index("--proxy"))
406
+ else:
407
+ proxy = None
408
+
409
+ # Inicializo las variables y estructuras para el archivo de intercambio:
410
+ transaccion_dto = []
411
+ transacciones = []
412
+ errores = []
413
+ formatos = [('TransaccionDTO', TRANSACCION_DTO, transaccion_dto),
414
+ ('Transacciones', TRANSACCIONES, transacciones),
415
+ ('Errores', ERRORES, errores),
416
+ ]
417
+
418
+ if '--formato' in sys.argv:
419
+ print("Formato:")
420
+ for msg, formato, lista in formatos:
421
+ comienzo = 1
422
+ print("=== %s ===" % msg)
423
+ print("|| %-25s || %-12s || %-5s || %-4s || %-10s ||" % (
424
+ "Nombre", "Tipo", "Long.", "Pos(txt)", "Campo(dbf)"))
425
+ claves = []
426
+ for fmt in formato:
427
+ clave, longitud, tipo = fmt[0:3]
428
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
429
+ claves.append(clave_dbf)
430
+ print("|| %-25s || %-12s || %5d || %4d || %-10s ||" % (
431
+ clave, tipo, longitud, comienzo, clave_dbf))
432
+ comienzo += longitud
433
+ sys.exit(0)
434
+
435
+ if '--cargar' in sys.argv:
436
+ if '--dbf' in sys.argv:
437
+ leer_dbf(formatos[:1], {})
438
+ elif '--json' in sys.argv:
439
+ for formato in formatos[:1]:
440
+ archivo = open(formato[0].lower() + ".json", "r")
441
+ d = json.load(archivo)
442
+ formato[2].extend(d)
443
+ archivo.close()
444
+ else:
445
+ for formato in formatos[:1]:
446
+ archivo = open(formato[0].lower() + ".txt", "r")
447
+ for linea in archivo:
448
+ d = leer(linea, formato[1])
449
+ formato[2].append(d)
450
+ archivo.close()
451
+
452
+ ws.Conectar("", WSDL, proxy)
453
+
454
+ if ws.Excepcion:
455
+ print(ws.Excepcion)
456
+ print(ws.Traceback)
457
+ sys.exit(-1)
458
+
459
+ # Datos de pruebas:
460
+
461
+ if '--test' in sys.argv:
462
+ transaccion_dto.append(dict(
463
+ gln_origen="9876543210982", gln_destino="3692581473693",
464
+ f_operacion=datetime.datetime.now().strftime("%d/%m/%Y"),
465
+ f_elaboracion=datetime.datetime.now().strftime("%d/%m/%Y"),
466
+ f_vto=(datetime.datetime.now() + datetime.timedelta(30)).strftime("%d/%m/%Y"),
467
+ id_evento=11,
468
+ cod_producto="88900000000001",
469
+ n_cantidad=1,
470
+ n_serie=int(time.time() * 10),
471
+ n_lote=datetime.datetime.now().strftime("%Y"),
472
+ n_cai="123456789012345",
473
+ n_cae="",
474
+ id_motivo_destruccion=0,
475
+ n_manifiesto="",
476
+ en_transporte="N",
477
+ n_remito="1234",
478
+ motivo_devolucion="",
479
+ observaciones="prueba",
480
+ n_vale_compra="",
481
+ apellidoNombres="Juan Peres",
482
+ direccion="Saraza", numero="1234",
483
+ localidad="Hurlingham", provincia="Buenos Aires",
484
+ n_postal="1688",
485
+ cuit="20267565393",
486
+ codigo_transaccion=None,
487
+ ))
488
+
489
+ # Opciones principales:
490
+
491
+ if '--confirma' in sys.argv:
492
+ if '--loadxml' in sys.argv:
493
+ ws.LoadTestXML("trazamed_confirma.xml") # cargo respuesta
494
+ ok = ws.SendConfirmaTransacc(usuario="pruebasws", password="pruebasws",
495
+ p_ids_transac="1", f_operacion="31-12-2013")
496
+ if not ok:
497
+ raise RuntimeError(ws.Excepcion)
498
+ ws.SendConfirmaTransacc(*sys.argv[sys.argv.index("--confirma") + 1:])
499
+ elif '--alerta' in sys.argv:
500
+ ws.SendAlertaTransacc(*sys.argv[sys.argv.index("--alerta") + 1:])
501
+ elif '--cancela' in sys.argv:
502
+ ws.SendCancelaTransac(*sys.argv[sys.argv.index("--cancela") + 1:])
503
+ elif '--consulta' in sys.argv:
504
+ ws.GetTransacciones(
505
+ *sys.argv[sys.argv.index("--consulta") + 1:]
506
+ )
507
+ print("CantPaginas", ws.CantPaginas)
508
+ print("HayError", ws.HayError)
509
+ # print "TransaccionSenasa", ws.TransaccionSenasa
510
+ # parametros comunes de salida (columnas de la tabla):
511
+ claves = [k for k, v, l in TRANSACCIONES]
512
+ # extiendo la lista de resultado para el archivo de intercambio:
513
+ transacciones.extend(ws.TransaccionSenasa)
514
+ # encabezado de la tabla:
515
+ print("||", "||".join(["%s" % clave for clave in claves]), "||")
516
+ # recorro los datos devueltos (TransaccionSenasa):
517
+ while ws.LeerTransaccion():
518
+ for clave in claves:
519
+ print("||", ws.GetParametro(clave), end=' ') # imprimo cada fila
520
+ print("||")
521
+ else:
522
+ argv = [argv for argv in sys.argv if not argv.startswith("--")]
523
+ if not transaccion_dto:
524
+ if len(argv) > 10:
525
+ ws.SaveTransaccion(*argv[1:])
526
+ else:
527
+ print("ERROR: no se indicaron todos los parοΏ½metros requeridos")
528
+ elif transaccion_dto:
529
+ try:
530
+ usuario, password = argv[-2:]
531
+ except BaseException:
532
+ print("ADVERTENCIA: no se indico parοΏ½metros usuario y passoword")
533
+ usuario, password = "senasaws", "Clave2013"
534
+ for i, dto in enumerate(transaccion_dto):
535
+ print("Procesando registro", i)
536
+ del dto['codigo_transaccion']
537
+ ws.SaveTransaccion(usuario, password, **dto)
538
+ dto['codigo_transaccion'] = ws.CodigoTransaccion
539
+ errores.extend(ws.errores)
540
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
541
+ ws.Resultado,
542
+ ws.CodigoTransaccion,
543
+ '|'.join(ws.Errores or []),
544
+ ))
545
+ else:
546
+ print("ERROR: no se especificaron productos a informar")
547
+
548
+ if not transaccion_dto:
549
+ print("|Resultado %5s|CodigoTransaccion %10s|Errores|%s|" % (
550
+ ws.Resultado,
551
+ ws.CodigoTransaccion,
552
+ '|'.join(ws.Errores or []),
553
+ ))
554
+
555
+ if ws.Excepcion:
556
+ print(ws.Traceback)
557
+
558
+ if '--grabar' in sys.argv:
559
+ if '--dbf' in sys.argv:
560
+ guardar_dbf(formatos, True, {})
561
+ elif '--json' in sys.argv:
562
+ for formato in formatos:
563
+ archivo = open(formato[0].lower() + ".json", "w")
564
+ json.dump(formato[2], archivo, sort_keys=True, indent=4)
565
+ archivo.close()
566
+ else:
567
+ for formato in formatos:
568
+ archivo = open(formato[0].lower() + ".txt", "w")
569
+ for it in formato[2]:
570
+ archivo.write(escribir(it, formato[1]))
571
+ archivo.close()
572
+
573
+
574
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
575
+ INSTALL_DIR = TrazaVet.InstallDir = get_install_dir()
576
+
577
+
578
+ if __name__ == '__main__':
579
+
580
+ # ajusto el encoding por defecto (si se redirije la salida)
581
+ if not hasattr(sys.stdout, "encoding") or sys.stdout.encoding is None:
582
+ import codecs
583
+ import locale
584
+ sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout, "replace")
585
+ sys.stderr = codecs.getwriter(locale.getpreferredencoding())(sys.stderr, "replace")
586
+
587
+ if '--register' in sys.argv or '--unregister' in sys.argv:
588
+ import pythoncom
589
+ import win32com.server.register
590
+ win32com.server.register.UseCommandLine(TrazaVet)
591
+ elif "/Automate" in sys.argv:
592
+ # MS seems to like /automate to run the class factories.
593
+ import win32com.server.localserver
594
+ # win32com.server.localserver.main()
595
+ # start the server.
596
+ win32com.server.localserver.serve([TrazaVet._reg_clsid_])
597
+ else:
598
+ main()
app/pyafipws/typelib/pyafipws.idl ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // pyafipws.idl: source PyAfipWs COM Type Library
3
+ // influenced by msdn example2.idl and pythoncom's pippo.idl
4
+ //
5
+
6
+ import "unknwn.idl","oaidl.idl";
7
+
8
+ [
9
+ uuid(65D24842-8691-4FB5-9B52-82393F12F4C6),
10
+ helpstring("PyAfipWs WSAA interface"),
11
+ pointer_default(unique),
12
+ dual,
13
+ object
14
+ //oleautomation
15
+ ]
16
+ interface IWSAA : IDispatch
17
+ {
18
+ [id(1)] HRESULT CreateTRA([in] BSTR service, [in] int ttl,
19
+ [out, retval] BSTR * tra);
20
+ [id(2)] HRESULT SignTRA([in] BSTR tra, [in] BSTR cert, [in] BSTR privatekey,
21
+ [out, retval] BSTR * cms);
22
+ [id(3)] HRESULT CallWSAA([in] BSTR cms, [in] BSTR proxy, [in] BSTR url,
23
+ [out, retval] BSTR * ta);
24
+
25
+ [propget, id(4)] HRESULT Token( [out, retval] BSTR * token);
26
+ [propget, id(5)] HRESULT Sign( [out, retval] BSTR * sign);
27
+ [propget, id(6)] HRESULT Version( [out, retval] BSTR * sign);
28
+ [propget, id(7)] HRESULT XmlResponse( [out, retval] BSTR * sign);
29
+
30
+ }; //end interface def
31
+
32
+
33
+ [
34
+ uuid(84A3B5EC-D019-4B27-8DB8-469D8D8E97D1),
35
+ version(1.02),
36
+ helpstring("PyAfipWs Type Library"),
37
+ ] library PyAfipWs
38
+ {
39
+ importlib("stdole32.tlb");
40
+
41
+ [
42
+ uuid(25949BFC-AE90-43D5-A577-C18F65F86B26),
43
+ helpstring("WSAA Component Class")
44
+ ] coclass WSAA
45
+ {
46
+ [default]interface IWSAA;
47
+ }
48
+ }; //end library def
app/pyafipws/typelib/trazamed.idl ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // trazamed.idl: source PyAfipWs TrazaMed (ANMAT/PAMI) COM Type Library
3
+ // influenced by msdn example2.idl and pythoncom's pippo.idl
4
+ //
5
+ // Use "print pythoncom.CreateGuid()" to make a new uuid
6
+ // Use MIDL.EXE to compile this file to create TrazaMed.TLB
7
+
8
+ import "unknwn.idl","oaidl.idl";
9
+
10
+ [
11
+ uuid(423FA890-1FE5-4F53-B5F7-71A7737EDB8B),
12
+ helpstring("PyAfipWs TrazaMed ANMAT / PAMI interface"),
13
+ pointer_default(unique),
14
+ dual,
15
+ object
16
+ //oleautomation
17
+ ]
18
+ interface ITrazaMed : IDispatch
19
+ {
20
+ [id(1)] HRESULT Conectar([in] BSTR cache, [in] BSTR wsdl,
21
+ [in] BSTR proxy, [in] BSTR wrapper, [in] BSTR cacert,
22
+ [out, retval] BOOL * ok);
23
+ [id(2)] HRESULT SendMedicamentos([in] BSTR usuario, [in] BSTR password,
24
+ [in] BSTR f_evento, [in] BSTR h_evento, [in] BSTR gln_origen, [in] BSTR gln_destino,
25
+ [in] BSTR n_remito, [in] BSTR n_factura, [in] BSTR vencimiento, [in] BSTR gtin, [in] BSTR lote,
26
+ [in] BSTR numero_serial, [in] BSTR id_obra_social, [in] BSTR id_evento,
27
+ [in] BSTR cuit_origen, [in] BSTR cuit_destino, [in] BSTR apellido, [in] BSTR nombres,
28
+ [in] BSTR tipo_docmento, [in] BSTR n_documento, [in] BSTR sexo,
29
+ [in] BSTR direccion, [in] BSTR numero, [in] BSTR piso, [in] BSTR depto, [in] BSTR localidad, [in] BSTR provincia,
30
+ [in] BSTR n_postal, [in] BSTR fecha_nacimiento, [in] BSTR telefono,
31
+ [out, retval] BOOL * ok);
32
+ [id(3)] HRESULT SendMedicamentosDHSerie([in] BSTR usuario, [in] BSTR password,
33
+ [in] BSTR f_evento, [in] BSTR h_evento, [in] BSTR gln_origen, [in] BSTR gln_destino,
34
+ [in] BSTR n_remito, [in] BSTR n_factura, [in] BSTR vencimiento, [in] BSTR gtin, [in] BSTR lote,
35
+ [in] BSTR desde_numero_serial, [in] BSTR hasta_numero_serial, [in] BSTR id_obra_social, [in] BSTR id_evento,
36
+ [in] BSTR cuit_origen, [in] BSTR cuit_destino, [in] BSTR apellido, [in] BSTR nombres,
37
+ [in] BSTR tipo_docmento, [in] BSTR n_documento, [in] BSTR sexo,
38
+ [in] BSTR direccion, [in] BSTR numero, [in] BSTR piso, [in] BSTR depto, [in] BSTR localidad, [in] BSTR provincia,
39
+ [in] BSTR n_postal, [in] BSTR fecha_nacimiento, [in] BSTR telefono,
40
+ [out, retval] BOOL * ok);
41
+ [id(4)] HRESULT SendCancelacTransacc([in] BSTR usuario, [in] BSTR password, [in] BSTR codigo_transaccion,
42
+ [out, retval] BOOL * ok);
43
+
44
+ [id(5)] HRESULT LeerError([out, retval] BSTR * texto);
45
+
46
+ [propget, id(6)] HRESULT InstallDir( [out, retval] BSTR * val);
47
+ [propget, id(7)] HRESULT Traceback( [out, retval] BSTR * val);
48
+ [propget, id(8)] HRESULT Excepcion( [out, retval] BSTR * val);
49
+ [propget, id(9)] HRESULT LanzarExcepciones( [in] BOOL modo, [out, retval] BOOL * val);
50
+ [propget, id(10)] HRESULT Version( [out, retval] BSTR * sign);
51
+ [propget, id(11)] HRESULT XmlRequest( [out, retval] BSTR * val);
52
+ [propget, id(12)] HRESULT XmlResponse( [out, retval] BSTR * val);
53
+ [propput, id(13)] HRESULT Username( [in] BSTR val);
54
+ [propput, id(14)] HRESULT Password( [in] BSTR val);
55
+ [propget, id(15)] HRESULT CodigoTransaccion( [out, retval] BSTR * codigo);
56
+ [propget, id(16)] HRESULT Resultado( [out, retval] BSTR * resultado);
57
+
58
+ [id(17)] HRESULT SetUsername([in] BSTR username);
59
+ [id(18)] HRESULT SetPassword([in] BSTR password);
60
+ [id(19)] HRESULT GetCodigoTransaccion([out, retval] BSTR * texto);
61
+ [id(20)] HRESULT GetResultado([out, retval] BSTR * texto);
62
+
63
+ }; //end interface def
64
+
65
+ [
66
+ uuid(F992EB7E-AFBD-41BB-B717-5693D3A2BADB),
67
+ version(1.5),
68
+ helpstring("PyAfipWs TrazaMed 1.05 Type Library"),
69
+ ] library PyAfipWsTrazaMedLib
70
+ {
71
+ importlib("stdole32.tlb");
72
+
73
+ [
74
+ uuid(EC779985-0CED-44A5-B44D-057FCA9E3389),
75
+ helpstring("TrazaMed Component Class")
76
+ ] coclass TrazaMed
77
+ {
78
+ [default]interface ITrazaMed;
79
+ }
80
+ }; //end library def
app/pyafipws/typelib/trazamed.tlb ADDED
Binary file (5 kB). View file
 
app/pyafipws/typelib/wsaa.idl ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // wsaa.idl: source PyAfipWs WSAA COM Type Library
3
+ // influenced by msdn example2.idl and pythoncom's pippo.idl
4
+ //
5
+ // Use "print pythoncom.CreateGuid()" to make a new one.
6
+
7
+ import "unknwn.idl","oaidl.idl";
8
+
9
+ [
10
+ uuid(95392BD9-54C3-4334-A045-A069BAF7FB97),
11
+ helpstring("PyAfipWs WSAA interface"),
12
+ pointer_default(unique),
13
+ dual,
14
+ object
15
+ //oleautomation
16
+ ]
17
+ interface IWSAA : IDispatch
18
+ {
19
+ [id(1)] HRESULT CreateTRA([in] BSTR service, [in] int ttl,
20
+ [out, retval] BSTR * tra);
21
+ [id(2)] HRESULT SignTRA([in] BSTR tra, [in] BSTR cert, [in] BSTR privatekey,
22
+ [out, retval] BSTR * cms);
23
+ [id(3)] HRESULT CallWSAA([in] BSTR cms, [in] BSTR proxy, [in] BSTR url,
24
+ [out, retval] BSTR * ta);
25
+
26
+ [propget, id(4)] HRESULT Token( [out, retval] BSTR * token);
27
+ [propget, id(5)] HRESULT Sign( [out, retval] BSTR * sign);
28
+ [propget, id(6)] HRESULT Version( [out, retval] BSTR * sign);
29
+ [propget, id(7)] HRESULT XmlResponse( [out, retval] BSTR * sign);
30
+
31
+ [id(8)] HRESULT Conectar([in] BSTR cache, [in] BSTR wsdl,
32
+ [in] BSTR proxy, [in] BSTR wrapper, [in] BSTR cacert,
33
+ [in] int timeout,
34
+ [out, retval] BOOL * ok);
35
+ [id(9)] HRESULT LoginCMS([in] BSTR cms, [out, retval] BSTR * ta);
36
+ [id(10)] HRESULT AnalizarXml([in] BSTR xml, [out, retval] BOOL * ok);
37
+ [id(11)] HRESULT ObtenerTagXml([in] BSTR tags, [out, retval] BSTR * texto);
38
+ [id(12)] HRESULT Expirado([in] BSTR fecha, [out, retval] BOOL * ok);
39
+
40
+ [propget, id(13)] HRESULT InstallDir( [out, retval] BSTR * val);
41
+ [propget, id(14)] HRESULT Traceback( [out, retval] BSTR * val);
42
+ [propget, id(15)] HRESULT Excepcion( [out, retval] BSTR * val);
43
+ [propget, id(16)] HRESULT SoapFault( [out, retval] BSTR * val);
44
+ [propput, id(17)] HRESULT LanzarExcepciones( [in] BOOL * val);
45
+ [propget, id(18)] HRESULT XmlRequest( [out, retval] BSTR * val);
46
+
47
+ [id(19)] HRESULT AnalizarCertificado(
48
+ [in] BSTR crt,
49
+ [in] BOOL binary,
50
+ [out, retval] BOOL * ok);
51
+
52
+ [propget, id(20)] HRESULT Identidad( [out, retval] BSTR * val);
53
+ [propget, id(21)] HRESULT Caducidad( [out, retval] BSTR * val);
54
+ [propget, id(22)] HRESULT Emisor( [out, retval] BSTR * val);
55
+ [propget, id(27)] HRESULT CertX509( [out, retval] BSTR * val);
56
+
57
+ [id(23)] HRESULT CrearClavePrivada(
58
+ [in] BSTR filename,
59
+ [in] int key_length,
60
+ [in] long pub_exponent,
61
+ [in] BSTR passphrase,
62
+ [out, retval] void
63
+ );
64
+
65
+ [id(24)] HRESULT CrearPedidoCertificado(
66
+ [in] BSTR cuit,
67
+ [in] BSTR empresa,
68
+ [in] BSTR nombre,
69
+ [in] BSTR filename,
70
+ [out, retval] void
71
+ );
72
+
73
+ [id(25)] HRESULT SetParametro(
74
+ [in] BSTR clave,
75
+ [in] BSTR valor,
76
+ [out, retval] BSTR * ok);
77
+
78
+ [id(26)] HRESULT Autenticar(
79
+ [in] BSTR service,
80
+ [in] BSTR crt,
81
+ [in] BSTR key,
82
+ [in] BSTR wsdl,
83
+ [in] BSTR proxy,
84
+ [in] BSTR wrapper,
85
+ [in] BSTR cacert,
86
+ [in] BOOL debug,
87
+ [out, retval] BSTR * TA);
88
+
89
+ }; //end interface def
90
+
91
+ [
92
+ uuid(30E9C94B-7385-4534-9A80-DF50FD169253),
93
+ version(2.11),
94
+ helpstring("PyAfipWs WSAA 2.11 Type Library"),
95
+ ] library PyAfipWsWSAALib
96
+ {
97
+ importlib("stdole32.tlb");
98
+
99
+ [
100
+ uuid(FC5FDFC3-1F41-4E4D-A346-5459DE1CE973),
101
+ helpstring("WSAA Component Class")
102
+ ] coclass WSAA
103
+ {
104
+ [default]interface IWSAA;
105
+ }
106
+ }; //end library def
app/pyafipws/typelib/wsaa.tlb ADDED
Binary file (4.7 kB). View file
 
app/pyafipws/typelib/wsfev1.idl ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // WSFEv1.idl: source PyAfipWs WSFEv1 COM Type Library
3
+ // influenced by msdn example2.idl and pythoncom's pippo.idl
4
+ //
5
+ // Use "print pythoncom.CreateGuid()" to make a new one.
6
+
7
+ import "unknwn.idl","oaidl.idl";
8
+
9
+ [
10
+ uuid(9588B307-67B7-4F27-8968-B78A37D342A2),
11
+ helpstring("PyAfipWs WSFEv1 interface"),
12
+ pointer_default(unique),
13
+ dual,
14
+ object
15
+ //oleautomation
16
+ ]
17
+ interface IWSFEv1 : IDispatch
18
+ {
19
+ [id( 1)] HRESULT CrearFactura(
20
+ [in] BSTR concepto,
21
+ [in] BSTR tipo_doc,
22
+ [in] BSTR nro_doc,
23
+ [in] BSTR tipo_cbte,
24
+ [in] BSTR punto_vta,
25
+ [in] BSTR cbt_desde,
26
+ [in] BSTR cbt_hasta,
27
+ [in] BSTR imp_total,
28
+ [in] BSTR imp_tot_conc,
29
+ [in] BSTR imp_neto,
30
+ [in] BSTR imp_iva,
31
+ [in] BSTR imp_trib,
32
+ [in] BSTR imp_op_ex,
33
+ [in] BSTR fecha_cbte,
34
+ [in] BSTR fecha_venc_pago,
35
+ [in] BSTR fecha_serv_desde,
36
+ [in] BSTR fecha_serv_hasta,
37
+ [in] BSTR moneda_id,
38
+ [in] BSTR moneda_ctz,
39
+ [in] BSTR caea,
40
+ [out, retval] BOOL * ok);
41
+ [id( 2)] HRESULT AgregarIva(
42
+ [in] BSTR iva_id,
43
+ [in] BSTR base_imp,
44
+ [in] BSTR importe,
45
+ [out, retval] BOOL * ok);
46
+ [id( 3)] HRESULT CAESolicitar(
47
+ [out, retval] BSTR * cae);
48
+ [id( 4)] HRESULT AgregarTributo(
49
+ [in] BSTR tributo_id,
50
+ [in] BSTR desc,
51
+ [in] BSTR base_imp,
52
+ [in] BSTR alic,
53
+ [in] BSTR importe,
54
+ [out, retval] BOOL * ok);
55
+ [id( 5)] HRESULT AgregarCmpAsoc(
56
+ [in] BSTR tipo,
57
+ [in] BSTR pto_vta,
58
+ [in] BSTR nro,
59
+ [out, retval] BOOL * ok);
60
+ [id( 6)] HRESULT CompUltimoAutorizado(
61
+ [in] BSTR tipo_cbte,
62
+ [in] BSTR punto_vta,
63
+ [out, retval] BSTR * cbte_nro);
64
+ [id( 7)] HRESULT CompConsultar(
65
+ [in] BSTR tipo_cbte,
66
+ [in] BSTR punto_vta,
67
+ [in] BSTR cbte_nro,
68
+ [in] BSTR reproceso,
69
+ [out, retval] BSTR * cae);
70
+ [id( 8)] HRESULT CAEASolicitar(
71
+ [in] BSTR periodo,
72
+ [in] BSTR orden,
73
+ [out, retval] BSTR * caea);
74
+ [id( 9)] HRESULT CAEAConsultar(
75
+ [in] BSTR periodo,
76
+ [in] BSTR orden,
77
+ [out, retval] BSTR * caea);
78
+ [id(10)] HRESULT CAEARegInformativo(
79
+ [out, retval] BSTR * caea);
80
+ [id(11)] HRESULT CAEASinMovimientoInformar(
81
+ [in] BSTR punto_vta,
82
+ [in] BSTR caea,
83
+ [out, retval] BOOL * ok);
84
+ [id(12)] HRESULT ParamGetTiposCbte(
85
+ [in] BSTR sep,
86
+ [out, retval] VARIANT * ret);
87
+ [id(13)] HRESULT ParamGetTiposConcepto(
88
+ [in] BSTR sep,
89
+ [out, retval] VARIANT * ret);
90
+ [id(14)] HRESULT ParamGetTiposDoc(
91
+ [in] BSTR sep,
92
+ [out, retval] VARIANT * ret);
93
+ [id(15)] HRESULT ParamGetTiposIva(
94
+ [in] BSTR sep,
95
+ [out, retval] VARIANT * ret);
96
+ [id(16)] HRESULT ParamGetTiposMonedas(
97
+ [in] BSTR sep,
98
+ [out, retval] VARIANT * ret);
99
+ [id(17)] HRESULT ParamGetTiposOpcional(
100
+ [in] BSTR sep,
101
+ [out, retval] VARIANT * ret);
102
+ [id(18)] HRESULT ParamGetTiposTributos(
103
+ [in] BSTR sep,
104
+ [out, retval] VARIANT * ret);
105
+ [id(19)] HRESULT ParamGetCotizacion(
106
+ [in] BSTR moneda_id,
107
+ [out, retval] BSTR * ctz);
108
+ [id(20)] HRESULT ParamGetPtosVenta(
109
+ [in] BSTR sep,
110
+ [out, retval] VARIANT * ret);
111
+ [id(21)] HRESULT AnalizarXml(
112
+ [in] BSTR xml,
113
+ [out, retval] BOOL * ok);
114
+ [id(22)] HRESULT ObtenerTagXml(
115
+ [out, retval] BOOL * ok);
116
+ [id(23)] HRESULT Dummy(
117
+ [out, retval] BOOL * ok);
118
+ [id(24)] HRESULT Conectar(
119
+ [in] BSTR cache,
120
+ [in] BSTR wsdl,
121
+ [in] BSTR proxy,
122
+ [in] BSTR wrapper,
123
+ [in] BSTR cacert,
124
+ [in] int timeout,
125
+ [out, retval] BOOL * ok);
126
+ [id(25)] HRESULT DebugLog(
127
+ [out, retval] BSTR * msg);
128
+
129
+ [propput, id(27)] HRESULT Token( [in] BSTR val);
130
+ [propput, id(28)] HRESULT Sign( [in] BSTR val);
131
+ [propput, id(29)] HRESULT Cuit( [in] BSTR val);
132
+ [propget, id(30)] HRESULT AppServerStatus( [out, retval] BSTR * val);
133
+ [propget, id(31)] HRESULT DbServerStatus( [out, retval] BSTR * val);
134
+ [propget, id(32)] HRESULT AuthServerStatus( [out, retval] BSTR * val);
135
+ [propget, id(33)] HRESULT XmlRequest( [out, retval] BSTR * val);
136
+ [propget, id(34)] HRESULT XmlResponse( [out, retval] BSTR * val);
137
+ [propget, id(35)] HRESULT Version( [out, retval] BSTR * val);
138
+ [propget, id(36)] HRESULT Excepcion( [out, retval] BSTR * val);
139
+ [propput, id(37)] HRESULT LanzarExcepciones( [in] BOOL * val);
140
+ [propget, id(38)] HRESULT Resultado( [out, retval] BSTR * val);
141
+ [propget, id(39)] HRESULT Obs( [out, retval] BSTR * val);
142
+ [propget, id(40)] HRESULT Observaciones( [out, retval] VARIANT * val);
143
+ [propget, id(41)] HRESULT Traceback( [out, retval] BSTR * val);
144
+ [propget, id(42)] HRESULT InstallDir( [out, retval] BSTR * val);
145
+ [propget, id(43)] HRESULT CAE( [out, retval] BSTR * val);
146
+ [propget, id(44)] HRESULT Vencimiento( [out, retval] BSTR * val);
147
+ [propget, id(45)] HRESULT Eventos( [out, retval] VARIANT * val);
148
+ [propget, id(46)] HRESULT Errores( [out, retval] VARIANT * val);
149
+ [propget, id(47)] HRESULT ErrCode( [out, retval] BSTR * val);
150
+ [propget, id(48)] HRESULT ErrMsg( [out, retval] BSTR * val);
151
+ [propput, id(49)] HRESULT Reprocesar( [in] BSTR val);
152
+ [propget, id(50)] HRESULT Reproceso( [out, retval] BSTR * val);
153
+ [propget, id(51)] HRESULT EmisionTipo( [out, retval] BSTR * val);
154
+ [propget, id(52)] HRESULT CAEA( [out, retval] BSTR * val);
155
+ [propget, id(53)] HRESULT CbteNro( [out, retval] BSTR * val);
156
+ [propget, id(54)] HRESULT CbtDesde( [out, retval] BSTR * val);
157
+ [propget, id(55)] HRESULT CbtHasta( [out, retval] BSTR * val);
158
+ [propget, id(56)] HRESULT FechaCbte( [out, retval] BSTR * val);
159
+ [propget, id(57)] HRESULT ImpTotal( [out, retval] BSTR * val);
160
+ [propget, id(58)] HRESULT ImpNeto( [out, retval] BSTR * val);
161
+ [propget, id(59)] HRESULT ImptoLiq( [out, retval] BSTR * val);
162
+ [propget, id(60)] HRESULT ImpOpExImptIVA( [out, retval] BSTR * val);
163
+ [propget, id(61)] HRESULT ImpOpEx( [out, retval] BSTR * val);
164
+ [propget, id(62)] HRESULT ImpTrib( [out, retval] BSTR * val);
165
+
166
+ [id(63)] HRESULT SetParametros(
167
+ [in] BSTR cuit,
168
+ [in] BSTR token,
169
+ [in] BSTR sign,
170
+ [out, retval] BOOL * ok);
171
+
172
+ [id(64)] HRESULT GetParametro(
173
+ [in] BSTR clave,
174
+ [out, retval] BSTR * ok);
175
+
176
+ [id(65)] HRESULT SetParametro(
177
+ [in] BSTR clave,
178
+ [in] BSTR valor,
179
+ [out, retval] BSTR * ok);
180
+
181
+ [id(66)] HRESULT EstablecerCampoFactura(
182
+ [in] BSTR campo,
183
+ [in] BSTR valor,
184
+ [out, retval] BOOL * ok);
185
+
186
+ [id(67)] HRESULT ObtenerCampoFactura(
187
+ [in] BSTR campo,
188
+ [out, retval] BSTR * valor);
189
+
190
+ [id(68)] HRESULT SetTicketAcceso(
191
+ [in] BSTR ta,
192
+ [out, retval] BOOL * ok);
193
+
194
+ [id(69)] HRESULT AgregarOpcional(
195
+ [in] BSTR opcional_id,
196
+ [in] BSTR valor,
197
+ [out, retval] BOOL * ok);
198
+
199
+ [propget, id(70)] HRESULT ImpIVA( [out, retval] BSTR * val);
200
+
201
+
202
+ [id(71)] HRESULT CAESolicitarX([out, retval] int * cant_reg);
203
+ [id(72)] HRESULT IniciarFacturasX([out, retval] BOOL * ok);
204
+ [id(73)] HRESULT AgregarFacturaX([out, retval] BOOL * ok);
205
+ [id(74)] HRESULT LeerFacturaX([in] int indice,
206
+ [out, retval] BOOL * ok);
207
+
208
+ }; //end interface def
209
+
210
+ [
211
+ uuid(B1D7283C-3EC2-463E-89B4-11F5228E2A15),
212
+ version(1.18),
213
+ helpstring("PyAfipWs WSFEv1 1.18 Type Library"),
214
+ ] library PyAfipWsWSFEv1Lib
215
+ {
216
+ importlib("stdole32.tlb");
217
+
218
+ [
219
+ uuid(D50B5D14-621E-4473-AC63-8A7B5E9DD57F),
220
+ helpstring("WSFEv1 Component Class")
221
+ ] coclass WSFEv1
222
+ {
223
+ [default]interface IWSFEv1;
224
+ }
225
+ }; //end library def
app/pyafipws/typelib/wsfev1.tlb ADDED
Binary file (8.99 kB). View file
 
app/pyafipws/utils.py ADDED
@@ -0,0 +1,936 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by the
6
+ # Free Software Foundation; version 3.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MΓ³dulo con funciones auxiliares para el manejo de errores y temas comunes"
14
+
15
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
16
+ __copyright__ = "Copyright (C) 2013 Mariano Reingart"
17
+ __license__ = "GPL 3.0"
18
+
19
+ from io import IOBase
20
+ import datetime
21
+ import functools
22
+ import inspect
23
+ import locale
24
+ import socket
25
+ import sys
26
+ import os
27
+ import stat
28
+ import time
29
+ import traceback
30
+ import warnings
31
+ from io import StringIO
32
+ from decimal import Decimal
33
+ from urllib.parse import urlencode
34
+ from urllib.parse import urlparse
35
+ import unicodedata
36
+ import mimetypes
37
+ from email.generator import _make_boundary
38
+ from html.parser import HTMLParser
39
+ from http.cookies import SimpleCookie
40
+ from configparser import ConfigParser as SafeConfigParser
41
+
42
+ from pysimplesoap.client import SimpleXMLElement, SoapClient, SoapFault, parse_proxy, set_http_wrapper
43
+ from pkg_resources import parse_version
44
+
45
+ try:
46
+ import json
47
+ except ImportError:
48
+ try:
49
+ import simplejson as json
50
+ except BaseException:
51
+ print("para soporte de JSON debe instalar simplejson")
52
+ json = None
53
+
54
+ import httplib2
55
+
56
+ DEBUG = False
57
+
58
+
59
+ # Funciones para manejo de errores:
60
+
61
+
62
+ def exception_info(current_filename=None, index=-1):
63
+ "Analizar el traceback y armar un dict con la info amigable user-friendly"
64
+ # guardo el traceback original (por si hay una excepciΓ³n):
65
+ info = sys.exc_info() # exc_type, exc_value, exc_traceback
66
+ # importante: no usar unpacking porque puede causar memory leak
67
+ if not current_filename:
68
+ # genero un call stack para ver quien me llamΓ³ y limitar la traza:
69
+ # advertencia: esto es necesario ya que en py2exe no tengo __file__
70
+ try:
71
+ raise ZeroDivisionError
72
+ except ZeroDivisionError:
73
+ f = sys.exc_info()[2].tb_frame.f_back
74
+ current_filename = os.path.normpath(os.path.abspath(f.f_code.co_filename))
75
+
76
+ # extraer la ΓΊltima traza del archivo solicitado:
77
+ # (ΓΊtil para no alargar demasiado la traza con lineas de las librerΓ­as)
78
+ ret = {'filename': "", 'lineno': 0, 'function_name': "", 'code': ""}
79
+ try:
80
+ for (filename, lineno, fn, text) in traceback.extract_tb(info[2]):
81
+ if os.path.normpath(os.path.abspath(filename)) == current_filename:
82
+ ret = {'filename': filename, 'lineno': lineno,
83
+ 'function_name': fn, 'code': text}
84
+ except Exception as e:
85
+ pass
86
+ # obtengo el mensaje de excepcion tal cual lo formatea python:
87
+ # (para evitar errores de encoding)
88
+ try:
89
+ ret['msg'] = traceback.format_exception_only(*info[0:2])[0]
90
+ except BaseException:
91
+ ret['msg'] = '<no disponible>'
92
+ # obtener el nombre de la excepcion (ej. "NameError")
93
+ try:
94
+ ret['name'] = info[0].__name__
95
+ except BaseException:
96
+ ret['name'] = 'Exception'
97
+ # obtener la traza formateada como string:
98
+ try:
99
+ tb = traceback.format_exception(*info)
100
+ ret['tb'] = ''.join(tb)
101
+ except BaseException:
102
+ ret['tb'] = ""
103
+ return ret
104
+
105
+
106
+ def inicializar_y_capturar_excepciones(func):
107
+ "Decorador para inicializar y capturar errores (version para webservices)"
108
+ @functools.wraps(func)
109
+ def capturar_errores_wrapper(self, *args, **kwargs):
110
+ try:
111
+ # inicializo (limpio variables)
112
+ self.Errores = [] # listas de str para lenguajes legados
113
+ self.Observaciones = []
114
+ self.errores = [] # listas de dict para usar en python
115
+ self.observaciones = []
116
+ self.Eventos = []
117
+ self.Traceback = self.Excepcion = ""
118
+ self.ErrCode = self.ErrMsg = self.Obs = ""
119
+ # limpio variables especificas del webservice:
120
+ self.inicializar()
121
+ # actualizo los parΓ‘metros
122
+ kwargs.update(self.params_in)
123
+ # limpio los parΓ‘metros
124
+ self.params_in = {}
125
+ self.params_out = {}
126
+ # llamo a la funciΓ³n (con reintentos)
127
+ retry = self.reintentos + 1
128
+ while retry:
129
+ try:
130
+ retry -= 1
131
+ return func(self, *args, **kwargs)
132
+ except socket.error as e:
133
+ if e.errno not in (10054, 10053):
134
+ # solo reintentar si el error es de conexiΓ³n
135
+ # (10054, 'Connection reset by peer')
136
+ # (10053, 'Software caused connection abort')
137
+ raise
138
+ else:
139
+ if DEBUG:
140
+ print(e, "Reintentando...")
141
+ self.log(exception_info().get("msg", ""))
142
+
143
+ except SoapFault as e:
144
+ # guardo destalle de la excepciΓ³n SOAP
145
+ self.ErrCode = str(e.faultcode)
146
+ self.ErrMsg = str(e.faultstring)
147
+ self.Excepcion = "%s: %s" % (e.faultcode, e.faultstring, )
148
+ if self.LanzarExcepciones:
149
+ raise
150
+ except Exception as e:
151
+ ex = exception_info()
152
+ self.Traceback = ex.get("tb", "")
153
+ try:
154
+ self.Excepcion = ex.get("msg", "")
155
+ except BaseException:
156
+ self.Excepcion = "<no disponible>"
157
+ if self.LanzarExcepciones:
158
+ raise
159
+ else:
160
+ self.ErrMsg = self.Excepcion
161
+ finally:
162
+ # guardo datos de depuraciΓ³n
163
+ if self.client:
164
+ self.XmlRequest = self.client.xml_request
165
+ self.XmlResponse = self.client.xml_response
166
+ return capturar_errores_wrapper
167
+
168
+
169
+ def inicializar_y_capturar_excepciones_simple(func):
170
+ "Decorador para inicializar y capturar errores (versiΓ³n bΓ‘sica indep.)"
171
+ @functools.wraps(func)
172
+ def capturar_errores_wrapper(self, *args, **kwargs):
173
+ self.inicializar()
174
+ try:
175
+ return func(self, *args, **kwargs)
176
+ except BaseException:
177
+ ex = exception_info()
178
+ self.Excepcion = ex['name']
179
+ self.Traceback = ex['msg']
180
+ if self.LanzarExcepciones:
181
+ raise
182
+ else:
183
+ return False
184
+ return capturar_errores_wrapper
185
+
186
+
187
+ class BaseWS:
188
+ "Infraestructura basica para interfaces webservices de AFIP"
189
+
190
+ def __init__(self, reintentos=1):
191
+ self.reintentos = reintentos
192
+ self.xml = self.client = self.Log = None
193
+ self.params_in = {}
194
+ self.inicializar()
195
+ self.Token = self.Sign = ""
196
+ self.LanzarExcepciones = True
197
+
198
+ def inicializar(self):
199
+ self.Excepcion = self.Traceback = ""
200
+ self.XmlRequest = self.XmlResponse = ""
201
+
202
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30, soap_server=None):
203
+ "Conectar cliente soap del web service"
204
+ try:
205
+ # analizar transporte y servidor proxy:
206
+ if wrapper:
207
+ Http = set_http_wrapper(wrapper)
208
+ self.Version = self.Version + " " + Http._wrapper_version
209
+ if isinstance(proxy, dict):
210
+ proxy_dict = proxy
211
+ else:
212
+ proxy_dict = parse_proxy(proxy)
213
+ self.log("Proxy Dict: %s" % str(proxy_dict))
214
+ if self.HOMO or not wsdl:
215
+ wsdl = self.WSDL
216
+ # agregar sufijo para descargar descripciΓ³n del servicio ?WSDL o ?wsdl
217
+ if not wsdl.endswith(self.WSDL[-5:]) and wsdl.startswith("http"):
218
+ wsdl += self.WSDL[-5:]
219
+ if not cache or self.HOMO:
220
+ # use 'cache' from installation base directory
221
+ cache = os.path.join(self.InstallDir, 'cache')
222
+ # deshabilitar verificaciΓ³n cert. servidor si es nulo falso vacio
223
+ if not cacert:
224
+ cacert = None
225
+ elif cacert is True or cacert.lower() == 'default':
226
+ # usar certificados predeterminados que vienen en la biblioteca
227
+ try:
228
+ import certifi
229
+ cacert = certifi.where()
230
+ except ImportError:
231
+ cacert = os.path.join(httplib2.__path__[0], 'cacerts.txt')
232
+ elif cacert.startswith("-----BEGIN CERTIFICATE-----"):
233
+ pass
234
+ else:
235
+ if not os.path.exists(cacert):
236
+ self.log("Buscando CACERT en conf...")
237
+ cacert = os.path.join(self.InstallDir, "conf", os.path.basename(cacert))
238
+ if cacert and not os.path.exists(cacert):
239
+ self.log("No se encuentra CACERT: %s" % str(cacert))
240
+ warnings.warn("No se encuentra CACERT: %s" % str(cacert))
241
+ cacert = None # wrong version, certificates not found...
242
+ raise RuntimeError("Error de configuracion CACERT ver DebugLog")
243
+ return False
244
+
245
+ self.log("Conectando a wsdl=%s cache=%s proxy=%s" % (wsdl, cache, proxy_dict))
246
+ # analizar espacio de nombres (axis vs .net):
247
+ ns = 'ser' if self.WSDL[-5:] == "?wsdl" else None
248
+ self.client = SoapClient(
249
+ wsdl=wsdl,
250
+ cache=cache,
251
+ proxy=proxy_dict,
252
+ cacert=cacert,
253
+ timeout=timeout,
254
+ ns=ns, soap_server=soap_server,
255
+ trace="--trace" in sys.argv)
256
+ self.cache = cache # utilizado por WSLPG y WSAA (Ticket de Acceso)
257
+ self.wsdl = wsdl # utilizado por TrazaMed (para corregir el location)
258
+ # corrijo ubicaciΓ³n del servidor (puerto http 80 en el WSDL AFIP)
259
+ for service in list(self.client.services.values()):
260
+ for port in list(service['ports'].values()):
261
+ location = port['location']
262
+ if location and location.startswith("http://"):
263
+ warnings.warn("Corrigiendo WSDL ... %s" % location)
264
+ location = location.replace("http://", "https://").replace(":80", ":443")
265
+ # usar servidor real si en el WSDL figura "localhost"
266
+ localhost = 'https://localhost:'
267
+ if location.startswith(localhost):
268
+ url = urlparse(wsdl)
269
+ location = location.replace("localhost", url.hostname)
270
+ location = location.replace(":9051", ":443")
271
+ port['location'] = location
272
+ return True
273
+ except BaseException:
274
+ ex = traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2])
275
+ self.Traceback = ''.join(ex)
276
+ try:
277
+ self.Excepcion = traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0]
278
+ except BaseException:
279
+ self.Excepcion = "<no disponible>"
280
+ if self.LanzarExcepciones:
281
+ raise
282
+ return False
283
+
284
+ def log(self, msg):
285
+ "Dejar mensaje en bitacora de depuraciΓ³n (mΓ©todo interno)"
286
+ if not isinstance(msg, str):
287
+ msg = str(msg, 'utf8', 'ignore')
288
+ if not self.Log:
289
+ self.Log = StringIO()
290
+ self.Log.write(msg)
291
+ self.Log.write('\n\r')
292
+ if DEBUG:
293
+ warnings.warn(msg)
294
+
295
+ def DebugLog(self):
296
+ "Devolver y limpiar la bitΓ‘cora de depuraciΓ³n"
297
+ if self.Log:
298
+ msg = self.Log.getvalue()
299
+ # limpiar log
300
+ self.Log.close()
301
+ self.Log = None
302
+ else:
303
+ msg = ''
304
+ return msg
305
+
306
+ def LoadTestXML(self, xml):
307
+ "Cargar un archivo de pruebas con la respuesta simulada (depuraciΓ³n)"
308
+ # si el parametro es un nombre de archivo, cargar el contenido:
309
+ if os.path.exists(xml):
310
+ xml = open(xml).read()
311
+
312
+ class DummyHTTP:
313
+ def __init__(self, xml_response):
314
+ self.xml_response = xml_response
315
+
316
+ def request(self, location, method, body, headers):
317
+ return {}, self.xml_response
318
+ self.client.http = DummyHTTP(xml)
319
+
320
+ @property
321
+ def xml_request(self):
322
+ return self.XmlRequest
323
+
324
+ @property
325
+ def xml_response(self):
326
+ return self.XmlResponse
327
+
328
+ def AnalizarXml(self, xml=""):
329
+ "Analiza un mensaje XML (por defecto el ticket de acceso)"
330
+ try:
331
+ if not xml or xml == 'XmlResponse':
332
+ xml = self.XmlResponse
333
+ elif xml == 'XmlRequest':
334
+ xml = self.XmlRequest
335
+ self.xml = SimpleXMLElement(xml)
336
+ return True
337
+ except Exception as e:
338
+ self.Excepcion = traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0]
339
+ return False
340
+
341
+ def ObtenerTagXml(self, *tags):
342
+ "Busca en el Xml analizado y devuelve el tag solicitado"
343
+ # convierto el xml a un objeto
344
+ try:
345
+ if self.xml:
346
+ xml = self.xml
347
+ # por cada tag, lo busco segun su nombre o posiciΓ³n
348
+ for tag in tags:
349
+ xml = xml(tag) # atajo a getitem y getattr
350
+ # vuelvo a convertir a string el objeto xml encontrado
351
+ return str(xml)
352
+ except Exception as e:
353
+ self.Excepcion = traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0]
354
+
355
+ def SetParametros(self, cuit, token, sign):
356
+ "Establece un parΓ‘metro general"
357
+ self.Token = token
358
+ self.Sign = sign
359
+ self.Cuit = cuit
360
+ return True
361
+
362
+ @inicializar_y_capturar_excepciones
363
+ def SetTicketAcceso(self, ta_string):
364
+ "Establecer el token y sign desde un ticket de acceso XML"
365
+ if ta_string:
366
+ ta = SimpleXMLElement(ta_string)
367
+ self.Token = str(ta.credentials.token)
368
+ self.Sign = str(ta.credentials.sign)
369
+ return True
370
+ else:
371
+ raise RuntimeError("Ticket de Acceso vacio!")
372
+
373
+ def SetParametro(self, clave, valor):
374
+ "Establece un parΓ‘metro de entrada (a usarse en llamada posterior)"
375
+ # ΓΊtil para parΓ‘metros de entrada (por ej. VFP9 no soporta mΓ‘s de 27)
376
+ self.params_in[str(clave)] = valor
377
+ return True
378
+
379
+ def GetParametro(self, clave, clave1=None, clave2=None, clave3=None, clave4=None):
380
+ "Devuelve un parΓ‘metro de salida (establecido por llamada anterior)"
381
+ # ΓΊtil para parΓ‘metros de salida (por ej. campos de TransaccionPlainWS)
382
+ valor = self.params_out.get(clave)
383
+ # busco datos "anidados" (listas / diccionarios)
384
+ for clave in (clave1, clave2, clave3, clave4):
385
+ if clave is not None and valor is not None:
386
+ if isinstance(clave1, str) and clave.isdigit():
387
+ clave = int(clave)
388
+ try:
389
+ valor = valor[clave]
390
+ except (KeyError, IndexError):
391
+ valor = None
392
+ if valor is not None:
393
+ if isinstance(valor, str):
394
+ return valor
395
+ else:
396
+ return str(valor)
397
+ else:
398
+ return ""
399
+
400
+ def LeerError(self):
401
+ "Recorro los errores devueltos y devuelvo el primero si existe"
402
+
403
+ if self.Errores:
404
+ # extraigo el primer item
405
+ er = self.Errores.pop(0)
406
+ return er
407
+ else:
408
+ return ""
409
+
410
+
411
+ class WebClient:
412
+ "Minimal webservice client to do POST request with multipart encoded FORM data"
413
+
414
+ def __init__(self, location, enctype="multipart/form-data", trace=False,
415
+ cacert=None, timeout=30):
416
+ kwargs = {}
417
+ if parse_version(httplib2.__version__) >= parse_version('0.3.0'):
418
+ kwargs['timeout'] = timeout
419
+ if parse_version(httplib2.__version__) >= parse_version('0.7.0'):
420
+ kwargs['disable_ssl_certificate_validation'] = cacert is None
421
+ kwargs['ca_certs'] = cacert
422
+ self.http = httplib2.Http(**kwargs)
423
+ self.trace = trace
424
+ self.location = location
425
+ self.enctype = enctype
426
+ self.cookies = None
427
+ self.method = "POST"
428
+ self.referer = None
429
+
430
+ def multipart_encode(self, vars):
431
+ "Enconde form data (vars dict)"
432
+ boundary = _make_boundary()
433
+ buf = StringIO()
434
+ for key, value in list(vars.items()):
435
+ if not isinstance(value, IOBase):
436
+ buf.write('--%s\r\n' % boundary)
437
+ buf.write('Content-Disposition: form-data; name="%s"' % key)
438
+ buf.write('\r\n\r\n' + value + '\r\n')
439
+ else:
440
+ fd = value
441
+ file_size = os.fstat(fd.fileno())[stat.ST_SIZE]
442
+ filename = os.path.basename(fd.name)
443
+ contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
444
+ buf.write('--%s\r\n' % boundary)
445
+ buf.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename))
446
+ buf.write('Content-Type: %s\r\n' % contenttype)
447
+ # buffer += 'Content-Length: %s\r\n' % file_size
448
+ fd.seek(0)
449
+ buf.write('\r\n' + fd.read() + '\r\n')
450
+ buf.write('--' + boundary + '--\r\n\r\n')
451
+ buf = buf.getvalue()
452
+ return boundary, buf
453
+
454
+ def __call__(self, *args, **vars):
455
+ "Perform a GET/POST request and return the response"
456
+
457
+ location = self.location
458
+ # if isinstance(location, str):
459
+ # location = location.encode("utf8")
460
+ # extend the base URI with additional components
461
+ if args:
462
+ location += "/".join(args)
463
+ if self.method == "GET":
464
+ location += "?%s" % urlencode(vars)
465
+
466
+ # prepare the request content suitable to be sent to the server:
467
+ if self.enctype == "multipart/form-data":
468
+ boundary, body = self.multipart_encode(vars)
469
+ content_type = '%s; boundary=%s' % (self.enctype, boundary)
470
+ elif self.enctype == "application/x-www-form-urlencoded":
471
+ body = urlencode(vars)
472
+ content_type = self.enctype
473
+ else:
474
+ body = None
475
+
476
+ # add headers according method, cookies, etc.:
477
+ headers = {}
478
+ if self.method == "POST":
479
+ headers.update({
480
+ 'Content-type': content_type,
481
+ 'Content-length': str(len(body)),
482
+ })
483
+ if self.cookies:
484
+ headers['Cookie'] = self.cookies.output(attrs=(), header="", sep=";")
485
+ if self.referer:
486
+ headers['Referer'] = self.referer
487
+
488
+ if self.trace:
489
+ print("-" * 80)
490
+ print("%s %s" % (self.method, location))
491
+ print('\n'.join(["%s: %s" % (k, v) for k, v in list(headers.items())]))
492
+ print("\n%s" % body)
493
+
494
+ # send the request to the server and store the result:
495
+ response, content = self.http.request(
496
+ location, self.method, body=body, headers=headers)
497
+ self.response = response
498
+ self.content = content
499
+
500
+ if self.trace:
501
+ print()
502
+ print('\n'.join(["%s: %s" % (k, v) for k, v in list(response.items())]))
503
+ print(content)
504
+ print("=" * 80)
505
+
506
+ # Parse and store the cookies (if any)
507
+ if "set-cookie" in self.response:
508
+ if not self.cookies:
509
+ self.cookies = SimpleCookie()
510
+ self.cookies.load(self.response["set-cookie"])
511
+
512
+ return content
513
+
514
+
515
+ class AttrDict(dict):
516
+ "Custom Dict to hold attributes and items"
517
+
518
+
519
+ class HTMLFormParser(HTMLParser):
520
+ "Convert HTML form into custom named-tuple dicts"
521
+
522
+ def __init__(self, *args, **kwargs):
523
+ HTMLParser.__init__(self, *args, **kwargs)
524
+ self.forms = {}
525
+
526
+ def handle_starttag(self, tag, attrs):
527
+ attrs = dict(attrs)
528
+ if 'name' in attrs:
529
+ name = attrs['name']
530
+ elif 'id' in attrs:
531
+ name = attrs['id']
532
+ else:
533
+ name = None
534
+ if tag == 'form':
535
+ form = AttrDict()
536
+ for k, v in list(attrs.items()):
537
+ setattr(form, "_%s" % k, v)
538
+ self.form = self.forms[name or len(self.forms)] = form
539
+ elif tag == 'input':
540
+ self.form[name or len(self.form)] = attrs.get('value')
541
+
542
+
543
+ # Funciones para manejo de archivos de texto de campos de ancho fijo:
544
+
545
+
546
+ def leer(linea, formato, expandir_fechas=False):
547
+ "Analiza una linea de texto dado un formato, devuelve un diccionario"
548
+ dic = {}
549
+ comienzo = 1
550
+ for fmt in formato:
551
+ clave, longitud, tipo = fmt[0:3]
552
+ dec = (len(fmt) > 3 and isinstance(fmt[3], int)) and fmt[3] or 2
553
+ valor = linea[comienzo - 1:comienzo - 1 + longitud].strip()
554
+ try:
555
+ if chr(8) in valor or chr(127) in valor or chr(255) in valor:
556
+ valor = None # nulo
557
+ elif tipo == N:
558
+ if valor:
559
+ valor = int(valor)
560
+ else:
561
+ valor = 0
562
+ elif tipo == I:
563
+ if valor:
564
+ try:
565
+ if '.' in valor:
566
+ valor = float(valor)
567
+ else:
568
+ valor = valor.strip(" ")
569
+ if valor[0] == "-":
570
+ sign = -1
571
+ valor = valor[1:]
572
+ else:
573
+ sign = +1
574
+ valor = sign * float(("%%s.%%0%sd" % dec) % (int(valor[:-dec] or '0'), int(valor[-dec:] or '0')))
575
+ except ValueError:
576
+ raise ValueError("Campo invalido: %s = '%s'" % (clave, valor))
577
+ else:
578
+ valor = 0.00
579
+ elif expandir_fechas and clave.lower().startswith("fec") and longitud <= 8:
580
+ if valor:
581
+ valor = "%s-%s-%s" % (valor[0:4], valor[4:6], valor[6:8])
582
+ else:
583
+ valor = None
584
+ else:
585
+ valor = valor.decode("ascii", "ignore")
586
+ if not valor and clave in dic and len(linea) <= comienzo:
587
+ pass # ignorar - compatibilidad hacia atrΓ‘s (cambios tamaΓ±o)
588
+ else:
589
+ dic[clave] = valor
590
+ comienzo += longitud
591
+ except Exception as e:
592
+ raise ValueError("Error al leer campo %s pos %s val '%s': %s" % (
593
+ clave, comienzo, valor, str(e)))
594
+ return dic
595
+
596
+
597
+ def escribir(dic, formato, contraer_fechas=False):
598
+ "Genera una cadena dado un formato y un diccionario de claves/valores"
599
+ linea = " " * sum([fmt[1] for fmt in formato])
600
+ comienzo = 1
601
+ for fmt in formato:
602
+ clave, longitud, tipo = fmt[0:3]
603
+ try:
604
+ dec = (len(fmt) > 3 and isinstance(fmt[3], int)) and fmt[3] or 2
605
+ if clave.capitalize() in dic:
606
+ clave = clave.capitalize()
607
+ s = dic.get(clave, "")
608
+ if isinstance(s, str):
609
+ s = s.encode("latin1")
610
+ if s is None:
611
+ valor = ""
612
+ else:
613
+ valor = str(s)
614
+ # reemplazo saltos de linea por tabulaci{on vertical
615
+ valor = valor.replace("\n\r", "\v").replace("\n", "\v").replace("\r", "\v")
616
+ if tipo == N and valor and valor != "NULL":
617
+ valor = ("%%0%dd" % longitud) % int(valor)
618
+ elif tipo == I and valor:
619
+ valor = ("%%0%d.%df" % (longitud + 1, dec) % float(valor)).replace(".", "")
620
+ elif contraer_fechas and clave.lower().startswith("fec") and longitud <= 8 and valor:
621
+ valor = valor.replace("-", "")
622
+ else:
623
+ valor = ("%%-0%ds" % longitud) % valor
624
+ linea = linea[:comienzo - 1] + valor + linea[comienzo - 1 + longitud:]
625
+ comienzo += longitud
626
+ except Exception as e:
627
+ warnings.warn("Error al escribir campo %s pos %s val '%s': %s" % (
628
+ clave, comienzo, valor, str(e)))
629
+ return linea + "\n"
630
+
631
+
632
+ # Tipos de datos (cΓ³digo RG1361)
633
+
634
+
635
+ N = 'Numerico' # 2
636
+ A = 'Alfanumerico' # 3
637
+ I = 'Importe' # 4
638
+ C = A # 1 (caracter alfabetico)
639
+ B = A # 9 (blanco)
640
+
641
+
642
+ # Funciones para manejo de tablas en DBF
643
+
644
+
645
+ def guardar_dbf(formatos, agrega=False, conf_dbf=None):
646
+ import dbf
647
+ if DEBUG:
648
+ print("Creando DBF...")
649
+
650
+ tablas = {}
651
+ for nombre, formato, l in formatos:
652
+ campos = []
653
+ claves = []
654
+ filename = conf_dbf.get(nombre.lower(), "%s.dbf" % nombre[:8])
655
+ if DEBUG:
656
+ print("=== tabla %s (%s) ===" % (nombre, filename))
657
+ for fmt in formato:
658
+ clave, longitud, tipo = fmt[0:3]
659
+ dec = len(fmt) > 3 and fmt[3] or (tipo == 'I' and '2' or '')
660
+ if longitud > 250:
661
+ tipo = "M" # memo!
662
+ elif tipo == A:
663
+ tipo = "C(%s)" % longitud
664
+ elif tipo == N:
665
+ if longitud >= 18:
666
+ longitud = 17
667
+ tipo = "N(%s,0)" % longitud
668
+ elif tipo == I:
669
+ if not dec:
670
+ dec = 0
671
+ else:
672
+ dec = int(dec)
673
+ if longitud >= 18:
674
+ longitud = 17
675
+ if longitud - 2 <= dec:
676
+ longitud += longitud - dec + 1 # ajusto long. decimales
677
+ tipo = "N(%s,%s)" % (longitud, dec)
678
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
679
+ campo = "%s %s" % (clave_dbf, tipo)
680
+ if DEBUG:
681
+ print(" * %s : %s" % (campo, clave))
682
+ campos.append(campo)
683
+ claves.append(clave_dbf)
684
+ if DEBUG:
685
+ print("leyendo tabla", nombre, filename)
686
+ if agrega:
687
+ tabla = dbf.Table(filename, campos)
688
+ else:
689
+ tabla = dbf.Table(filename)
690
+
691
+ for d in l:
692
+ # si no es un diccionario, ignorar ya que seguramente va en otra
693
+ # tabla (por ej. retenciones tiene su propio formato)
694
+ if isinstance(d, str):
695
+ continue
696
+ r = {}
697
+ claves = []
698
+ for fmt in formato:
699
+ clave, longitud, tipo = fmt[0:3]
700
+ if agrega or clave in d:
701
+ v = d.get(clave, None)
702
+ if DEBUG:
703
+ print(clave, v, tipo)
704
+ if v is None and tipo == A:
705
+ v = ''
706
+ if (v is None or v == '') and tipo in (I, N):
707
+ v = 0
708
+ if tipo == A:
709
+ if isinstance(v, str):
710
+ v = v.encode("ascii", "replace")
711
+ if isinstance(v, str):
712
+ v = v.decode("ascii", "replace").encode("ascii", "replace")
713
+ if not isinstance(v, str):
714
+ v = str(v)
715
+ if len(v) > longitud:
716
+ v = v[:longitud] # recorto el string para que quepa
717
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
718
+ claves.append(clave_dbf)
719
+ r[clave_dbf] = v
720
+ # agregar si lo solicitaron o si la tabla no tiene registros:
721
+ if agrega or not tabla:
722
+ if DEBUG:
723
+ print("Agregando !!!", r)
724
+ registro = tabla.append(r)
725
+ else:
726
+ if DEBUG:
727
+ print("Actualizando ", r)
728
+ reg = tabla.current()
729
+ for k, v in list(reg.scatter_fields().items()):
730
+ if k not in r:
731
+ r[k] = v
732
+ if DEBUG:
733
+ print("Actualizando ", r)
734
+ reg.write_record(**r)
735
+ # mover de registro para no actualizar siempre el primero:
736
+ if not tabla.eof() and len(l) > 1:
737
+ if DEBUG:
738
+ print("Moviendo al prΓ³ximo registro ", tabla.record_number)
739
+ next(tabla)
740
+ tabla.close()
741
+
742
+
743
+ def leer_dbf(formatos, conf_dbf):
744
+ import dbf
745
+ if DEBUG:
746
+ print("Leyendo DBF...")
747
+
748
+ for nombre, formato, ld in formatos:
749
+ filename = conf_dbf.get(nombre.lower(), "%s.dbf" % nombre[:8])
750
+ if DEBUG:
751
+ print("leyendo tabla", nombre, filename)
752
+ if not os.path.exists(filename):
753
+ continue
754
+ tabla = dbf.Table(filename)
755
+ for reg in tabla:
756
+ r = {}
757
+ d = reg.scatter_fields()
758
+ claves = []
759
+ for fmt in formato:
760
+ clave, longitud, tipo = fmt[0:3]
761
+ #import pdb; pdb.set_trace()
762
+ clave_dbf = dar_nombre_campo_dbf(clave, claves)
763
+ claves.append(clave_dbf)
764
+ v = d.get(clave_dbf)
765
+ r[clave] = v
766
+ if isinstance(ld, dict):
767
+ ld.update(r)
768
+ else:
769
+ ld.append(r)
770
+
771
+
772
+ def dar_nombre_campo_dbf(clave, claves):
773
+ "Reducir nombre de campo a 10 caracteres, sin espacios ni _, sin repetir"
774
+ # achico el nombre del campo para que quepa en la tabla:
775
+ nombre = clave.replace("_", "")[:10]
776
+ # si el campo esta repetido, le agrego un nΓΊmero
777
+ i = 0
778
+ while nombre in claves:
779
+ i += 1
780
+ nombre = nombre[:9] + str(i)
781
+ return nombre.lower()
782
+
783
+
784
+ def verifica(ver_list, res_dict, difs):
785
+ "Verificar que dos diccionarios sean iguales, actualiza lista diferencias"
786
+ for k, v in list(ver_list.items()):
787
+ # normalizo a float para poder comparar numericamente:
788
+ if isinstance(v, (Decimal, int)):
789
+ v = float(v)
790
+ if isinstance(res_dict.get(k), (Decimal, int)):
791
+ res_dict[k] = float(res_dict[k])
792
+ if isinstance(v, list):
793
+ # verifico que ambas listas tengan la misma cantidad de elementos:
794
+ if v and not k in res_dict and v:
795
+ difs.append("falta tag %s: %s %s" % (k, repr(v), repr(res_dict.get(k))))
796
+ elif len(res_dict.get(k, [])) != len(v or []):
797
+ difs.append("tag %s len !=: %s %s" % (k, repr(v), repr(res_dict.get(k))))
798
+ else:
799
+ # ordeno las listas para poder compararlas si vienen mezcladas
800
+ rl = sorted(res_dict.get(k, []))
801
+ # comparo los elementos uno a uno:
802
+ for i, vl in enumerate(sorted(v)):
803
+ verifica(vl, rl[i], difs)
804
+ elif isinstance(v, dict):
805
+ # comparo recursivamente los elementos:
806
+ verifica(v, res_dict.get(k, {}), difs)
807
+ elif res_dict.get(k) is None or v is None:
808
+ # alguno de los dos es nulo, verifico si ambos lo son o faltan
809
+ if v == "":
810
+ v = None
811
+ r = res_dict.get(k)
812
+ if r == "":
813
+ r = None
814
+ if not (r is None and v is None):
815
+ difs.append("%s: nil %s!=%s" % (k, repr(v), repr(r)))
816
+ elif isinstance(res_dict.get(k), type(v)):
817
+ # tipos iguales, los comparo directamente
818
+ if res_dict.get(k) != v:
819
+ difs.append("%s: %s!=%s" % (k, repr(v), repr(res_dict.get(k))))
820
+ elif isinstance(v, float) or isinstance(res_dict.get(k), float):
821
+ # comparar numericamente
822
+ if float(res_dict.get(k)) != float(v):
823
+ difs.append("%s: %s!=%s" % (k, repr(v), repr(res_dict.get(k))))
824
+ elif str(res_dict.get(k)) != str(v):
825
+ # tipos diferentes, comparo la representaciΓ³n
826
+ difs.append("%s: str %s!=%s" % (k, repr(v), repr(res_dict.get(k))))
827
+ else:
828
+ pass
829
+ # print "%s: %s==%s" % (k, repr(v), repr(res_dict[k]))
830
+
831
+
832
+ def safe_console():
833
+ if False and sys.stdout.encoding is None:
834
+ class SafeWriter:
835
+ def __init__(self, target):
836
+ self.target = target
837
+ self.encoding = 'utf-8'
838
+ self.errors = 'replace'
839
+ self.encode_to = 'latin-1'
840
+
841
+ def write(self, s):
842
+ self.target.write(self.intercept(s))
843
+
844
+ def flush(self):
845
+ self.target.flush()
846
+
847
+ def intercept(self, s):
848
+ if not isinstance(s, str):
849
+ s = s.decode(self.encode_to, self.errors)
850
+ return s.encode(self.encoding, self.errors)
851
+
852
+ sys.stdout = SafeWriter(sys.stdout)
853
+ #sys.stderr = SafeWriter(sys.stderr)
854
+ print("Encodign in %s" % locale.getpreferredencoding())
855
+
856
+
857
+ def norm(x, encoding="latin1"):
858
+ "Convertir acentos codificados en ISO 8859-1 u otro, a ASCII regular"
859
+ if not isinstance(x, str):
860
+ x = str(x)
861
+ elif isinstance(x, str):
862
+ x = x.decode(encoding, 'ignore')
863
+ return unicodedata.normalize('NFKD', x).encode('ASCII', 'ignore')
864
+
865
+
866
+ def date(fmt=None, timestamp=None):
867
+ "Manejo de fechas (simil PHP)"
868
+ if fmt == 'U': # return timestamp
869
+ t = datetime.datetime.now()
870
+ return int(time.mktime(t.timetuple()))
871
+ if fmt == 'c': # return isoformat
872
+ d = datetime.datetime.fromtimestamp(timestamp)
873
+ return d.isoformat()
874
+ if fmt == 'Ymd':
875
+ d = datetime.datetime.now()
876
+ return d.strftime("%Y%m%d")
877
+
878
+
879
+ def get_install_dir():
880
+ if not hasattr(sys, "frozen"):
881
+ basepath = __file__
882
+ elif sys.frozen == 'dll':
883
+ import win32api
884
+ basepath = win32api.GetModuleFileName(sys.frozendllhandle)
885
+ else:
886
+ basepath = sys.executable
887
+
888
+ if hasattr(sys, "frozen"):
889
+ # we are running as py2exe-packed executable
890
+ try:
891
+ import pythoncom
892
+ pythoncom.frozen = 1
893
+ except ModuleNotFoundError:
894
+ pass
895
+ sys.argv[0] = sys.executable
896
+
897
+ return os.path.dirname(os.path.abspath(basepath))
898
+
899
+
900
+ def abrir_conf(config_file, debug=False):
901
+ "Abrir el archivo de configuraciΓ³n (usar primer parΓ‘metro como ruta)"
902
+ # en principio, usar el nombre de archivo predeterminado
903
+ # si se pasa el archivo de configuraciΓ³n por parΓ‘metro, confirmar que exista
904
+ # y descartar que sea una opciΓ³n
905
+ if len(sys.argv) > 1:
906
+ if os.path.splitext(sys.argv[1])[1].lower() == ".ini":
907
+ config_file = sys.argv.pop(1)
908
+ if not os.path.exists(config_file) or not os.path.isfile(config_file):
909
+ warnings.warn("Archivo de configuracion %s invalido" % config_file)
910
+
911
+ if debug:
912
+ print("CONFIG_FILE:", config_file)
913
+
914
+ config = SafeConfigParser()
915
+ config.read(config_file, encoding="latin1")
916
+
917
+ return config
918
+
919
+
920
+ def json_serializer(obj):
921
+ if isinstance(obj, (datetime.datetime, datetime.date)):
922
+ return obj.isoformat()
923
+ else:
924
+ return str(obj)
925
+
926
+
927
+ if __name__ == "__main__":
928
+ print(get_install_dir())
929
+ try:
930
+ 1 / 0
931
+ except BaseException:
932
+ ex = exception_info()
933
+ print(ex)
934
+ assert ex['name'] == "ZeroDivisionError"
935
+ assert ex['lineno'] == 73
936
+ assert ex['tb']
app/pyafipws/venv.bat ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ rem CreaciοΏ½n del entorno virtual (opcional) para el proyecto PyAfipWs
4
+ rem 2015 (c) Mariano Reingart <reingart@gmail.com> - Licencia: GPLv3+
5
+
6
+ rem Nota: Es recomendable ejecutar este programa como Administrador
7
+ rem Ver https://code.google.com/p/pyafipws/wiki/InstalacionCodigoFuente
8
+
9
+ pip 1> NUL 2> NUL
10
+ if %ERRORLEVEL%==9009 (
11
+ echo Python 2.7.9 / PIP no puede ser ejecutado
12
+ echo Por favor instale: https://www.python.org/ftp/python/2.7.9/python-2.7.9.msi
13
+ echo Asegurese que el PATH contenga a C:\Python27 y C:\Python27\scripts
14
+ pause
15
+ start https://www.python.org/ftp/python/2.7.9/python-2.7.9.msi
16
+ exit 1
17
+ )
18
+ echo *** Instalar utilidades de instalaciοΏ½n / entorno virtual:
19
+
20
+ rem pip install --upgrade pip
21
+ pip install --upgrade wheel
22
+ pip install --upgrade virtualenv
23
+
24
+ echo *** Crear y activar el entorno virtual venv (en el directorio actual):
25
+
26
+ virtualenv venv
27
+ venv\Scripts\activate
28
+
29
+ echo *** Listo!, para salir del entorno ejecute deactivate
app/pyafipws/wdigdepfiel.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ import time
14
+ import sys
15
+ import os
16
+ from .utils import date, SimpleXMLElement, SoapClient, SoapFault
17
+ """MοΏ½dulo para interfaz Depositario Fiel web service wDigDepFiel de AFIP
18
+ """
19
+
20
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
21
+ __copyright__ = "Copyright (C) 2010 Mariano Reingart"
22
+ __license__ = "LGPL 3.0"
23
+ __version__ = "1.01"
24
+
25
+ LICENCIA = """
26
+ wdigdepfiel.py: Interfaz para Digitalizacion Depositario Fiel AFIP
27
+ Copyright (C) 2010 Mariano Reingart reingart@gmail.com
28
+
29
+ Este progarma es software libre, se entrega ABSOLUTAMENTE SIN GARANTIA
30
+ y es bienvenido a redistribuirlo bajo la licencia GPLv3.
31
+
32
+ Para informaciοΏ½n adicional sobre garantοΏ½a, soporte tοΏ½cnico comercial
33
+ e incorporaciοΏ½n/distribuciοΏ½n en programas propietarios ver PyAfipWs:
34
+ http://www.sistemasagiles.com.ar/trac/wiki/PyAfipWs
35
+ """
36
+
37
+
38
+ WSDDFURL = "https://testdia.afip.gov.ar/Dia/Ws/wDigDepFiel/wDigDepFiel.asmx"
39
+ SOAP_ACTION = 'ar.gov.afip.dia.serviciosWeb.wDigDepFiel/'
40
+ SOAP_NS = 'ar.gov.afip.dia.serviciosWeb.wDigDepFiel'
41
+
42
+ DEBUG = True
43
+ XML = False
44
+ HOMO = True
45
+
46
+
47
+ def dummy(client):
48
+ "Metodo dummy para verificacion de funcionamiento"
49
+ response = client.Dummy()
50
+ result = response.DummyResult
51
+ appserver = dbserver = authserver = None
52
+ try:
53
+ appserver = str(result.appserver)
54
+ dbserver = str(result.dbserver)
55
+ authserver = str(result.authserver)
56
+ except (RuntimeError, IndexError, AttributeError) as e:
57
+ pass
58
+ return {'appserver': appserver,
59
+ 'dbserver': dbserver,
60
+ 'authserver': authserver}
61
+
62
+
63
+ def aviso_recep_acept(client, token, sign, cuit, tipo_agente, rol,
64
+ nro_legajo, cuit_declarante, cuit_psad, cuit_ie,
65
+ codigo, fecha_hora_acept, ticket):
66
+ "Aviso de recepcion y aceptacion."
67
+ response = client.AvisoRecepAcept(
68
+ autentica=dict(Cuit=cuit, Token=token, Sign=sign, TipoAgente=tipo_agente, Rol=rol),
69
+ nroLegajo=nro_legajo,
70
+ cuitDeclarante=cuit_declarante,
71
+ cuitPSAD=cuit_psad,
72
+ cuitIE=cuit_ie,
73
+ codigo=codigo,
74
+ fechaHoraAcept=fecha_hora_acept,
75
+ ticket=ticket,
76
+ )
77
+ result = response.AvisoRecepAceptResult
78
+ return str(result.codError), str(result.descError)
79
+
80
+
81
+ def aviso_digit(client, token, sign, cuit, tipo_agente, rol,
82
+ nro_legajo, cuit_declarante, cuit_psad, cuit_ie, cuit_ata,
83
+ codigo, url, familias, ticket, hashing, cantidad_total):
84
+ "Aviso de digitalizacion."
85
+ response = client.AvisoDigit(
86
+ autentica=dict(Cuit=cuit, Token=token, Sign=sign, TipoAgente=tipo_agente, Rol=rol),
87
+ nroLegajo=nro_legajo,
88
+ cuitDeclarante=cuit_declarante,
89
+ cuitPSAD=cuit_psad,
90
+ cuitIE=cuit_ie,
91
+ cuitATA=cuit_ata,
92
+ codigo=codigo,
93
+ url=url,
94
+ familias=familias,
95
+ ticket=ticket,
96
+ hashing=hashing,
97
+ cantidadTotal=cantidad_total,
98
+ )
99
+ result = response.AvisoDigitResult
100
+ return str(result.codError), str(result.descError)
101
+
102
+
103
+ if __name__ == '__main__':
104
+ if '--ayuda' in sys.argv:
105
+ print(LICENCIA)
106
+ print(AYUDA)
107
+ sys.exit(0)
108
+
109
+ import csv
110
+ import traceback
111
+ import datetime
112
+
113
+ from . import wsaa
114
+
115
+ try:
116
+
117
+ if "--version" in sys.argv:
118
+ print("VersiοΏ½n: ", __version__)
119
+
120
+ CERT = 'reingart.crt'
121
+ PRIVATEKEY = 'reingart.key'
122
+ # obteniendo el TA
123
+ TA = "wsddf-ta.xml"
124
+ if not os.path.exists(TA) or os.path.getmtime(TA) + (60 * 60 * 5) < time.time():
125
+ tra = wsaa.create_tra(service="wDigDepFiel")
126
+ cms = wsaa.sign_tra(tra, CERT, PRIVATEKEY)
127
+ ta_string = wsaa.call_wsaa(cms)
128
+ open(TA, "w").write(ta_string)
129
+ ta_string = open(TA).read()
130
+ ta = SimpleXMLElement(ta_string)
131
+ token = str(ta.credentials.token)
132
+ sign = str(ta.credentials.sign)
133
+ # fin TA
134
+
135
+ # cliente soap del web service
136
+ client = SoapClient(WSDDFURL,
137
+ action=SOAP_ACTION,
138
+ namespace=SOAP_NS, exceptions=True,
139
+ trace=True, ns='ar', soap_ns='soap')
140
+
141
+ if '--dummy' in sys.argv:
142
+ ret = dummy(client)
143
+ print('\n'.join(["%s: %s" % it for it in list(ret.items())]))
144
+
145
+ # ejemplos aviso recep acept (prueba):
146
+
147
+ cuit = 20267565393
148
+ tipo_agente = 'DESP' # 'DESP'
149
+ rol = 'EXTE'
150
+ nro_legajo = '0' * 16 # '1234567890123456'
151
+ cuit_declarante = cuit_psad = cuit_ie = cuit
152
+ codigo = '000' # carpeta completa, '0001' carpeta adicional
153
+ fecha_hora_acept = datetime.datetime.now().isoformat()
154
+ ticket = '1234'
155
+ r = aviso_recep_acept(client, token, sign, cuit, tipo_agente, rol,
156
+ nro_legajo, cuit_declarante, cuit_psad, cuit_ie,
157
+ codigo, fecha_hora_acept, ticket)
158
+ print(r)
159
+
160
+ # ejemplos aviso digit (prueba):
161
+
162
+ cuit = 20267565393
163
+ tipo_agente = 'DESP' # 'DESP'
164
+ rol = 'EXTE'
165
+ nro_legajo = '0' * 16 # '1234567890123456'
166
+ cuit_declarante = cuit_psad = cuit_ie = cuit_ata = cuit
167
+ codigo = '000' # carpeta completa, '0001' carpeta adicional
168
+ ticket = '1234'
169
+ url = 'http://www.example.com'
170
+ hashing = 'db1491eda47d78532cdfca19c62875aade941dc2'
171
+ familias = [{'Familia': {'codigo': '02', 'cantidad': 1}}, {'Familia': {'codigo': '03', 'cantidad': 3}}, ]
172
+ cantidad_total = 4
173
+ r = aviso_digit(client, token, sign, cuit, tipo_agente, rol,
174
+ nro_legajo, cuit_declarante, cuit_psad, cuit_ie, cuit_ata,
175
+ codigo, url, familias, ticket, hashing, cantidad_total)
176
+ print(r)
177
+ print("hecho.")
178
+
179
+ except SoapFault as e:
180
+ print("Falla SOAP:", e.faultcode, e.faultstring.encode("ascii", "ignore"))
181
+ sys.exit(3)
182
+ except Exception as e:
183
+ print(str(e).encode("ascii", "ignore"))
184
+ if DEBUG:
185
+ raise
186
+ sys.exit(5)
app/pyafipws/ws_sr_padron.py ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MΓ³dulo para acceder a los datos de un contribuyente registrado en el PadrΓ³n
14
+ de AFIP (WS-SR-PADRON de AFIP). Consulta a PadrΓ³n Alcance 4 version 1.1
15
+ Consulta de PadrΓ³n Constancia InscripciΓ³n Alcance 5 version 2.0
16
+ """
17
+
18
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
19
+ __copyright__ = "Copyright (C) 2017 Mariano Reingart"
20
+ __license__ = "GPL 3.0"
21
+ __version__ = "1.03e"
22
+
23
+ import csv
24
+ import datetime
25
+ import decimal
26
+ import json
27
+ import os
28
+ import sys
29
+
30
+ from .utils import inicializar_y_capturar_excepciones, BaseWS, get_install_dir, json_serializer, abrir_conf, norm, SoapFault
31
+ from configparser import ConfigParser as SafeConfigParser
32
+ from .padron import TIPO_CLAVE, PROVINCIAS
33
+
34
+
35
+ HOMO = False
36
+ LANZAR_EXCEPCIONES = True
37
+ WSDL = "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA4?wsdl"
38
+ CONFIG_FILE = "rece.ini"
39
+
40
+
41
+ class WSSrPadronA4(BaseWS):
42
+ "Interfaz para el WebService de Consulta PadrΓ³n Contribuyentes Alcance 4"
43
+ _public_methods_ = ['Consultar',
44
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
45
+ 'SetParametros', 'SetTicketAcceso', 'GetParametro',
46
+ 'Dummy', 'Conectar', 'DebugLog', 'SetTicketAcceso']
47
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
48
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
49
+ 'XmlRequest', 'XmlResponse', 'Version', 'InstallDir',
50
+ 'LanzarExcepciones', 'Excepcion', 'Traceback',
51
+ 'Persona', 'data',
52
+ 'denominacion', 'imp_ganancias', 'imp_iva',
53
+ 'monotributo', 'integrante_soc', 'empleador',
54
+ 'actividad_monotributo', 'cat_iva', 'domicilios',
55
+ 'tipo_doc', 'nro_doc',
56
+ 'tipo_persona', 'estado', 'impuestos', 'actividades',
57
+ 'direccion', 'localidad', 'provincia', 'cod_postal',
58
+ ]
59
+
60
+ _reg_progid_ = "WSSrPadronA4"
61
+ _reg_clsid_ = "{C2270008-4324-46F6-A2D3-60836EE63BD7}"
62
+
63
+ # Variables globales para BaseWS:
64
+ HOMO = HOMO
65
+ WSDL = WSDL
66
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
67
+ Reprocesar = True # recuperar automaticamente CAE emitidos
68
+ LanzarExcepciones = LANZAR_EXCEPCIONES
69
+ factura = None
70
+
71
+ def inicializar(self):
72
+ BaseWS.inicializar(self)
73
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
74
+ self.Persona = ''
75
+ self.Reproceso = '' # no implementado
76
+ self.cuit = self.dni = 0
77
+ self.tipo_persona = "" # FISICA o JURIDICA
78
+ self.tipo_doc = self.nro_doc = 0
79
+ self.estado = "" # ACTIVO
80
+ self.denominacion = ""
81
+ self.direccion = self.localidad = self.provincia = self.cod_postal = ""
82
+ self.domicilios = []
83
+ self.impuestos = []
84
+ self.actividades = []
85
+ self.imp_iva = self.empleador = self.integrante_soc = self.cat_iva = ""
86
+ self.monotributo = self.actividad_monotributo = ""
87
+ self.data = {}
88
+ self.errores = []
89
+
90
+ def Dummy(self):
91
+ "Obtener el estado de los servidores de la AFIP"
92
+ ret = self.client.dummy()
93
+ result = ret['dummyReturn']
94
+ self.AppServerStatus = result['appserver']
95
+ self.DbServerStatus = result['dbserver']
96
+ self.AuthServerStatus = result['authserver']
97
+ return True
98
+
99
+ @inicializar_y_capturar_excepciones
100
+ def Consultar(self, id_persona):
101
+ "Devuelve el detalle de todos los datos del contribuyente solicitado"
102
+ # llamar al webservice:
103
+ res = self.client.getPersona(
104
+ sign=self.Sign,
105
+ token=self.Token,
106
+ cuitRepresentada=self.Cuit,
107
+ idPersona=id_persona,
108
+ )
109
+ ret = res.get('personaReturn', {})
110
+ # obtengo el resultado de AFIP (dict):
111
+ data = ret.get('persona', None)
112
+ if isinstance(data, list):
113
+ data = data[0]
114
+ self.data = data
115
+ # lo serializo
116
+ self.Persona = json.dumps(self.data,
117
+ default=json_serializer)
118
+ # extraigo los campos principales:
119
+ self.cuit = data["idPersona"]
120
+ self.tipo_persona = data["tipoPersona"]
121
+ self.tipo_doc = TIPO_CLAVE.get(data["tipoClave"])
122
+ self.nro_doc = data.get("numeroDocumento")
123
+ self.estado = data.get("estadoClave")
124
+ if not "razonSocial" in data:
125
+ self.denominacion = ", ".join([data.get("apellido", ""),
126
+ data.get("nombre", "")])
127
+ else:
128
+ self.denominacion = data.get("razonSocial", "")
129
+ # analizo el domicilio, dando prioridad al FISCAL, luego LEGAL/REAL
130
+ domicilios = data.get("domicilio", [])
131
+ domicilios.sort(key=lambda item: item["tipoDomicilio"] != "FISCAL")
132
+ if domicilios:
133
+ domicilio = domicilios[0]
134
+ self.direccion = domicilio.get("direccion", "")
135
+ self.localidad = domicilio.get("localidad", "") # no usado en CABA
136
+ self.provincia = PROVINCIAS.get(domicilio.get("idProvincia"), "")
137
+ self.cod_postal = domicilio.get("codPostal")
138
+ else:
139
+ self.direccion = self.localidad = self.provincia = ""
140
+ self.cod_postal = ""
141
+ # retrocompatibilidad:
142
+ self.domicilios = domicilios
143
+ self.domicilio = "%s - %s (%s) - %s" % (
144
+ self.direccion, self.localidad,
145
+ self.cod_postal, self.provincia,)
146
+ # analizo impuestos:
147
+ self.impuestos = [imp["idImpuesto"] for imp in data.get("impuesto", [])
148
+ if imp['estado'] == 'ACTIVO']
149
+ self.actividades = [act["idActividad"] for act in data.get("actividad", [])]
150
+ mt = [cat for cat in data.get("categoria", [])
151
+ if cat["idImpuesto"] in (20, 21) and cat["estado"] == "ACTIVO"]
152
+ mt.sort(key=lambda cat: cat["idImpuesto"])
153
+ self.analizar_datos(mt[0] if mt else {})
154
+ return True
155
+
156
+ def analizar_datos(self, cat_mt):
157
+ # intenta determinar situaciΓ³n de IVA:
158
+ if 32 in self.impuestos:
159
+ self.imp_iva = "EX"
160
+ elif 33 in self.impuestos:
161
+ self.imp_iva = "NI"
162
+ elif 34 in self.impuestos:
163
+ self.imp_iva = "NA"
164
+ else:
165
+ self.imp_iva = "S" if 30 in self.impuestos else "N"
166
+ self.monotributo = "S" if cat_mt else "N"
167
+ self.actividad_monotributo = cat_mt.get("descripcionCategoria") if cat_mt else ""
168
+ self.integrante_soc = ""
169
+ self.empleador = "S" if 301 in self.impuestos else "N"
170
+ # intenta determinar categorΓ­a de IVA (confirmar)
171
+ if self.imp_iva in ('AC', 'S'):
172
+ self.cat_iva = 1 # RI
173
+ elif self.imp_iva == 'EX':
174
+ self.cat_iva = 4 # EX
175
+ elif self.monotributo == 'S':
176
+ self.cat_iva = 6 # MT
177
+ else:
178
+ self.cat_iva = 5 # CF
179
+ return True
180
+
181
+
182
+ class WSSrPadronA5(WSSrPadronA4):
183
+ "Interfaz para el WebService de Consulta PadrΓ³n Constancia de InscripciΓ³n Alcance 5"
184
+
185
+ _reg_progid_ = "WSSrPadronA5"
186
+ _reg_clsid_ = "{DF7447DD-EEF3-4E6B-A93B-F969B5075EC8}"
187
+
188
+ WSDL = WSDL.replace("personaServiceA4", "personaServiceA5")
189
+
190
+ @inicializar_y_capturar_excepciones
191
+ def Consultar(self, id_persona):
192
+ "Devuelve el detalle de todos los datos del contribuyente solicitado"
193
+ # llamar al webservice:
194
+ res = self.client.getPersona(
195
+ sign=self.Sign,
196
+ token=self.Token,
197
+ cuitRepresentada=self.Cuit,
198
+ idPersona=id_persona,
199
+ )
200
+ ret = res.get('personaReturn', {})
201
+ # obtengo el resultado de AFIP (dict):
202
+ data = ret.get('datosGenerales', {})
203
+ if isinstance(data, list):
204
+ data = data[0]
205
+ self.data = data
206
+ # lo serializo
207
+ self.Persona = json.dumps(ret,
208
+ default=json_serializer)
209
+ for er in 'errorConstancia', 'errorMonotributo', 'errorRegimenGeneral':
210
+ if er in ret:
211
+ self.errores.extend(ret[er])
212
+ self.Excepcion = '\n\r'.join([er["error"] for er in self.errores])
213
+ # extraigo los campos principales:
214
+ self.tipo_persona = data.get("tipoPersona")
215
+ self.tipo_doc = TIPO_CLAVE.get(data.get("tipoClave"))
216
+ self.nro_doc = data.get("idPersona")
217
+ self.cuit = self.nro_doc
218
+ self.estado = data.get("estadoClave")
219
+ if not "razonSocial" in data:
220
+ self.denominacion = ", ".join([data.get("apellido", ""),
221
+ data.get("nombre", "")])
222
+ else:
223
+ self.denominacion = data.get("razonSocial", "")
224
+ # analizo el domicilio, dando prioridad al FISCAL, luego LEGAL/REAL
225
+ domicilio = data.get("domicilioFiscal", [])
226
+ if domicilio:
227
+ self.direccion = domicilio.get("direccion", "")
228
+ self.localidad = domicilio.get("localidad", "") # no usado en CABA
229
+ self.provincia = PROVINCIAS.get(domicilio.get("idProvincia"), "")
230
+ self.cod_postal = domicilio.get("codPostal")
231
+ else:
232
+ self.direccion = self.localidad = self.provincia = ""
233
+ self.cod_postal = ""
234
+ # retrocompatibilidad:
235
+ self.domicilios = [domicilio]
236
+ self.domicilio = "%s - %s (%s) - %s" % (
237
+ self.direccion, self.localidad,
238
+ self.cod_postal, self.provincia,)
239
+ # extraer datos impositivos (inscripciΓ³n / opciΓ³n) para unificarlos:
240
+ data_mt = ret.get("datosMonotributo", {})
241
+ data_rg = ret.get("datosRegimenGeneral", {})
242
+ # analizo impuestos:
243
+ impuestos = data_mt.get("impuesto", []) + data_rg.get("impuesto", [])
244
+ self.impuestos = [imp["idImpuesto"] for imp in impuestos]
245
+ actividades = data_rg.get("actividad", []) + data_mt.get("actividadMonotributista", [])
246
+ self.actividades = [act["idActividad"] for act in actividades]
247
+ cat_mt = data_mt.get("categoriaMonotributo", {})
248
+ self.analizar_datos(cat_mt)
249
+ return not self.errores
250
+
251
+
252
+ def main():
253
+ "FunciΓ³n principal de pruebas (obtener CAE)"
254
+ import os
255
+ import time
256
+ global CONFIG_FILE
257
+
258
+ DEBUG = '--debug' in sys.argv
259
+
260
+ if '--constancia' in sys.argv:
261
+ padron = WSSrPadronA5()
262
+ SECTION = 'WS-SR-PADRON-A5'
263
+ service = "ws_sr_constancia_inscripcion"
264
+ else:
265
+ padron = WSSrPadronA4()
266
+ SECTION = 'WS-SR-PADRON-A4'
267
+ service = "ws_sr_padron_a4"
268
+
269
+ config = abrir_conf(CONFIG_FILE, DEBUG)
270
+ if config.has_section('WSAA'):
271
+ crt = config.get('WSAA', 'CERT')
272
+ key = config.get('WSAA', 'PRIVATEKEY')
273
+ cuit = config.get(SECTION, 'CUIT')
274
+ else:
275
+ crt, key = "reingart.crt", "reingart.key"
276
+ cuit = "20267565393"
277
+ url_wsaa = url_ws = None
278
+ if config.has_option('WSAA', 'URL'):
279
+ url_wsaa = config.get('WSAA', 'URL')
280
+ if config.has_option(SECTION, 'URL') and not HOMO:
281
+ url_ws = config.get(SECTION, 'URL')
282
+
283
+ # obteniendo el TA para pruebas
284
+ from .wsaa import WSAA
285
+
286
+ cache = ""
287
+ ta = WSAA().Autenticar(service, crt, key, url_wsaa)
288
+
289
+ padron.SetTicketAcceso(ta)
290
+ padron.Cuit = cuit
291
+ padron.Conectar(cache, url_ws, cacert="conf/afip_ca_info.crt")
292
+
293
+ if "--dummy" in sys.argv:
294
+ print(padron.client.help("dummy"))
295
+ wssrpadron4.Dummy()
296
+ print("AppServerStatus", wssrpadron4.AppServerStatus)
297
+ print("DbServerStatus", wssrpadron4.DbServerStatus)
298
+ print("AuthServerStatus", wssrpadron4.AuthServerStatus)
299
+
300
+ if '--csv' in sys.argv:
301
+ csv_reader = csv.reader(open("entrada.csv", "rU"),
302
+ dialect='excel', delimiter=",")
303
+ csv_writer = csv.writer(open("salida.csv", "w"),
304
+ dialect='excel', delimiter=",")
305
+ encabezado = next(csv_reader)
306
+ columnas = ["cuit", "denominacion", "estado", "direccion",
307
+ "localidad", "provincia", "cod_postal",
308
+ "impuestos", "actividades", "imp_iva",
309
+ "monotributo", "actividad_monotributo",
310
+ "empleador", "imp_ganancias", "integrante_soc"]
311
+ csv_writer.writerow(columnas)
312
+
313
+ for fila in csv_reader:
314
+ cuit = (fila[0] if fila else "").replace("-", "")
315
+ if cuit.isdigit():
316
+ print("Consultando AFIP online...", cuit, end=' ')
317
+ try:
318
+ ok = padron.Consultar(cuit)
319
+ except SoapFault as e:
320
+ ok = None
321
+ if e.faultstring != "No existe persona con ese Id":
322
+ raise
323
+ print('ok' if ok else "error", padron.Excepcion)
324
+ # domicilio posiblemente estΓ© en Latin1, normalizar
325
+ csv_writer.writerow([norm(getattr(padron, campo, ""))
326
+ for campo in columnas])
327
+ sys.exit(0)
328
+
329
+ try:
330
+
331
+ if "--prueba" in sys.argv:
332
+ id_persona = "20000000516"
333
+ else:
334
+ id_persona = len(sys.argv) > 1 and sys.argv[1] or "20267565393"
335
+
336
+ if "--testing" in sys.argv:
337
+ padron.LoadTestXML("tests/xml/%s_resp.xml" % service)
338
+ print("Consultando AFIP online via webservice...", end=' ')
339
+ ok = padron.Consultar(id_persona)
340
+
341
+ if DEBUG:
342
+ print("Persona", padron.Persona)
343
+ print(padron.Excepcion)
344
+
345
+ print('ok' if ok else "error", padron.Excepcion)
346
+ print("Denominacion:", padron.denominacion)
347
+ print("Tipo:", padron.tipo_persona, padron.tipo_doc, padron.nro_doc)
348
+ print("Estado:", padron.estado)
349
+ print("Direccion:", padron.direccion)
350
+ print("Localidad:", padron.localidad)
351
+ print("Provincia:", padron.provincia)
352
+ print("Codigo Postal:", padron.cod_postal)
353
+ print("Impuestos:", padron.impuestos)
354
+ print("Actividades:", padron.actividades)
355
+ print("IVA", padron.imp_iva)
356
+ print("MT", padron.monotributo, padron.actividad_monotributo)
357
+ print("Empleador", padron.empleador)
358
+
359
+ if padron.Excepcion:
360
+ print("Excepcion:", padron.Excepcion)
361
+ # ver padron.errores para el detalle
362
+
363
+ except BaseException:
364
+ raise
365
+ print(padron.XmlRequest)
366
+ print(padron.XmlResponse)
367
+
368
+
369
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
370
+ INSTALL_DIR = WSSrPadronA4.InstallDir = WSSrPadronA5.InstallDir = get_install_dir()
371
+
372
+
373
+ if __name__ == '__main__':
374
+
375
+ if "--register" in sys.argv or "--unregister" in sys.argv:
376
+ import win32com.server.register
377
+ win32com.server.register.UseCommandLine(WSSrPadronA4)
378
+ win32com.server.register.UseCommandLine(WSSrPadronA5)
379
+ else:
380
+ main()
app/pyafipws/wsaa.py ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MΓ³dulo para obtener un ticket de autorizaciΓ³n del web service WSAA de AFIP"
14
+
15
+ # Basado en wsaa-client.php de Gerardo Fisanotti - DvSHyS/DiOPIN/AFIP - 13-apr-07
16
+ # Definir WSDL, CERT, PRIVATEKEY, PASSPHRASE, SERVICE, WSAAURL
17
+ # Devuelve TA.xml (ticket de autorizaciΓ³n de WSAA)
18
+
19
+ __author__ = "Mariano Reingart (reingart@gmail.com)"
20
+ __copyright__ = "Copyright (C) 2008-2011 Mariano Reingart"
21
+ __license__ = "GPL 3.0"
22
+ __version__ = "2.11c"
23
+
24
+ import hashlib
25
+ import datetime
26
+ import email
27
+ import os
28
+ import sys
29
+ import time
30
+ import traceback
31
+ import warnings
32
+ import unicodedata
33
+ from pysimplesoap.client import SimpleXMLElement
34
+ from .utils import inicializar_y_capturar_excepciones, BaseWS, get_install_dir, \
35
+ exception_info, safe_console, date
36
+ try:
37
+ from M2Crypto import BIO, Rand, SMIME, SSL
38
+ except ImportError:
39
+ ex = exception_info()
40
+ warnings.warn("No es posible importar M2Crypto (OpenSSL)")
41
+ warnings.warn(ex['msg']) # revisar instalaciΓ³n y DLLs de OpenSSL
42
+ BIO = Rand = SMIME = SSL = None
43
+ # utilizar alternativa (ejecutar proceso por separado)
44
+ from subprocess import Popen, PIPE
45
+ from base64 import b64encode
46
+ from tempfile import NamedTemporaryFile
47
+
48
+ # Constantes (si se usa el script de linea de comandos)
49
+ WSDL = "https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl" # El WSDL correspondiente al WSAA
50
+ CERT = "reingart.crt" # El certificado X.509 obtenido de Seg. Inf.
51
+ PRIVATEKEY = "reingart.key" # La clave privada del certificado CERT
52
+ PASSPHRASE = "xxxxxxx" # La contraseΓ±a para firmar (si hay)
53
+ SERVICE = "wsfe" # El nombre del web service al que se le pide el TA
54
+
55
+ # WSAAURL: la URL para acceder al WSAA, verificar http/https y wsaa/wsaahomo
56
+ # WSAAURL = "https://wsaa.afip.gov.ar/ws/services/LoginCms" # PRODUCCION!!!
57
+ WSAAURL = "https://wsaahomo.afip.gov.ar/ws/services/LoginCms" # homologacion (pruebas)
58
+ SOAP_ACTION = 'http://ar.gov.afip.dif.facturaelectronica/' # Revisar WSDL
59
+ SOAP_NS = "http://wsaa.view.sua.dvadac.desein.afip.gov" # Revisar WSDL
60
+
61
+ # VerificaciΓ³n del web server remoto, necesario para verificar canal seguro
62
+ CACERT = "conf/afip_ca_info.crt" # WSAA CA Cert (Autoridades de Confiaza)
63
+
64
+ HOMO = False
65
+ TYPELIB = False
66
+ DEFAULT_TTL = 60 * 60 * 5 # five hours
67
+ DEBUG = False
68
+
69
+ # No deberΓ­a ser necesario modificar nada despues de esta linea
70
+
71
+
72
+ def create_tra(service=SERVICE, ttl=2400):
73
+ "Crear un Ticket de Requerimiento de Acceso (TRA)"
74
+ tra = SimpleXMLElement(
75
+ '<?xml version="1.0" encoding="UTF-8"?>'
76
+ '<loginTicketRequest version="1.0">'
77
+ '</loginTicketRequest>')
78
+ tra.add_child('header')
79
+ # El source es opcional. Si falta, toma la firma (recomendado).
80
+ # tra.header.addChild('source','subject=...')
81
+ #tra.header.addChild('destination','cn=wsaahomo,o=afip,c=ar,serialNumber=CUIT 33693450239')
82
+ tra.header.add_child('uniqueId', str(date('U')))
83
+ tra.header.add_child('generationTime', str(date('c', date('U') - ttl)))
84
+ tra.header.add_child('expirationTime', str(date('c', date('U') + ttl)))
85
+ tra.add_child('service', service)
86
+ xml = tra.as_xml()
87
+ if isinstance(xml, bytes):
88
+ return xml
89
+ return xml.encode('utf-8')
90
+
91
+
92
+ def sign_tra(tra, cert=CERT, privatekey=PRIVATEKEY, passphrase=""):
93
+ "Firmar PKCS#7 el TRA y devolver CMS (recortando los headers SMIME)"
94
+
95
+ if BIO:
96
+ # Firmar el texto (tra) usando m2crypto (openssl bindings para python)
97
+ buf = BIO.MemoryBuffer(tra) # Crear un buffer desde el texto
98
+ #Rand.load_file('randpool.dat', -1) # Alimentar el PRNG
99
+ s = SMIME.SMIME() # Instanciar un SMIME
100
+ # soporte de contraseΓ±a de encriptaciΓ³n (clave privada, opcional)
101
+ callback = lambda *args, **kwarg: passphrase
102
+ # Cargar clave privada y certificado
103
+ if not privatekey.startswith("-----BEGIN RSA PRIVATE KEY-----"):
104
+ # leer contenido desde archivo (evitar problemas Applink / MSVCRT)
105
+ if os.path.exists(privatekey) and os.path.exists(cert):
106
+ privatekey = open(privatekey).read()
107
+ cert = open(cert).read()
108
+ else:
109
+ raise RuntimeError("Archivos no encontrados: %s, %s" % (privatekey, cert))
110
+ # crear buffers en memoria de la clave privada y certificado:
111
+ key_bio = BIO.MemoryBuffer(privatekey.encode('utf8'))
112
+ crt_bio = BIO.MemoryBuffer(cert.encode('utf8'))
113
+ s.load_key_bio(key_bio, crt_bio, callback) # (desde buffer)
114
+ p7 = s.sign(buf, 0) # Firmar el buffer
115
+ out = BIO.MemoryBuffer() # Crear un buffer para la salida
116
+ s.write(out, p7) # Generar p7 en formato mail
117
+ # Rand.save_file('randpool.dat') # Guardar el estado del PRNG's
118
+
119
+ # extraer el cuerpo del mensaje (parte firmada)
120
+ msg = email.message_from_string(out.read().decode('utf8'))
121
+ for part in msg.walk():
122
+ filename = part.get_filename()
123
+ if filename == "smime.p7m": # es la parte firmada?
124
+ return part.get_payload(decode=False) # devolver CMS
125
+ else:
126
+ # Firmar el texto (tra) usando OPENSSL directamente
127
+ try:
128
+ if sys.platform.startswith("linux"):
129
+ openssl = "openssl"
130
+ else:
131
+ if sys.maxsize <= 2**32:
132
+ openssl = r"c:\OpenSSL-Win32\bin\openssl.exe"
133
+ else:
134
+ openssl = r"c:\OpenSSL-Win64\bin\openssl.exe"
135
+ # NOTE: workaround if certificate is not already stored in a file
136
+ # SECURITY WARNING: the private key will be exposed a bit in /tmp
137
+ # (in theory only for the current user)
138
+ if cert.startswith("-----BEGIN CERTIFICATE-----"):
139
+ cert_f = NamedTemporaryFile()
140
+ cert_f.write(cert.encode('utf-8'))
141
+ cert_f.flush()
142
+ cert = cert_f.name
143
+ else:
144
+ cert_f = None
145
+ if privatekey.startswith("-----BEGIN RSA PRIVATE KEY-----"):
146
+ key_f = NamedTemporaryFile()
147
+ key_f.write(privatekey.encode('utf-8'))
148
+ key_f.flush()
149
+ privatekey = key_f.name
150
+ else:
151
+ key_f = None
152
+ try:
153
+ out = Popen([openssl, "smime", "-sign",
154
+ "-signer", cert, "-inkey", privatekey,
155
+ "-outform","DER", "-nodetach"],
156
+ stdin=PIPE, stdout=PIPE,
157
+ stderr=PIPE).communicate(tra)[0]
158
+ finally:
159
+ # close temp files to delete them (just in case):
160
+ if cert_f:
161
+ cert_f.close()
162
+ if key_f:
163
+ key_f.close()
164
+ return b64encode(out).decode("utf8")
165
+ except OSError as e:
166
+ if e.errno == 2:
167
+ warnings.warn("El ejecutable de OpenSSL no esta disponible en el PATH")
168
+ raise
169
+
170
+
171
+ def call_wsaa(cms, location=WSAAURL, proxy=None, trace=False, cacert=None):
172
+ "Llamar web service con CMS para obtener ticket de autorizaciΓ³n (TA)"
173
+
174
+ # creo la nueva clase
175
+ wsaa = WSAA()
176
+ try:
177
+ wsaa.Conectar(proxy=proxy, wsdl=location, cache="", cacert=cacert)
178
+ ta = wsaa.LoginCMS(cms)
179
+ if not ta:
180
+ raise RuntimeError(wsaa.Excepcion)
181
+ else:
182
+ return ta
183
+ except BaseException:
184
+ raise
185
+
186
+
187
+ class WSAA(BaseWS):
188
+ "Interfaz para el WebService de AutenticaciΓ³n y AutorizaciΓ³n"
189
+ _public_methods_ = ['CreateTRA', 'SignTRA', 'CallWSAA', 'LoginCMS', 'Conectar',
190
+ 'AnalizarXml', 'ObtenerTagXml', 'Expirado', 'Autenticar',
191
+ 'DebugLog', 'AnalizarCertificado',
192
+ 'CrearClavePrivada', 'CrearPedidoCertificado',
193
+ ]
194
+ _public_attrs_ = ['Token', 'Sign', 'ExpirationTime', 'Version',
195
+ 'XmlRequest', 'XmlResponse',
196
+ 'InstallDir', 'Traceback', 'Excepcion',
197
+ 'Identidad', 'Caducidad', 'Emisor', 'CertX509',
198
+ 'SoapFault', 'LanzarExcepciones',
199
+ ]
200
+ _readonly_attrs_ = _public_attrs_[:-1]
201
+ _reg_progid_ = "WSAA"
202
+ _reg_clsid_ = "{6268820C-8900-4AE9-8A2D-F0A1EBD4CAC5}"
203
+
204
+ if TYPELIB:
205
+ _typelib_guid_ = '{30E9C94B-7385-4534-9A80-DF50FD169253}'
206
+ _typelib_version_ = 2, 11
207
+ _com_interfaces_ = ['IWSAA']
208
+
209
+ # Variables globales para BaseWS:
210
+ HOMO = HOMO
211
+ WSDL = WSDL
212
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
213
+
214
+ @inicializar_y_capturar_excepciones
215
+ def CreateTRA(self, service="wsfe", ttl=2400):
216
+ "Crear un Ticket de Requerimiento de Acceso (TRA)"
217
+ return create_tra(service, ttl)
218
+
219
+ @inicializar_y_capturar_excepciones
220
+ def AnalizarCertificado(self, crt, binary=False):
221
+ "Carga un certificado digital y extrae los campos mΓ‘s importantes"
222
+ from M2Crypto import BIO, EVP, RSA, X509
223
+ if binary:
224
+ bio = BIO.MemoryBuffer(cert.encode('utf8'))
225
+ x509 = X511.load_cert_bio(bio, X509.FORMAT_DER)
226
+ else:
227
+ if not crt.startswith("-----BEGIN CERTIFICATE-----"):
228
+ crt = open(crt).read()
229
+ if isinstance(crt, str):
230
+ crt = crt.encode('utf-8')
231
+ bio = BIO.MemoryBuffer(crt.encode('utf8'))
232
+ x509 = X509.load_cert_bio(bio, X509.FORMAT_PEM)
233
+ if x509:
234
+ self.Identidad = x509.get_subject().as_text()
235
+ self.Caducidad = x509.get_not_after().get_datetime()
236
+ self.Emisor = x509.get_issuer().as_text()
237
+ self.CertX509 = x509.as_text()
238
+ return True
239
+
240
+ @inicializar_y_capturar_excepciones
241
+ def CrearClavePrivada(self, filename="privada.key", key_length=4096,
242
+ pub_exponent=0x10001, passphrase=""):
243
+ "Crea una clave privada (private key)"
244
+ from M2Crypto import RSA, EVP
245
+
246
+ # only protect if passphrase was given (it will fail otherwise)
247
+ callback = lambda *args, **kwarg: passphrase
248
+ chiper = None if not passphrase else "aes_128_cbc"
249
+ # create the RSA key pair (and save the result to a file):
250
+ rsa_key_pair = RSA.gen_key(key_length, pub_exponent, callback)
251
+ bio = BIO.MemoryBuffer()
252
+ rsa_key_pair.save_key_bio(bio, chiper, callback)
253
+ f = open(filename, "w")
254
+ f.write(bio.read())
255
+ f.close()
256
+ # create a public key to sign the certificate request:
257
+ self.pkey = EVP.PKey(md='sha256')
258
+ self.pkey.assign_rsa(rsa_key_pair)
259
+ return True
260
+
261
+ @inicializar_y_capturar_excepciones
262
+ def CrearPedidoCertificado(self, cuit="", empresa="", nombre="pyafipws",
263
+ filename="empresa.csr"):
264
+ "Crear un certificate signing request (X509 CSR)"
265
+ from M2Crypto import RSA, EVP, X509
266
+
267
+ # create the certificate signing request (CSR):
268
+ self.x509_req = X509.Request()
269
+
270
+ # normalizar encoding (reemplazar acentos, eΓ±e, etc.)
271
+ if isinstance(empresa, str):
272
+ empresa = unicodedata.normalize('NFKD', empresa).encode('ASCII', 'ignore')
273
+ if isinstance(nombre, str):
274
+ nombre = unicodedata.normalize('NFKD', nombre).encode('ASCII', 'ignore')
275
+
276
+ # subjet: C=AR/O=[empresa]/CN=[nombre]/serialNumber=CUIT [nro_cuit]
277
+ x509name = X509.X509_Name()
278
+ # default OpenSSL parameters:
279
+ kwargs = {"type": 0x1000 | 1, "len": -1, "loc": -1, "set": 0}
280
+ x509name.add_entry_by_txt(field='C', entry='AR', **kwargs)
281
+ x509name.add_entry_by_txt(field='O', entry=empresa, **kwargs)
282
+ x509name.add_entry_by_txt(field='CN', entry=nombre, **kwargs)
283
+ x509name.add_entry_by_txt(field='serialNumber', entry="CUIT %s" % str(cuit), **kwargs)
284
+ self.x509_req.set_subject_name(x509name)
285
+
286
+ # sign the request with the previously created key (CrearClavePrivada)
287
+ self.x509_req.set_pubkey(pkey=self.pkey)
288
+ self.x509_req.sign(pkey=self.pkey, md='sha256')
289
+ # save the CSR result to a file:
290
+ f = open(filename, "w")
291
+ f.write(self.x509_req.as_pem())
292
+ f.close()
293
+ return True
294
+
295
+ @inicializar_y_capturar_excepciones
296
+ def SignTRA(self, tra, cert, privatekey, passphrase=""):
297
+ "Firmar el TRA y devolver CMS"
298
+ return sign_tra(tra, cert, privatekey, passphrase)
299
+
300
+ @inicializar_y_capturar_excepciones
301
+ def LoginCMS(self, cms):
302
+ "Obtener ticket de autorizaciΓ³n (TA)"
303
+ results = self.client.loginCms(in0=str(cms))
304
+ ta_xml = results['loginCmsReturn'] # .encode("utf-8")
305
+ self.xml = ta = SimpleXMLElement(ta_xml)
306
+ self.Token = str(ta.credentials.token)
307
+ self.Sign = str(ta.credentials.sign)
308
+ self.ExpirationTime = str(ta.header.expirationTime)
309
+ return ta_xml
310
+
311
+ def CallWSAA(self, cms, url="", proxy=None):
312
+ "Obtener ticket de autorizaciΓ³n (TA) -version retrocompatible-"
313
+ self.Conectar("", url, proxy)
314
+ ta_xml = self.LoginCMS(cms)
315
+ if not ta_xml:
316
+ raise RuntimeError(self.Excepcion)
317
+ return ta_xml
318
+
319
+ @inicializar_y_capturar_excepciones
320
+ def Expirado(self, fecha=None):
321
+ "Comprueba la fecha de expiraciΓ³n, devuelve si ha expirado"
322
+ if not fecha:
323
+ fecha = self.ObtenerTagXml('expirationTime')
324
+ now = datetime.datetime.now()
325
+ d = datetime.datetime.strptime(fecha[:19], '%Y-%m-%dT%H:%M:%S')
326
+ return now > d
327
+
328
+ def Autenticar(self, service, crt, key, wsdl=None, proxy=None, wrapper=None, cacert=None, cache=None, debug=False):
329
+ "MΓ©todo unificado para obtener el ticket de acceso (cacheado)"
330
+
331
+ self.LanzarExcepciones = True
332
+ try:
333
+ # sanity check: verificar las credenciales
334
+ for filename in (crt, key):
335
+ if not os.access(filename, os.R_OK):
336
+ raise RuntimeError("Imposible abrir %s\n" % filename)
337
+ # creo el nombre para el archivo del TA (segΓΊn credenciales y ws)
338
+ ta_src = (service + crt + key).encode("utf8")
339
+ fn = "TA-%s.xml" % hashlib.md5(ta_src).hexdigest()
340
+ if cache:
341
+ fn = os.path.join(cache, fn)
342
+ else:
343
+ fn = os.path.join(self.InstallDir, "cache", fn)
344
+
345
+ # leer el ticket de acceso (si fue previamente solicitado)
346
+ if not os.path.exists(fn) or os.path.getsize(fn) == 0 or \
347
+ os.path.getmtime(fn) + (DEFAULT_TTL) < time.time():
348
+ # ticket de acceso (TA) vencido, crear un nuevo req. (TRA)
349
+ if DEBUG:
350
+ print("Creando TRA...")
351
+ tra = self.CreateTRA(service=service, ttl=DEFAULT_TTL)
352
+ # firmarlo criptogrΓ‘ficamente
353
+ if DEBUG:
354
+ print("Frimando TRA...")
355
+ cms = self.SignTRA(tra, crt, key)
356
+ # concectar con el servicio web:
357
+ if DEBUG:
358
+ print("Conectando a WSAA...")
359
+ ok = self.Conectar(cache, wsdl, proxy, wrapper, cacert)
360
+ if not ok or self.Excepcion:
361
+ raise RuntimeError("Fallo la conexiΓ³n: %s" % self.Excepcion)
362
+ # llamar al mΓ©todo remoto para solicitar el TA
363
+ if DEBUG:
364
+ print("Llamando WSAA...")
365
+ ta = self.LoginCMS(cms)
366
+ if not ta:
367
+ raise RuntimeError("Ticket de acceso vacio: %s" % WSAA.Excepcion)
368
+ # grabar el ticket de acceso para poder reutilizarlo luego
369
+ if DEBUG:
370
+ print("Grabando TA en %s..." % fn)
371
+ try:
372
+ open(fn, "w").write(ta)
373
+ except IOError as e:
374
+ self.Excepcion = "Imposible grabar ticket de accesso: %s" % fn
375
+ else:
376
+ # leer el ticket de acceso del archivo en cache
377
+ if DEBUG:
378
+ print("Leyendo TA de %s..." % fn)
379
+ ta = open(fn, "r").read()
380
+ # analizar el ticket de acceso y extraer los datos relevantes
381
+ self.AnalizarXml(xml=ta)
382
+ self.Token = self.ObtenerTagXml("token")
383
+ self.Sign = self.ObtenerTagXml("sign")
384
+ except BaseException:
385
+ ta = ""
386
+ if not self.Excepcion:
387
+ # avoid encoding problem when reporting exceptions to the user:
388
+ self.Excepcion = traceback.format_exception_only(sys.exc_info()[0],
389
+ sys.exc_info()[1])[0]
390
+ self.Traceback = ""
391
+ if DEBUG or debug:
392
+ raise
393
+ return ta
394
+
395
+
396
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
397
+ INSTALL_DIR = WSAA.InstallDir = get_install_dir()
398
+
399
+
400
+ if __name__ == "__main__":
401
+
402
+ safe_console()
403
+
404
+ if '--register' in sys.argv or '--unregister' in sys.argv:
405
+ import pythoncom
406
+ if TYPELIB:
407
+ if '--register' in sys.argv:
408
+ tlb = os.path.abspath(os.path.join(INSTALL_DIR, "typelib", "wsaa.tlb"))
409
+ print("Registering %s" % (tlb,))
410
+ tli = pythoncom.LoadTypeLib(tlb)
411
+ pythoncom.RegisterTypeLib(tli, tlb)
412
+ elif '--unregister' in sys.argv:
413
+ k = WSAA
414
+ pythoncom.UnRegisterTypeLib(k._typelib_guid_,
415
+ k._typelib_version_[0],
416
+ k._typelib_version_[1],
417
+ 0,
418
+ pythoncom.SYS_WIN32)
419
+ print("Unregistered typelib")
420
+ import win32com.server.register
421
+ win32com.server.register.UseCommandLine(WSAA)
422
+ elif "/Automate" in sys.argv:
423
+ # MS seems to like /automate to run the class factories.
424
+ import win32com.server.localserver
425
+ # win32com.server.localserver.main()
426
+ # start the server.
427
+ win32com.server.localserver.serve([WSAA._reg_clsid_])
428
+ elif "--crear_pedido_cert" in sys.argv:
429
+ # instanciar el helper y revisar los parΓ‘metros
430
+ wsaa = WSAA()
431
+ args = [arg for arg in sys.argv if not arg.startswith("--")]
432
+ # obtengo el CUIT y lo normalizo:
433
+ cuit = len(args) > 1 and args[1] or input("Ingrese un CUIT: ")
434
+ cuit = ''.join([c for c in cuit if c.isdigit()])
435
+ nombre = len(args) > 2 and args[2] or "PyAfipWs"
436
+ # consultar el padrΓ³n online de AFIP si no se especificΓ³ razΓ³n social:
437
+ empresa = len(args) > 3 and args[3] or ""
438
+ if not empresa:
439
+ from .padron import PadronAFIP
440
+ padron = PadronAFIP()
441
+ ok = padron.Consultar(cuit)
442
+ if ok and padron.denominacion:
443
+ print("DenominaciΓ³n segΓΊn AFIP:", padron.denominacion)
444
+ empresa = padron.denominacion
445
+ else:
446
+ print("CUIT %s no encontrado: %s..." % (cuit, padron.Excepcion))
447
+ empresa = input("Empresa: ")
448
+ # longitud de la clave (2048 predeterminada a partir de 8/2016)
449
+ key_length = len(args) > 4 and args[4] or ""
450
+ try:
451
+ key_length = int(key_length)
452
+ except ValueError:
453
+ key_length = 2048
454
+ # generar los archivos (con fecha para no pisarlo)
455
+ ts = datetime.datetime.now().strftime("%Y%m%d%M%S")
456
+ clave_privada = "clave_privada_%s_%s.key" % (cuit, ts)
457
+ pedido_cert = "pedido_cert_%s_%s.csr" % (cuit, ts)
458
+ print("Longitud clave %s (bits)" % key_length)
459
+ wsaa.CrearClavePrivada(clave_privada, key_length)
460
+ wsaa.CrearPedidoCertificado(cuit, empresa, nombre, pedido_cert)
461
+ print("Se crearon los archivos:")
462
+ print(clave_privada)
463
+ print(pedido_cert)
464
+ # convertir a terminaciΓ³n de linea windows y abrir con bloc de notas
465
+ if sys.platform == "win32":
466
+ txt = open(pedido_cert + ".txt", "wb")
467
+ for linea in open(pedido_cert, "r"):
468
+ txt.write("%s\r\n" % linea)
469
+ txt.close()
470
+ os.startfile(pedido_cert + ".txt")
471
+ else:
472
+
473
+ # Leer argumentos desde la linea de comando (si no viene tomar default)
474
+ args = [arg for arg in sys.argv if arg.startswith("--")]
475
+ argv = [arg for arg in sys.argv if not arg.startswith("--")]
476
+ crt = len(argv) > 1 and argv[1] or CERT
477
+ key = len(argv) > 2 and argv[2] or PRIVATEKEY
478
+ service = len(argv) > 3 and argv[3] or "wsfe"
479
+ ttl = len(argv) > 4 and int(argv[4]) or 36000
480
+ url = len(argv) > 5 and argv[5] or WSAAURL
481
+ wrapper = len(argv) > 6 and argv[6] or None
482
+ cacert = len(argv) > 7 and argv[7] or CACERT
483
+ DEBUG = "--debug" in args or DEBUG
484
+
485
+ print("Usando CRT=%s KEY=%s URL=%s SERVICE=%s TTL=%s" % (crt, key, url, service, ttl), file=sys.stderr)
486
+
487
+ # creo el objeto para comunicarme con el ws
488
+ wsaa = WSAA()
489
+ wsaa.LanzarExcepciones = True
490
+
491
+ print("WSAA Version %s %s" % (WSAA.Version, HOMO), file=sys.stderr)
492
+
493
+ if '--proxy' in args:
494
+ proxy = sys.argv[sys.argv.index("--proxy") + 1]
495
+ print("Usando PROXY:", proxy, file=sys.stderr)
496
+ else:
497
+ proxy = None
498
+
499
+ if '--analizar' in sys.argv:
500
+ wsaa.AnalizarCertificado(crt)
501
+ print(wsaa.Identidad)
502
+ print(wsaa.Caducidad)
503
+ print(wsaa.Emisor)
504
+ print(wsaa.CertX509)
505
+
506
+ ta = wsaa.Autenticar(service, crt, key, url, proxy, wrapper, cacert)
507
+ if not ta:
508
+ if DEBUG:
509
+ print(wsaa.Traceback, file=sys.stderr)
510
+ sys.exit("Excepcion: %s" % wsaa.Excepcion)
511
+
512
+ else:
513
+ print(ta)
514
+
515
+ if wsaa.Excepcion:
516
+ print(wsaa.Excepcion, file=sys.stderr)
517
+
518
+ if DEBUG:
519
+ print("Source:", wsaa.ObtenerTagXml('source'))
520
+ print("UniqueID Time:", wsaa.ObtenerTagXml('uniqueId'))
521
+ print("Generation Time:", wsaa.ObtenerTagXml('generationTime'))
522
+ print("Expiration Time:", wsaa.ObtenerTagXml('expirationTime'))
523
+ print("Expiro?", wsaa.Expirado())
524
+ ##import time; time.sleep(10)
525
+ # print "Expiro?", wsaa.Expirado()
app/pyafipws/wsbfev1.py ADDED
@@ -0,0 +1,659 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para obtener cοΏ½digo de autorizaciοΏ½n electrοΏ½nico (CAE) del web service
14
+ WSBFEv1 de AFIP (Bonos Fiscales electronicos v1.1 - Factura ElectrοΏ½nica RG)
15
+ a fin de gestionar los Bonos en la SecretarοΏ½a de Industria segοΏ½n RG 2557
16
+ """
17
+
18
+ __author__ = "Mariano Reingart (reingart@gmail.com)"
19
+ __copyright__ = "Copyright (C) 2013-2016 Mariano Reingart"
20
+ __license__ = "GPL 3.0"
21
+ __version__ = "1.06g"
22
+
23
+ import datetime
24
+ import decimal
25
+ import os
26
+ import sys
27
+ from .utils import inicializar_y_capturar_excepciones, BaseWS, get_install_dir
28
+
29
+ HOMO = False
30
+ LANZAR_EXCEPCIONES = True # valor por defecto: True
31
+ WSDL = "https://wswhomo.afip.gov.ar/wsbfev1/service.asmx?WSDL"
32
+
33
+
34
+ class WSBFEv1(BaseWS):
35
+ "Interfaz para el WebService de Bono Fiscal ElectrοΏ½nico V1 (FE Bs. Capital)"
36
+ _public_methods_ = ['CrearFactura', 'AgregarItem', 'Authorize', 'GetCMP', 'AgregarOpcional', 'AgregarCmpAsoc',
37
+ 'GetParamMon', 'GetParamTipoCbte', 'GetParamUMed',
38
+ 'GetParamTipoIVA', 'GetParamNCM', 'GetParamZonas',
39
+ 'GetParamTipoDoc',
40
+ 'Dummy', 'Conectar', 'GetLastCMP', 'GetLastID',
41
+ 'GetParamCtz', 'LoadTestXML',
42
+ 'AnalizarXml', 'ObtenerTagXml', 'DebugLog',
43
+ 'SetParametros', 'SetTicketAcceso', 'GetParametro',
44
+ 'Dummy', 'Conectar', 'SetTicketAcceso']
45
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
46
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
47
+ 'XmlRequest', 'XmlResponse', 'Version',
48
+ 'Resultado', 'Obs', 'Reproceso', 'FechaCAE',
49
+ 'CAE', 'Vencimiento', 'Eventos', 'ErrCode', 'ErrMsg', 'FchVencCAE',
50
+ 'Excepcion', 'LanzarExcepciones', 'Traceback', "InstallDir",
51
+ 'PuntoVenta', 'CbteNro', 'FechaCbte', 'ImpTotal', 'ImpNeto', 'ImptoLiq',
52
+ ]
53
+
54
+ _reg_progid_ = "WSBFEv1"
55
+ _reg_clsid_ = "{EE4ABEE2-76DD-450F-880B-66710AE464D6}"
56
+
57
+ # Variables globales para BaseWS:
58
+ HOMO = HOMO
59
+ WSDL = WSDL
60
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
61
+ factura = None
62
+
63
+ def inicializar(self):
64
+ BaseWS.inicializar(self)
65
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
66
+ self.Resultado = self.Motivo = self.Reproceso = ''
67
+ self.LastID = self.LastCMP = self.CAE = self.Vencimiento = ''
68
+ self.CbteNro = self.FechaCbte = self.PuntoVenta = self.ImpTotal = None
69
+ self.ImpNeto = self.ImptoLiq = None
70
+ self.LanzarExcepciones = LANZAR_EXCEPCIONES
71
+ self.InstallDir = INSTALL_DIR
72
+ self.FechaCAE = self.FchVencCAE = "" # retrocompatibilidad
73
+
74
+ def __analizar_errores(self, ret):
75
+ "Comprueba y extrae errores si existen en la respuesta XML"
76
+ if 'BFEErr' in ret:
77
+ errores = [ret['BFEErr']]
78
+ for error in errores:
79
+ self.Errores.append("%s: %s" % (
80
+ error['ErrCode'],
81
+ error.get('ErrMsg', ""),
82
+ ))
83
+ self.ErrCode = ' '.join([str(error['ErrCode']) for error in errores])
84
+ self.ErrMsg = '\n'.join(self.Errores)
85
+ if 'BFEEvents' in ret:
86
+ events = [ret['BFEEvents']]
87
+ self.Eventos = ['%s: %s' % (evt['EventCode'], evt.get('EventMsg', "")) for evt in events]
88
+
89
+ def CrearFactura(self, tipo_doc=80, nro_doc=23111111113,
90
+ zona=0, tipo_cbte=1, punto_vta=1, cbte_nro=0, fecha_cbte=None,
91
+ imp_total=0.0, imp_neto=0.0, impto_liq=0.0,
92
+ imp_tot_conc=0.0, impto_liq_rni=0.00, imp_op_ex=0.00,
93
+ imp_perc=0.00, imp_iibb=0.00, imp_perc_mun=0.00, imp_internos=0.00,
94
+ imp_moneda_id=0, imp_moneda_ctz=1.0, fecha_venc_pago=None, **kwargs):
95
+ "Creo un objeto factura (interna)"
96
+ # Creo una factura para bonos fiscales electrοΏ½nicos
97
+
98
+ fact = {'tipo_cbte': tipo_cbte, 'punto_vta': punto_vta,
99
+ 'cbte_nro': cbte_nro, 'fecha_cbte': fecha_cbte, 'zona': zona,
100
+ 'tipo_doc': tipo_doc, 'nro_doc': nro_doc,
101
+ 'imp_total': imp_total, 'imp_neto': imp_neto,
102
+ 'impto_liq': impto_liq, 'impto_liq_rni': impto_liq_rni,
103
+ 'imp_op_ex': imp_op_ex, 'imp_tot_conc': imp_tot_conc,
104
+ 'imp_perc': imp_perc, 'imp_perc_mun': imp_perc_mun,
105
+ 'imp_iibb': imp_iibb, 'imp_internos': imp_internos,
106
+ 'imp_moneda_id': imp_moneda_id, 'imp_moneda_ctz': imp_moneda_ctz,
107
+ 'fecha_venc_pago': fecha_venc_pago,
108
+ 'cbtes_asoc': [],
109
+ 'opcionales': [],
110
+ 'iva': [],
111
+ 'detalles': [],
112
+ }
113
+ self.factura = fact
114
+ return True
115
+
116
+ def AgregarItem(self, ncm, sec, ds, qty, umed, precio, bonif, iva_id, imp_total, **kwargs):
117
+ "Agrego un item a una factura (interna)"
118
+ # ds = unicode(ds, "latin1") # convierto a latin1
119
+ # Nota: no se calcula neto, iva, etc (deben venir calculados!)
120
+ self.factura['detalles'].append({
121
+ 'ncm': ncm, 'sec': sec,
122
+ 'ds': ds,
123
+ 'qty': qty,
124
+ 'umed': umed,
125
+ 'precio': precio,
126
+ 'bonif': bonif,
127
+ 'iva_id': iva_id,
128
+ 'imp_total': imp_total,
129
+ })
130
+ return True
131
+
132
+ def AgregarCmpAsoc(self, tipo=1, pto_vta=0, nro=0, cuit=None, fecha=None, **kwarg):
133
+ "Agrego un comprobante asociado a una factura (interna)"
134
+ cmp_asoc = {'tipo': tipo, 'pto_vta': pto_vta, 'nro': nro}
135
+ if cuit is not None:
136
+ cmp_asoc['cuit'] = cuit
137
+ if fecha is not None:
138
+ cmp_asoc['fecha'] = fecha
139
+ self.factura['cbtes_asoc'].append(cmp_asoc)
140
+ return True
141
+
142
+ def AgregarOpcional(self, opcional_id=0, valor="", **kwarg):
143
+ "Agrego un dato opcional a una factura (interna)"
144
+ op = { 'opcional_id': opcional_id, 'valor': valor }
145
+ self.factura['opcionales'].append(op)
146
+ return True
147
+
148
+ @inicializar_y_capturar_excepciones
149
+ def Authorize(self, id):
150
+ "Autoriza la factura cargada en memoria"
151
+ f = self.factura
152
+ ret = self.client.BFEAuthorize(
153
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
154
+ Cmp={
155
+ 'Id': id,
156
+ 'Zona': f['zona'],
157
+ 'Fecha_cbte': f['fecha_cbte'],
158
+ 'Tipo_cbte': f['tipo_cbte'],
159
+ 'Punto_vta': f['punto_vta'],
160
+ 'Cbte_nro': f['cbte_nro'],
161
+ 'Tipo_doc': f['tipo_doc'], 'Nro_doc': f['nro_doc'],
162
+ 'Imp_moneda_Id': f['imp_moneda_id'],
163
+ 'Imp_moneda_ctz': f['imp_moneda_ctz'],
164
+ 'Imp_total': f['imp_total'],
165
+ 'Imp_tot_conc': f['imp_tot_conc'], 'Imp_op_ex': f['imp_op_ex'],
166
+ 'Imp_neto': f['imp_neto'], 'Impto_liq': f['impto_liq'],
167
+ 'Impto_liq_rni': f['impto_liq_rni'],
168
+ 'Imp_perc': f['imp_perc'], 'Imp_perc_mun': f['imp_perc_mun'],
169
+ 'Imp_iibb': f['imp_iibb'],
170
+ 'Imp_internos': f['imp_internos'],
171
+ 'Fecha_vto_pago': f['fecha_venc_pago'],
172
+ 'Items': [
173
+ {'Item': {
174
+ 'Pro_codigo_ncm': d['ncm'],
175
+ 'Pro_codigo_sec': d['sec'],
176
+ 'Pro_ds': d['ds'],
177
+ 'Pro_qty': d['qty'],
178
+ 'Pro_umed': d['umed'],
179
+ 'Pro_precio_uni': d['precio'],
180
+ 'Imp_bonif': d['bonif'],
181
+ 'Imp_total': d['imp_total'],
182
+ 'Iva_id': d['iva_id'],
183
+ }} for d in f['detalles']],
184
+ 'CbtesAsoc': f['cbtes_asoc'] and [
185
+ {'CbteAsoc': {
186
+ 'Tipo_cbte': cbte_asoc['tipo'],
187
+ 'Punto_vta': cbte_asoc['pto_vta'],
188
+ 'Cbte_nro': cbte_asoc['nro'],
189
+ 'Cuit': cbte_asoc.get('cuit'),
190
+ 'Fecha_cbte': cbte_asoc.get('fecha'),
191
+ }}
192
+ for cbte_asoc in f['cbtes_asoc']] or None,
193
+ 'Opcionales': [
194
+ {'Opcional': {
195
+ 'Id': opcional['opcional_id'],
196
+ 'Valor': opcional['valor'],
197
+ }} for opcional in f['opcionales']] or None,
198
+ })
199
+
200
+ result = ret['BFEAuthorizeResult']
201
+ self.__analizar_errores(result)
202
+ if 'BFEResultAuth' in result:
203
+ auth = result['BFEResultAuth']
204
+ # Resultado: A: Aceptado, R: Rechazado
205
+ self.Resultado = auth.get('Resultado', "")
206
+ # Obs:
207
+ self.Obs = auth.get('Obs', "")
208
+ self.Reproceso = auth.get('Reproceso', "")
209
+ self.CAE = auth.get('Cae', "")
210
+ self.CbteNro = auth.get('Fch_cbte', "")
211
+ self.ImpTotal = str(auth.get('Imp_total', ''))
212
+ self.ImptoLiq = str(auth.get('Impto_liq', ''))
213
+ self.ImpNeto = str(auth.get('Imp_neto', ''))
214
+ vto = str(auth.get('Fch_venc_Cae', ''))
215
+ self.FchVencCAE = vto
216
+ self.Vencimiento = "%s/%s/%s" % (vto[6:8], vto[4:6], vto[0:4])
217
+ return self.CAE
218
+
219
+ @inicializar_y_capturar_excepciones
220
+ def Dummy(self):
221
+ "Obtener el estado de los servidores de la AFIP"
222
+ result = self.client.BFEDummy()['BFEDummyResult']
223
+ self.__analizar_errores(result)
224
+ self.AppServerStatus = str(result.get('AppServer', ""))
225
+ self.DbServerStatus = str(result.get('DbServer', ""))
226
+ self.AuthServerStatus = str(result.get('AuthServer', ""))
227
+ return True
228
+
229
+ @inicializar_y_capturar_excepciones
230
+ def GetCMP(self, tipo_cbte, punto_vta, cbte_nro):
231
+ "Recuperar los datos completos de un comprobante ya autorizado"
232
+ ret = self.client.BFEGetCMP(
233
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
234
+ Cmp={"Tipo_cbte": tipo_cbte,
235
+ "Punto_vta": punto_vta, "Cbte_nro": cbte_nro})
236
+ result = ret['BFEGetCMPResult']
237
+ self.__analizar_errores(result)
238
+ if 'BFEResultGet' in result:
239
+ resultget = result['BFEResultGet']
240
+ # Obs, cae y fecha cae
241
+ if 'Cae' in resultget:
242
+ self.Obs = resultget['Obs'] and resultget['Obs'].strip(" ") or ''
243
+ self.CAE = resultget['Cae']
244
+ vto = str(resultget['Fch_venc_Cae'])
245
+ self.Vencimiento = "%s/%s/%s" % (vto[6:8], vto[4:6], vto[0:4])
246
+ self.FechaCbte = resultget['Fecha_cbte_orig'] # .strftime("%Y/%m/%d")
247
+ self.FechaCAE = resultget['Fecha_cbte_cae'] # .strftime("%Y/%m/%d")
248
+ self.PuntoVenta = resultget['Punto_vta'] # 4000
249
+ self.Resultado = resultget['Resultado']
250
+ self.CbteNro = resultget['Cbte_nro']
251
+ self.ImpTotal = resultget['Imp_total']
252
+ self.ImptoLiq = resultget['Impto_liq']
253
+ self.ImpNeto = resultget['Imp_neto']
254
+ return self.CAE
255
+ else:
256
+ return 0
257
+
258
+ @inicializar_y_capturar_excepciones
259
+ def GetLastCMP(self, tipo_cbte, punto_vta):
260
+ "Recuperar οΏ½ltimo nοΏ½mero de comprobante emitido"
261
+ ret = self.client.BFEGetLast_CMP(
262
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit,
263
+ "Tipo_cbte": tipo_cbte,
264
+ "Pto_venta": punto_vta,
265
+ })
266
+ result = ret['BFEGetLast_CMPResult']
267
+ self.__analizar_errores(result)
268
+ if 'BFEResult_LastCMP' in result:
269
+ resultget = result['BFEResult_LastCMP']
270
+ self.CbteNro = resultget.get('Cbte_nro')
271
+ self.FechaCbte = resultget.get('Cbte_fecha') # .strftime("%Y/%m/%d")
272
+ return self.CbteNro
273
+
274
+ @inicializar_y_capturar_excepciones
275
+ def GetLastID(self):
276
+ "Recuperar οΏ½ltimo nοΏ½mero de transacciοΏ½n (ID)"
277
+ ret = self.client.BFEGetLast_ID(
278
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
279
+ result = ret['BFEGetLast_IDResult']
280
+ self.__analizar_errores(result)
281
+ if 'BFEResultGet' in result:
282
+ resultget = result['BFEResultGet']
283
+ return resultget.get('Id')
284
+
285
+ @inicializar_y_capturar_excepciones
286
+ def GetParamUMed(self):
287
+ ret = self.client.BFEGetPARAM_UMed(
288
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
289
+ result = ret['BFEGetPARAM_UMedResult']
290
+ self.__analizar_errores(result)
291
+
292
+ umeds = [] # unidades de medida
293
+ for u in result['BFEResultGet']:
294
+ u = u['ClsBFEResponse_UMed']
295
+ try:
296
+ umed = {'id': u.get('Umed_Id'), 'ds': u.get('Umed_Ds'),
297
+ 'vig_desde': u.get('Umed_vig_desde'),
298
+ 'vig_hasta': u.get('Umed_vig_hasta')}
299
+ except Exception as e:
300
+ print(e)
301
+ if u is None:
302
+ # <ClsFEXResponse_UMed xsi:nil="true"/> WTF!
303
+ umed = {'id': '', 'ds': '', 'vig_desde': '', 'vig_hasta': ''}
304
+ #import pdb; pdb.set_trace()
305
+ # print u
306
+
307
+ umeds.append(umed)
308
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % p for p in umeds]
309
+
310
+ @inicializar_y_capturar_excepciones
311
+ def GetParamMon(self):
312
+ ret = self.client.BFEGetPARAM_MON(
313
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
314
+ result = ret['BFEGetPARAM_MONResult']
315
+ self.__analizar_errores(result)
316
+
317
+ mons = [] # unidades de medida
318
+ for m in result['BFEResultGet']:
319
+ m = m['ClsBFEResponse_Mon']
320
+ try:
321
+ mon = {'id': m.get('Mon_Id'), 'ds': m.get('Mon_Ds'),
322
+ 'vig_desde': m.get('Mon_vig_desde'),
323
+ 'vig_hasta': m.get('Mon_vig_hasta')}
324
+ except Exception as e:
325
+ raise
326
+ if m is None:
327
+ # <ClsFEXResponse_UMed xsi:nil="true"/> WTF!
328
+ mon = {'id': '', 'ds': '', 'vig_desde': '', 'vig_hasta': ''}
329
+ #import pdb; pdb.set_trace()
330
+ # print u
331
+ mons.append(mon)
332
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % p for p in mons]
333
+
334
+ @inicializar_y_capturar_excepciones
335
+ def GetParamTipoIVA(self):
336
+ "Recuperar lista de valores referenciales de tipos de IVA (alοΏ½cuotas)"
337
+ ret = self.client.BFEGetPARAM_Tipo_IVA(
338
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
339
+ result = ret['BFEGetPARAM_Tipo_IVAResult']
340
+ self.__analizar_errores(result)
341
+
342
+ ivas = [] # tipos de iva
343
+ for i in result['BFEResultGet']:
344
+ i = i['ClsBFEResponse_Tipo_IVA']
345
+ try:
346
+ iva = {'id': i.get('IVA_Id'), 'ds': i.get('IVA_Ds'),
347
+ 'vig_desde': i.get('IVA_vig_desde'),
348
+ 'vig_hasta': i.get('IVA_vig_hasta')}
349
+ ivas.append(iva)
350
+ except Exception as e:
351
+ pass
352
+ raise
353
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % p for p in ivas]
354
+
355
+ @inicializar_y_capturar_excepciones
356
+ def GetParamTipoDoc(self):
357
+ "Recuperar lista de valores referenciales de tipos de documentos"
358
+ ret = self.client.BFEGetPARAM_Tipo_doc(
359
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
360
+ result = ret['BFEGetPARAM_Tipo_docResult']
361
+ self.__analizar_errores(result)
362
+
363
+ docs = [] # tipos de documentos
364
+ for d in result['BFEResultGet']:
365
+ d = d['ClsBFEResponse_Tipo_doc']
366
+ try:
367
+ doc = {'id': d.get('Doc_Id'), 'ds': d.get('Doc_Ds'),
368
+ 'vig_desde': d.get('Doc_vig_desde'),
369
+ 'vig_hasta': d.get('Doc_vig_hasta')}
370
+ docs.append(doc)
371
+ except Exception as e:
372
+ pass
373
+ raise
374
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % d for d in docs]
375
+
376
+ def GetParamTipoCbte(self):
377
+ "Recuperar lista de valores referenciales de Tipos de Comprobantes"
378
+ ret = self.client.BFEGetPARAM_Tipo_Cbte(
379
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
380
+ result = ret['BFEGetPARAM_Tipo_CbteResult']
381
+ self.__analizar_errores(result)
382
+
383
+ tipos = [] # tipos de comprobantes
384
+ for t in result['BFEResultGet']:
385
+ t = t['ClsBFEResponse_Tipo_Cbte']
386
+ try:
387
+ tipo = {'id': t.get('Cbte_Id'), 'ds': t.get('Cbte_Ds'),
388
+ 'vig_desde': t.get('Cbte_vig_desde'),
389
+ 'vig_hasta': t.get('Cbte_vig_hasta')}
390
+ tipos.append(tipo)
391
+ except Exception as e:
392
+ pass
393
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % p for p in tipos]
394
+
395
+ def GetParamNCM(self):
396
+ "Recuperar lista de valores referenciales de cοΏ½digos del Nomenclador ComοΏ½n del Mercosur"
397
+ ret = self.client.BFEGetPARAM_NCM(
398
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
399
+ result = ret['BFEGetPARAM_NCMResult']
400
+ self.__analizar_errores(result)
401
+
402
+ ncms = [] # nomenclador comun del mercosur
403
+ for n in result['BFEResultGet']:
404
+ n = n['ClsBFEResponse_NCM']
405
+ try:
406
+ ncm = {'id': n.get('NCM_Codigo'), 'ds': n.get('NCM_Ds'),
407
+ 'vig_desde': n.get('NCM_vig_desde'),
408
+ 'vig_hasta': n.get('NCM_vig_hasta')}
409
+ ncms.append(ncm)
410
+ except Exception as e:
411
+ pass
412
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % p for p in ncms]
413
+
414
+ def GetParamZonas(self):
415
+ "Recuperar lista de valores referenciales de Zonas"
416
+ ret = self.client.BFEGetPARAM_Zonas(
417
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
418
+ result = ret['BFEGetPARAM_ZonasResult']
419
+ self.__analizar_errores(result)
420
+
421
+ zonas = [] # zonas
422
+ for z in result['BFEResultGet']:
423
+ z = z['ClsBFEResponse_Zon']
424
+ try:
425
+ zona = {'id': z.get('Zon_Id'), 'ds': z.get('Zon_Ds'),
426
+ 'vig_desde': z.get('Zon_vig_desde'),
427
+ 'vig_hasta': z.get('Zon_vig_hasta')}
428
+ zonas.append(zona)
429
+ except Exception as e:
430
+ pass
431
+ return ['%(id)s: %(ds)s (%(vig_desde)s - %(vig_hasta)s)' % p for p in zonas]
432
+
433
+ @inicializar_y_capturar_excepciones
434
+ def GetParamCtz(self, moneda_id):
435
+ "Recuperador de cotizaciοΏ½n de moneda"
436
+ ret = self.client.BFEGetPARAM_Ctz(
437
+ auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
438
+ MonId=moneda_id,
439
+ )
440
+ self.__analizar_errores(ret['BFEGetPARAM_CtzResult'])
441
+ res = ret['BFEGetPARAM_CtzResult'].get('BFEResultGet')
442
+ if res:
443
+ ctz = str(res.get('Mon_ctz', ""))
444
+ else:
445
+ ctz = ''
446
+ return ctz
447
+
448
+
449
+ class WSBFE(WSBFEv1):
450
+ "Wrapper para retrocompatibilidad con WSBFE"
451
+
452
+ _reg_progid_ = "WSBFE"
453
+ _reg_clsid_ = "{02CBC6DA-455D-4EE6-8302-411D13253CBF}"
454
+
455
+ def __init__(self):
456
+ WSBFEv1.__init__(self)
457
+ self.Version = "%s %s WSBFEv1" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
458
+
459
+ def Conectar(self, cache=None, url="", **kwargs):
460
+ # Ajustar URL de V0 a V1:
461
+ if url in ("https://wswhomo.afip.gov.ar/wsfex/service.asmx",
462
+ "http://wswhomo.afip.gov.ar/WSFEX/service.asmx"):
463
+ url = "https://wswhomo.afip.gov.ar/wsbfev1/service.asmx"
464
+ elif url in ("https://servicios1.afip.gov.ar/wsfex/service.asmx",
465
+ "http://servicios1.afip.gov.ar/WSFEX/service.asmx"):
466
+ url = "https://servicios1.afip.gov.ar/wsbfev1/service.asmx"
467
+ return WSBFEv1.Conectar(self, cache=cache, wsdl=url, **kwargs)
468
+
469
+
470
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
471
+ INSTALL_DIR = WSBFEv1.InstallDir = get_install_dir()
472
+
473
+
474
+ def p_assert_eq(a, b):
475
+ print(a, a == b and '==' or '!=', b)
476
+
477
+
478
+ if __name__ == "__main__":
479
+
480
+ if "--register" in sys.argv or "--unregister" in sys.argv:
481
+ import win32com.server.register
482
+ win32com.server.register.UseCommandLine(WSBFEv1)
483
+ if '--wsbfe' in sys.argv:
484
+ win32com.server.register.UseCommandLine(WSBFE)
485
+ else:
486
+
487
+ # Crear objeto interface Web Service de Factura ElectrοΏ½nica de ExportaciοΏ½n
488
+ wsbfev1 = WSBFEv1()
489
+ # Setear token y sing de autorizaciοΏ½n (pasos previos)
490
+
491
+ # obteniendo el TA para pruebas
492
+ from .wsaa import WSAA
493
+ ta = WSAA().Autenticar("wsbfe", "reingart.crt", "reingart.key")
494
+ wsbfev1.SetTicketAcceso(ta)
495
+
496
+ # CUIT del emisor (debe estar registrado en la AFIP)
497
+ wsbfev1.Cuit = "20267565393"
498
+
499
+ # Conectar al Servicio Web de FacturaciοΏ½n (homologaciοΏ½n)
500
+ wsdl = "http://wswhomo.afip.gov.ar/WSBFEv1/service.asmx"
501
+ cache = proxy = ""
502
+ wrapper = "httplib2"
503
+ cacert = open("conf/afip_ca_info.crt").read()
504
+ ok = wsbfev1.Conectar(cache, wsdl, proxy, wrapper, cacert)
505
+
506
+ if '--dummy' in sys.argv:
507
+ #wsbfev1.LanzarExcepciones = False
508
+ print(wsbfev1.Dummy())
509
+ print("AppServerStatus", wsbfev1.AppServerStatus)
510
+ print("DbServerStatus", wsbfev1.DbServerStatus)
511
+ print("AuthServerStatus", wsbfev1.AuthServerStatus)
512
+
513
+ if "--prueba" in sys.argv:
514
+ try:
515
+ # Establezco los valores de la factura a autorizar:
516
+ tipo_cbte = '--nc' in sys.argv and 3 or 201 # FC/NC Expo (ver tabla de parΓ‘metros)
517
+ punto_vta = 5
518
+ tipo_doc = 80
519
+ nro_doc = 23111111113
520
+ zona = 0
521
+ # Obtengo el οΏ½ltimo nοΏ½mero de comprobante y le agrego 1
522
+ cbte_nro = int(wsbfev1.GetLastCMP(tipo_cbte, punto_vta)) + 1
523
+ fecha_cbte = datetime.datetime.now().strftime("%Y%m%d")
524
+ imp_moneda_id = "PES" # (ver tabla de parοΏ½metros)
525
+ imp_moneda_ctz = 1
526
+ imp_neto = "390.00"
527
+ impto_liq = "81.90" # 21% IVA
528
+ impto_liq_rni = imp_tot_conc = imp_op_ex = "0.00"
529
+ imp_perc = imp_iibb = imp_perc_mun = imp_internos = "0.00"
530
+ imp_total = "471.90"
531
+ fecha_venc_pago = fecha_cbte if "--fce" in sys.argv else None
532
+
533
+ # Creo una factura (internamente, no se llama al WebService):
534
+ ok = wsbfev1.CrearFactura(tipo_doc, nro_doc,
535
+ zona, tipo_cbte, punto_vta, cbte_nro, fecha_cbte,
536
+ imp_total, imp_neto, impto_liq,
537
+ imp_tot_conc, impto_liq_rni, imp_op_ex,
538
+ imp_perc, imp_iibb, imp_perc_mun, imp_internos,
539
+ imp_moneda_id, imp_moneda_ctz, fecha_venc_pago)
540
+
541
+ # Agrego un item:
542
+ ncm = '7308.10.00'
543
+ sec = ''
544
+ umed = 7 # unidades
545
+ ds = 'prueba Anafe economico'
546
+ qty = "2.00"
547
+ precio = "100.00"
548
+ bonif = "0.00"
549
+ iva_id = 5
550
+ imp_total = "242.00"
551
+ ok = wsbfev1.AgregarItem(ncm, sec, ds, qty, umed, precio, bonif, iva_id, imp_total)
552
+
553
+ # Agrego otro item:
554
+ ncm = '7308.20.00'
555
+ sec = ''
556
+ umed = 7 # unidades
557
+ ds = 'prueba 2'
558
+ qty = "4.00"
559
+ precio = "50.00"
560
+ bonif = "10.00"
561
+ iva_id = 5
562
+ imp_total = "229.90"
563
+ ok = wsbfev1.AgregarItem(ncm, sec, ds, qty, umed, precio, bonif, iva_id, imp_total)
564
+
565
+ # comprobantes asociados (notas de crΓ©dito / dΓ©bito)
566
+ if True:
567
+ tipo = 91
568
+ pto_vta = 4001
569
+ nro = 1
570
+ cuit = "20267565393"
571
+ # obligatorio en Factura de CrΓ©dito ElectrΓ³nica MiPyMEs (FCE):
572
+ fecha_cbte = fecha_cbte if "--fce" in sys.argv else None
573
+ wsbfev1.AgregarCmpAsoc(tipo, pto_vta, nro, cuit, fecha_cbte)
574
+
575
+ # datos de Factura de CrΓ©dito ElectrΓ³nica MiPyMEs (FCE):
576
+ if '--fce' in sys.argv:
577
+ wsbfev1.AgregarOpcional(2101, "2850590940090418135201") # CBU
578
+ wsbfev1.AgregarOpcional(2102, "pyafipws") # alias
579
+ if tipo_cbte in (203, 208, 213):
580
+ wsbfev1.AgregarOpcional(22, "S") # AnulaciΓ³n
581
+
582
+ # id = "99000000000100" # nοΏ½mero propio de transacciοΏ½n
583
+ # obtengo el οΏ½ltimo ID y le adiciono 1
584
+ # (advertencia: evitar overflow y almacenar!)
585
+ id = int(wsbfev1.GetLastID()) + 1
586
+
587
+ # Llamo al WebService de AutorizaciοΏ½n para obtener el CAE
588
+ cae = wsbfev1.Authorize(id)
589
+
590
+ print("Comprobante", tipo_cbte, wsbfev1.CbteNro)
591
+ print("Resultado", wsbfev1.Resultado)
592
+ print("CAE", wsbfev1.CAE)
593
+ print("Vencimiento", wsbfev1.Vencimiento)
594
+
595
+ if wsbfev1.Resultado and False:
596
+ print(wsbfev1.client.help("FEXGetCMP").encode("latin1"))
597
+ wsbfev1.GetCMP(tipo_cbte, punto_vta, cbte_nro)
598
+ print("CAE consulta", wsbfev1.CAE, wsbfev1.CAE == cae)
599
+ print("NRO consulta", wsbfev1.CbteNro, wsbfev1.CbteNro == cbte_nro)
600
+ print("TOTAL consulta", wsbfev1.ImpTotal, wsbfev1.ImpTotal == imp_total)
601
+
602
+ except Exception as e:
603
+ print(wsbfev1.XmlRequest)
604
+ print(wsbfev1.XmlResponse)
605
+ print(wsbfev1.ErrCode)
606
+ print(wsbfev1.ErrMsg)
607
+ print(wsbfev1.Excepcion)
608
+ print(wsbfev1.Traceback)
609
+ raise
610
+
611
+ if "--get" in sys.argv:
612
+ tipo_cbte = 1
613
+ punto_vta = 5
614
+ cbte_nro = wsbfev1.GetLastCMP(tipo_cbte, punto_vta)
615
+
616
+ wsbfev1.GetCMP(tipo_cbte, punto_vta, cbte_nro)
617
+
618
+ print("FechaCbte = ", wsbfev1.FechaCbte)
619
+ print("CbteNro = ", wsbfev1.CbteNro)
620
+ print("PuntoVenta = ", wsbfev1.PuntoVenta)
621
+ print("ImpTotal =", wsbfev1.ImpTotal)
622
+ print("CAE = ", wsbfev1.CAE)
623
+ print("Vencimiento = ", wsbfev1.Vencimiento)
624
+
625
+ wsbfev1.AnalizarXml("XmlResponse")
626
+ p_assert_eq(wsbfev1.ObtenerTagXml('Cae'), str(wsbfev1.CAE))
627
+ p_assert_eq(wsbfev1.ObtenerTagXml('Fecha_cbte_orig'), wsbfev1.FechaCbte)
628
+ p_assert_eq(wsbfev1.ObtenerTagXml('Imp_moneda_Id'), "PES")
629
+ p_assert_eq(wsbfev1.ObtenerTagXml('Imp_moneda_ctz'), "1")
630
+ p_assert_eq(wsbfev1.ObtenerTagXml('Items', 'Item', 1, 'Pro_ds'), "prueba 2")
631
+
632
+ if "--params" in sys.argv:
633
+ import codecs
634
+ import locale
635
+ sys.stdout = codecs.getwriter('latin1')(sys.stdout)
636
+
637
+ print("=== Tipos de Comprobante ===")
638
+ print('\n'.join(wsbfev1.GetParamTipoCbte()))
639
+
640
+ print("=== Zonas ===")
641
+ print('\n'.join(wsbfev1.GetParamZonas()))
642
+
643
+ print("=== Monedas ===")
644
+ print('\n'.join(wsbfev1.GetParamMon()))
645
+
646
+ print("=== Tipos de Documentos ===")
647
+ print('\n'.join(wsbfev1.GetParamTipoDoc()))
648
+
649
+ print("=== Tipos de IVA ===")
650
+ print('\n'.join(wsbfev1.GetParamTipoIVA()))
651
+
652
+ print("=== Unidades de medida ===")
653
+ print('\n'.join(wsbfev1.GetParamUMed()))
654
+
655
+ print("=== CοΏ½digos NCM ===")
656
+ print('\n'.join(wsbfev1.GetParamNCM()))
657
+
658
+ if "--ctz" in sys.argv:
659
+ print(wsbfev1.GetParamCtz('DOL'))
app/pyafipws/wscdc.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MοΏ½dulo para utilizar el servicio web ConstataciοΏ½n de Comprobantes de AFIP"
14
+
15
+ # InformaciοΏ½n adicional y documentaciοΏ½n:
16
+ # http://www.sistemasagiles.com.ar/trac/wiki/ConstatacionComprobantes
17
+
18
+ __author__ = "Mariano Reingart (reingart@gmail.com)"
19
+ __copyright__ = "Copyright (C) 2013-2015 Mariano Reingart"
20
+ __license__ = "GPL 3.0"
21
+ __version__ = "1.02e"
22
+
23
+ import sys
24
+ import os
25
+ import time
26
+ from configparser import SafeConfigParser
27
+ from .utils import inicializar_y_capturar_excepciones, BaseWS, get_install_dir
28
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json
29
+
30
+
31
+ # Constantes (si se usa el script de linea de comandos)
32
+ WSDL = "https://wswhomo.afip.gov.ar/WSCDC/service.asmx?WSDL"
33
+ HOMO = False
34
+ CONFIG_FILE = "rece.ini"
35
+
36
+ # No deberοΏ½a ser necesario modificar nada despues de esta linea
37
+
38
+ # definiciοΏ½n del formato del archivo de intercambio (sοΏ½lo para linea de comandos):
39
+
40
+ ENCABEZADO = [
41
+ ('tipo_reg', 1, A, "0: encabezado"),
42
+ ('cbte_modo', 4, A, "Modalidad de autorizaciοΏ½n (CAI, CAE, CAEA)"),
43
+ ('cuit_emisor', 11, A, "CUIT del emisor del comprobante"),
44
+ ('pto_vta', 4, N, "Punto de Venta del comprobante"),
45
+ ('cbte_tipo', 3, N, "Tipo de comprobante"),
46
+ ('cbte_nro', 8, N, "NοΏ½mero de comprobante"),
47
+ ('cbte_fch', 8, A, "Fecha en formato AAAAMMDD"),
48
+ ('imp_total', 15, I, "Importe total Double (13 + 2)"),
49
+ ('cod_autorizacion', 14, A, "NοΏ½mero de CAI, CAE, CAEA"),
50
+ ('doc_tipo_receptor', 2, A, "Tipo de documento del receptor"),
51
+ ('doc_nro_receptor', 20, A, "NοΏ½ de documento del receptor"),
52
+ # campos devueltos por AFIP (respuesta)
53
+ ('resultado', 1, A, "Resultado (A: Aprobado, O: Observado, R: rechazado)"),
54
+ ('fch_proceso', 14, A, "Fecha y hora de procesamiento"),
55
+ ]
56
+
57
+ OBSERVACION = [
58
+ ('tipo_reg', 1, A, "O: observaciones devueltas por AFIP"),
59
+ ('code', 5, N, "CοΏ½digo de ObservaciοΏ½n / Error / Evento"),
60
+ ('msg', 255, A, "Mensaje"),
61
+ ]
62
+
63
+ EVENTO = ERROR = OBSERVACION # misma estructura, cambia tipo de registro
64
+
65
+
66
+ class WSCDC(BaseWS):
67
+ "Interfaz para el WebService de ConstataciοΏ½n de Comprobantes"
68
+ _public_methods_ = ['Conectar', 'SetTicketAcceso', 'DebugLog',
69
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
70
+ 'ConstatarComprobante', 'Dummy',
71
+ 'ConsultarModalidadComprobantes',
72
+ 'ConsultarTipoComprobantes',
73
+ 'ConsultarTipoDocumentos', 'ConsultarTipoOpcionales',
74
+ 'SetParametros', 'SetParametro', 'GetParametro',
75
+ ]
76
+ _public_attrs_ = ['Token', 'Sign', 'Cuit', 'ExpirationTime', 'Version',
77
+ 'XmlRequest', 'XmlResponse', 'Observaciones', 'Errores',
78
+ 'InstallDir', 'Traceback', 'Excepcion', 'ErrMsg', 'Obs',
79
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
80
+ 'Resultado', 'FchProceso', 'Observaciones', 'Obs',
81
+ 'FechaCbte', 'CbteNro', 'PuntoVenta', 'ImpTotal',
82
+ 'EmisionTipo', 'CAE', 'CAEA', 'CAI',
83
+ 'DocTipo', 'DocNro',
84
+ 'SoapFault', 'LanzarExcepciones',
85
+ ]
86
+ _readonly_attrs_ = _public_attrs_[3:-1]
87
+ _reg_progid_ = "WSCDC"
88
+ _reg_clsid_ = "{D1B97BDD-A78C-4D51-8999-1D9A5034EC10}"
89
+
90
+ # Variables globales para BaseWS:
91
+ HOMO = HOMO
92
+ WSDL = WSDL
93
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
94
+
95
+ def inicializar(self):
96
+ BaseWS.inicializar(self)
97
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
98
+ self.Resultado = self.EmisionTipo = ""
99
+ self.CAI = self.CAE = self.CAEA = self.Vencimiento = ''
100
+ self.CbteNro = self.PuntoVenta = self.ImpTotal = None
101
+
102
+ def __analizar_errores(self, ret):
103
+ "Comprueba y extrae errores si existen en la respuesta XML"
104
+ if 'Errors' in ret:
105
+ errores = ret['Errors']
106
+ for error in errores:
107
+ self.Errores.append("%s: %s" % (
108
+ error['Err']['Code'],
109
+ error['Err']['Msg'],
110
+ ))
111
+ self.errores = [
112
+ {'code': err['Err']['Code'],
113
+ 'msg': err['Err']['Msg'].replace("\n", "")
114
+ .replace("\r", "")}
115
+ for err in errores]
116
+ self.ErrCode = ' '.join([str(error['Err']['Code']) for error in errores])
117
+ self.ErrMsg = '\n'.join(self.Errores)
118
+
119
+ @inicializar_y_capturar_excepciones
120
+ def Dummy(self):
121
+ "MοΏ½todo Dummy para verificaciοΏ½n de funcionamiento de infraestructura"
122
+ result = self.client.ComprobanteDummy()['ComprobanteDummyResult']
123
+ self.AppServerStatus = result['AppServer']
124
+ self.DbServerStatus = result['DbServer']
125
+ self.AuthServerStatus = result['AuthServer']
126
+ self.__analizar_errores(result)
127
+ return True
128
+
129
+ @inicializar_y_capturar_excepciones
130
+ def ConstatarComprobante(self, cbte_modo, cuit_emisor, pto_vta, cbte_tipo,
131
+ cbte_nro, cbte_fch, imp_total, cod_autorizacion,
132
+ doc_tipo_receptor=None, doc_nro_receptor=None,
133
+ **kwargs):
134
+ "MοΏ½todo de ConstataciοΏ½n de Comprobantes"
135
+ response = self.client.ComprobanteConstatar(
136
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
137
+ CmpReq={
138
+ 'CbteModo': cbte_modo,
139
+ 'CuitEmisor': cuit_emisor,
140
+ 'PtoVta': pto_vta,
141
+ 'CbteTipo': cbte_tipo,
142
+ 'CbteNro': cbte_nro,
143
+ 'CbteFch': cbte_fch,
144
+ 'ImpTotal': imp_total,
145
+ 'CodAutorizacion': cod_autorizacion,
146
+ 'DocTipoReceptor': doc_tipo_receptor,
147
+ 'DocNroReceptor': doc_nro_receptor,
148
+ }
149
+ )
150
+ result = response['ComprobanteConstatarResult']
151
+ self.__analizar_errores(result)
152
+ if 'CmpResp' in result:
153
+ resp = result['CmpResp']
154
+ self.Resultado = result['Resultado']
155
+ self.FchProceso = result.get('FchProceso', "")
156
+ self.observaciones = []
157
+ for obs in result.get('Observaciones', []):
158
+ self.Observaciones.append("%(Code)s: %(Msg)s" % (obs['Obs']))
159
+ self.observaciones.append({
160
+ 'code': obs['Obs']['Code'],
161
+ 'msg': obs['Obs']['Msg'].replace("\n", "")
162
+ .replace("\r", "")})
163
+ self.Obs = '\n'.join(self.Observaciones)
164
+ self.FechaCbte = resp.get('CbteFch', "") # .strftime("%Y/%m/%d")
165
+ self.CbteNro = resp.get('CbteNro', 0) # 1L
166
+ self.PuntoVenta = resp.get('PtoVta', 0) # 4000
167
+ self.ImpTotal = str(resp['ImpTotal'])
168
+ self.EmisionTipo = resp['CbteModo']
169
+ self.DocTipo = resp.get('DocTipoReceptor', '')
170
+ self.DocNro = resp.get('DocNroReceptor', '')
171
+ cod_aut = str(resp.get('CodAutorizacion', "")) # 60423794871430L
172
+ if self.EmisionTipo == 'CAE':
173
+ self.CAE = cod_aut
174
+ elif self.EmisionTipo == 'CAEA':
175
+ self.CAEA = cod_aut
176
+ elif self.EmisionTipo == 'CAI':
177
+ self.CAI = cod_aut
178
+ return True
179
+
180
+ @inicializar_y_capturar_excepciones
181
+ def ConsultarModalidadComprobantes(self, sep="|"):
182
+ "Recuperador de modalidades de autorizaciοΏ½n de comprobantes"
183
+ response = self.client.ComprobantesModalidadConsultar(
184
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
185
+ )
186
+ result = response['ComprobantesModalidadConsultarResult']
187
+ self.__analizar_errores(result)
188
+ return [("\t%(Cod)s\t%(Desc)s\t" % p['FacModTipo']).replace("\t", sep)
189
+ for p in result['ResultGet']]
190
+
191
+ @inicializar_y_capturar_excepciones
192
+ def ConsultarTipoComprobantes(self, sep="|"):
193
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de comprobante"
194
+ response = self.client.ComprobantesTipoConsultar(
195
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
196
+ )
197
+ result = response['ComprobantesTipoConsultarResult']
198
+ self.__analizar_errores(result)
199
+ return [("\t%(Id)s\t%(Desc)s\t" % p['CbteTipo']).replace("\t", sep)
200
+ for p in result['ResultGet']]
201
+
202
+ @inicializar_y_capturar_excepciones
203
+ def ConsultarTipoDocumentos(self, sep="|"):
204
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de Documentos"
205
+ response = self.client.DocumentosTipoConsultar(
206
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
207
+ )
208
+ result = response['DocumentosTipoConsultarResult']
209
+ self.__analizar_errores(result)
210
+ return [("\t%(Id)s\t%(Desc)s\t" % p['DocTipo']).replace("\t", sep)
211
+ for p in result['ResultGet']]
212
+
213
+ @inicializar_y_capturar_excepciones
214
+ def ConsultarTipoOpcionales(self, sep="|"):
215
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de datos Opcionales"
216
+ response = self.client.OpcionalesTipoConsultar(
217
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
218
+ )
219
+ result = response['OpcionalesTipoConsultarResult']
220
+ res = result['ResultGet'] if 'ResultGet' in result else []
221
+ self.__analizar_errores(result)
222
+ return [("\t%(Id)s\t%(Desc)s\t" % p['OpcionalTipo']).replace("\t", sep)
223
+ for p in res]
224
+
225
+
226
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
227
+ INSTALL_DIR = WSCDC.InstallDir = get_install_dir()
228
+
229
+
230
+ def escribir_archivo(dic, nombre_archivo, agrega=True):
231
+ archivo = open(nombre_archivo, agrega and "a" or "w")
232
+ formatos = [('Encabezado', ENCABEZADO, [dic], 0),
233
+ ('Observacion', OBSERVACION, dic.get('observaciones', []), 'O'),
234
+ ('Eventos', ERROR, dic.get('eventos', []), 'V'),
235
+ ('Error', ERROR, dic.get('errores', []), 'E'),
236
+ ]
237
+ if '--json' in sys.argv:
238
+ json.dump(dic, archivo, sort_keys=True, indent=4)
239
+ elif '--dbf' in sys.argv:
240
+ guardar_dbf(formatos, agrega, conf_dbf)
241
+ else:
242
+ for nombre, formato, registros, tipo_reg in formatos:
243
+ for it in registros:
244
+ it['tipo_reg'] = tipo_reg
245
+ archivo.write(escribir(it, formato))
246
+ archivo.close()
247
+
248
+
249
+ def leer_archivo(nombre_archivo):
250
+ archivo = open(nombre_archivo, "r")
251
+ if '--json' in sys.argv:
252
+ dic = json.load(archivo)
253
+ elif '--dbf' in sys.argv:
254
+ dic = {}
255
+ formatos = [('Encabezado', ENCABEZADO, dic),
256
+ ]
257
+ leer_dbf(formatos, conf_dbf)
258
+ else:
259
+ dic = {}
260
+ for linea in archivo:
261
+ if str(linea[0]) == '0':
262
+ d = leer(linea, ENCABEZADO)
263
+ dic.update(d)
264
+ else:
265
+ print("Tipo de registro incorrecto:", linea[0])
266
+ archivo.close()
267
+
268
+ if not 'cod_autorizacion' in dic:
269
+ raise RuntimeError("Archivo de entrada invalido, revise campos y lineas en blanco")
270
+
271
+ return dic
272
+
273
+
274
+ def main():
275
+ "Funcion principal para utilizar la interfaz por linea de comando"
276
+
277
+ if '--formato' in sys.argv:
278
+ print("Formato:")
279
+ for msg, formato in [('Encabezado', ENCABEZADO),
280
+ ('Observacion', OBSERVACION),
281
+ ('Evento', EVENTO), ('Error', ERROR),
282
+ ]:
283
+ comienzo = 1
284
+ print("=== %s ===" % msg)
285
+ print("|| %-20s || %8s || %9s || %-12s || %-20s ||" % (
286
+ "Campo", "PosiciοΏ½n", "Longitud", "Tipo", "DescripciοΏ½n"))
287
+ for fmt in formato:
288
+ clave, longitud, tipo, desc = fmt
289
+ print("|| %-20s || %8d || %9d || %-12s || %-20s ||" % (
290
+ clave, comienzo, longitud, tipo, desc.encode("latin1")))
291
+ comienzo += longitud
292
+ sys.exit(0)
293
+
294
+ # leer configuracion
295
+ global CONFIG_FILE
296
+ if len(sys.argv) > 1 and sys.argv[1][0] not in "-/":
297
+ CONFIG_FILE = sys.argv.pop(1)
298
+ config = SafeConfigParser()
299
+ config.read(CONFIG_FILE)
300
+ crt = config.get('WSAA', 'CERT')
301
+ key = config.get('WSAA', 'PRIVATEKEY')
302
+ cuit = config.get('WSCDC', 'CUIT')
303
+ url_wsaa = config.get('WSAA', 'URL') if config.has_option('WSAA', 'URL') else ""
304
+ url_wscdc = config.get('WSCDC', 'URL') if config.has_option('WSCDC', 'URL') else ""
305
+
306
+ # leo configuraciοΏ½n de archivos de intercambio
307
+ ENTRADA = config.get('WSCDC', 'ENTRADA')
308
+ SALIDA = config.get('WSCDC', 'SALIDA')
309
+ if config.has_section('DBF'):
310
+ conf_dbf = dict(config.items('DBF'))
311
+ else:
312
+ conf_dbf = {}
313
+
314
+ # instanciar la interfaz con el webservice
315
+ wscdc = WSCDC()
316
+ ok = wscdc.Conectar("", url_wscdc)
317
+
318
+ if "--dummy" in sys.argv:
319
+ # print wscdc.client.help("ComprobanteDummy")
320
+ wscdc.Dummy()
321
+ print("AppServerStatus", wscdc.AppServerStatus)
322
+ print("DbServerStatus", wscdc.DbServerStatus)
323
+ print("AuthServerStatus", wscdc.AuthServerStatus)
324
+ sys.exit(0)
325
+
326
+ # Gestionar credenciales de acceso con AFIP:
327
+ from .wsaa import WSAA
328
+ wsaa = WSAA()
329
+ ta = wsaa.Autenticar("wscdc", crt, key, url_wsaa)
330
+ if not ta:
331
+ sys.exit("Imposible autenticar con WSAA: %s" % wsaa.Excepcion)
332
+ wscdc.SetTicketAcceso(ta)
333
+ wscdc.Cuit = cuit
334
+
335
+ if "--constatar" in sys.argv:
336
+ if len(sys.argv) < 8:
337
+ if "--prueba" in sys.argv:
338
+ dic = dict(
339
+ cbte_modo="CAE",
340
+ cuit_emisor="20267565393",
341
+ pto_vta=3,
342
+ cbte_tipo=6,
343
+ cbte_nro=1,
344
+ cbte_fch="20131231",
345
+ imp_total="1.21",
346
+ cod_autorizacion="63533727749637",
347
+ doc_tipo_receptor=99,
348
+ doc_nro_receptor=0,
349
+ )
350
+ # escribir archivo de intercambio con datos de prueba:
351
+ escribir_archivo(dic, ENTRADA)
352
+ else:
353
+ # leer archivo de intercambio:
354
+ dic = leer_archivo(ENTRADA)
355
+ # constatar el comprobante
356
+ wscdc.ConstatarComprobante(**dic)
357
+ # actualizar el diccionario con los datos de devueltos por AFIP
358
+ dic.update({'resultado': wscdc.Resultado,
359
+ 'fch_proceso': wscdc.FchProceso,
360
+ })
361
+ dic['observaciones'] = wscdc.observaciones
362
+ dic['errores'] = wscdc.errores
363
+ escribir_archivo(dic, SALIDA)
364
+ else:
365
+ # usar los datos pasados por linea de comandos:
366
+ wscdc.ConstatarComprobante(*sys.argv[sys.argv.index("--constatar") + 1:])
367
+
368
+ print("Resultado:", wscdc.Resultado)
369
+ print("Mensaje de Error:", wscdc.ErrMsg)
370
+ print("Observaciones:", wscdc.Obs)
371
+
372
+ if "--params" in sys.argv:
373
+
374
+ print("=== Modalidad Comprobantes ===")
375
+ print('\n'.join(wscdc.ConsultarModalidadComprobantes("||")))
376
+ print("=== Tipo Comprobantes ===")
377
+ print('\n'.join(wscdc.ConsultarTipoComprobantes("||")))
378
+ print("=== Tipo Documentos ===")
379
+ print('\n'.join(wscdc.ConsultarTipoDocumentos("||")))
380
+ print("=== Tipo Opcionales ===")
381
+ print('\n'.join(wscdc.ConsultarTipoOpcionales("||")))
382
+ print("Mensaje de Error:", wscdc.ErrMsg)
383
+
384
+
385
+ if __name__ == "__main__":
386
+
387
+ if '--register' in sys.argv or '--unregister' in sys.argv:
388
+ import pythoncom
389
+ import win32com.server.register
390
+ win32com.server.register.UseCommandLine(WSCDC)
391
+ elif "/Automate" in sys.argv:
392
+ # MS seems to like /automate to run the class factories.
393
+ import win32com.server.localserver
394
+ # win32com.server.localserver.main()
395
+ # start the server.
396
+ win32com.server.localserver.serve([WSCDC._reg_clsid_])
397
+ else:
398
+ main()
app/pyafipws/wscoc.py ADDED
@@ -0,0 +1,1032 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ "MοΏ½dulo para Consulta de Operaciones Cambiarias"
14
+
15
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
16
+ __copyright__ = "Copyright (C) 2011 Mariano Reingart"
17
+ __license__ = "GPL 3.0"
18
+ __version__ = "1.09a"
19
+
20
+ import os
21
+ import socket
22
+ import sys
23
+ import traceback
24
+ import pysimplesoap.client
25
+ from pysimplesoap.client import SoapClient, SoapFault, parse_proxy, \
26
+ set_http_wrapper
27
+ from pysimplesoap.simplexml import SimpleXMLElement
28
+ from io import StringIO
29
+
30
+ HOMO = True
31
+
32
+ if HOMO:
33
+ WSDL = "https://fwshomo.afip.gov.ar/wscoc/COCService"
34
+ LOCATION = "https://fwshomo.afip.gob.ar:443/wscoc2/COCService"
35
+ else:
36
+ WSDL = "https://serviciosjava.afip.gob.ar/wscoc2/COCService"
37
+ LOCATION = "https://serviciosjava.afip.gob.ar:443/wscoc2/COCService"
38
+
39
+
40
+ def inicializar_y_capturar_excepciones(func):
41
+ "Decorador para inicializar y capturar errores"
42
+
43
+ def capturar_errores_wrapper(self, *args, **kwargs):
44
+ try:
45
+ # inicializo (limpio variables)
46
+ self.Errores = []
47
+ self.ErroresFormato = []
48
+ self.Traceback = self.Excepcion = ""
49
+ self.ErrCode = ""
50
+ self.ErrMsg = ""
51
+ self.CodigoSolicitud = self.FechaSolicitud = None
52
+ self.COC = self.FechaEmisionCOC = self.CodigoDestino = None
53
+ self.EstadoSolicitud = self.FechaEstado = None
54
+ self.CUITComprador = self.DenominacionComprador = None
55
+ self.CodigoMoneda = self.CotizacionMoneda = self.MontoPesos = None
56
+ self.CUITRepresentante = self.DenominacionRepresentante = None
57
+ self.DJAI = self.CodigoExcepcionDJAI = self.EstadoDJAI = None
58
+ self.DJAS = self.CodigoExcepcionDJAS = self.EstadoDJAS = None
59
+ self.MontoFOB = self.CodigoMoneda = None
60
+ # iniciaalizo estructuras internas persistentes
61
+ self.__detalles_solicitudes = []
62
+ self.__detalles_cuit = []
63
+
64
+ # llamo a la funciοΏ½n (sin reintentos)
65
+ return func(self, *args, **kwargs)
66
+
67
+ except SoapFault as e:
68
+ # guardo destalle de la excepciοΏ½n SOAP
69
+ self.ErrCode = str(e.faultcode)
70
+ self.ErrMsg = str(e.faultstring)
71
+ self.Excepcion = "%s: %s" % (e.faultcode, e.faultstring, )
72
+ if self.LanzarExcepciones:
73
+ raise
74
+ except Exception as e:
75
+ ex = traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1],
76
+ sys.exc_info()[2])
77
+ self.Traceback = ''.join(ex)
78
+ self.Excepcion = "%s" % (e)
79
+ if self.LanzarExcepciones:
80
+ raise
81
+ finally:
82
+ # guardo datos de depuraciοΏ½n
83
+ if self.client:
84
+ self.XmlRequest = self.client.xml_request
85
+ self.XmlResponse = self.client.xml_response
86
+ return capturar_errores_wrapper
87
+
88
+
89
+ class WSCOC:
90
+ "Interfaz para el WebService de Consulta de Operaciones Cambiarias"
91
+ _public_methods_ = ['GenerarSolicitudCompraDivisa',
92
+ 'GenerarSolicitudCompraDivisaTurExt',
93
+ 'InformarSolicitudCompraDivisa',
94
+ 'ConsultarCUIT',
95
+ 'ConsultarCOC',
96
+ 'AnularCOC',
97
+ 'ConsultarSolicitudCompraDivisa',
98
+ 'ConsultarSolicitudesCompraDivisas',
99
+ 'ConsultarDestinosCompra',
100
+ 'ConsultarTiposReferencia',
101
+ 'ConsultarMonedas',
102
+ 'ConsultarTiposDocumento',
103
+ 'ConsultarTiposEstadoSolicitud',
104
+ 'ConsultarMotivosExcepcionDJAI',
105
+ 'ConsultarDestinosCompraDJAI',
106
+ 'ConsultarMotivosExcepcionDJAS',
107
+ 'ConsultarDestinosCompraDJAS',
108
+ 'ConsultarDestinosCompraTipoReferencia',
109
+ 'LeerSolicitudConsultada', 'LeerCUITConsultado',
110
+ 'ConsultarDJAI', 'ConsultarDJAS', 'ConsultarReferencia',
111
+ 'LeerError', 'LeerErrorFormato', 'LeerInconsistencia',
112
+ 'LoadTestXML',
113
+ 'AnalizarXml', 'ObtenerTagXml',
114
+ 'Dummy', 'Conectar', 'DebugLog']
115
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
116
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
117
+ 'XmlRequest', 'XmlResponse', 'Version', 'InstallDir',
118
+ 'Resultado', 'Inconsistencias', 'ErrCode', 'ErrMsg',
119
+ 'CodigoSolicitud', 'FechaSolicitud', 'EstadoSolicitud', 'FechaEstado',
120
+ 'COC', 'FechaEmisionCOC', 'CodigoDestino',
121
+ 'CUITComprador', 'DenominacionComprador',
122
+ 'CodigoMoneda', 'CotizacionMoneda', 'MontoPesos',
123
+ 'CUITRepresentante', 'DenominacionRepresentante',
124
+ 'TipoDoc', 'NumeroDoc', 'CUITConsultada', 'DenominacionConsultada',
125
+ 'DJAI', 'CodigoExcepcionDJAI', 'DJAS', 'CodigoExcepcionDJAS',
126
+ 'MontoFOB', 'EstadoDJAI', 'EstadoDJAS', 'Estado', 'Tipo', 'Codigo',
127
+ 'ErroresFormato', 'Errores', 'Traceback', 'Excepcion', 'LanzarExcepciones',
128
+ ]
129
+
130
+ _reg_progid_ = "WSCOC"
131
+ _reg_clsid_ = "{B30406CE-326A-46D9-B807-B7916E3F1B96}"
132
+
133
+ Version = "%s %s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '', pysimplesoap.client.__file__)
134
+ LanzarExcepciones = False
135
+
136
+ def __init__(self):
137
+ self.Token = self.Sign = self.Cuit = None
138
+ self.AppServerStatus = None
139
+ self.DbServerStatus = None
140
+ self.AuthServerStatus = None
141
+ self.XmlRequest = ''
142
+ self.XmlResponse = ''
143
+ self.Resultado = self.Motivo = self.Reproceso = ''
144
+ self.__analizar_solicitud({})
145
+ self.__analizar_inconsistencias({})
146
+ self.__analizar_errores({})
147
+ self.__detalles_solicitudes = None
148
+ self.__detalles_cuit = None
149
+ self.client = None
150
+ self.ErrCode = self.ErrMsg = self.Traceback = self.Excepcion = ""
151
+ self.EmisionTipo = ''
152
+ self.Reprocesar = self.Reproceso = '' # no implementado
153
+ self.Log = None
154
+ self.InstallDir = INSTALL_DIR
155
+
156
+ def __analizar_errores(self, ret):
157
+ "Comprueba y extrae errores si existen en la respuesta XML"
158
+ self.Errores = []
159
+ self.ErroresFormato = []
160
+ if 'arrayErrores' in ret:
161
+ errores = ret['arrayErrores']
162
+ for error in errores:
163
+ self.Errores.append("%s: %s" % (
164
+ error['codigoDescripcion']['codigo'],
165
+ error['codigoDescripcion']['descripcion'],
166
+ ))
167
+ if 'arrayErroresFormato' in ret:
168
+ errores = ret['arrayErroresFormato']
169
+ for error in errores:
170
+ self.ErroresFormato.append("%s: %s" % (
171
+ error['codigoDescripcionString']['codigo'],
172
+ error['codigoDescripcionString']['descripcion'],
173
+ ))
174
+
175
+ def __analizar_solicitud(self, det):
176
+ "Analiza y extrae los datos de una solicitud"
177
+ self.CodigoSolicitud = det.get("codigoSolicitud")
178
+ self.FechaSolicitud = det.get("fechaSolicitud")
179
+ self.COC = str(det.get("coc"))
180
+ self.FechaEmisionCOC = det.get("fechaEmisionCOC")
181
+ self.EstadoSolicitud = det.get("estadoSolicitud")
182
+ self.FechaEstado = det.get("fechaEstado")
183
+ self.CUITComprador = str(det.get("detalleCUITComprador",
184
+ {}).get("cuit", ""))
185
+ self.DenominacionComprador = det.get("detalleCUITComprador",
186
+ {}).get("denominacion")
187
+ self.CodigoMoneda = det.get("codigoMoneda")
188
+ self.CotizacionMoneda = det.get("cotizacionMoneda")
189
+ self.MontoPesos = det.get("montoPesos")
190
+ self.CUITRepresentante = str(det.get("DetalleCUITRepresentante",
191
+ {}).get("cuit", ""))
192
+ self.DenominacionRepresentante = det.get("DetalleCUITRepresentante",
193
+ {}).get("denominacion")
194
+ self.CodigoDestino = det.get("codigoDestino")
195
+ self.DJAI = det.get("djai")
196
+ self.CodigoExcepcionDJAI = det.get("codigoExcepcionDJAI")
197
+ self.DJAS = det.get("djas")
198
+ self.CodigoExcepcionDJAS = det.get("codigoExcepcionDJAS")
199
+ ref = det.get("referencia")
200
+ if ref:
201
+ self.Tipo = ref['tipo']
202
+ self.Codigo = ref['codigo']
203
+
204
+ def __analizar_inconsistencias(self, ret):
205
+ "Comprueba y extrae (formatea) las inconsistencias"
206
+ self.Inconsistencias = []
207
+ if 'arrayInconsistencias' in ret:
208
+ inconsistencias = ret['arrayInconsistencias']
209
+ for inconsistencia in inconsistencias:
210
+ self.Inconsistencias.append("%s: %s" % (
211
+ inconsistencia['codigoDescripcion']['codigo'],
212
+ inconsistencia['codigoDescripcion']['descripcion'],
213
+ ))
214
+
215
+ def __log(self, msg):
216
+ if not isinstance(msg, str):
217
+ msg = str(msg, 'utf8', 'ignore')
218
+ if not self.Log:
219
+ self.Log = StringIO()
220
+ self.Log.write(msg)
221
+ self.Log.write('\n\r')
222
+
223
+ def DebugLog(self):
224
+ "Devolver y limpiar la bitοΏ½cora de depuraciοΏ½n"
225
+ if self.Log:
226
+ msg = self.Log.getvalue()
227
+ # limpiar log
228
+ self.Log.close()
229
+ self.Log = None
230
+ else:
231
+ msg = ''
232
+ return msg
233
+
234
+ @inicializar_y_capturar_excepciones
235
+ def Conectar(self, cache=None, wsdl=None, proxy="", wrapper=None, cacert=None, timeout=30):
236
+ # cliente soap del web service
237
+ if timeout:
238
+ self.__log("Estableciendo timeout=%s" % (timeout, ))
239
+ socket.setdefaulttimeout(timeout)
240
+ if wrapper:
241
+ Http = set_http_wrapper(wrapper)
242
+ self.Version = WSCOC.Version + " " + Http._wrapper_version
243
+ proxy_dict = parse_proxy(proxy)
244
+ location = LOCATION
245
+ if HOMO or not wsdl:
246
+ wsdl = WSDL
247
+ elif not wsdl.endswith("?wsdl") and wsdl.startswith("http"):
248
+ location = wsdl
249
+ wsdl += "?wsdl"
250
+ elif wsdl.endswith("?wsdl"):
251
+ location = wsdl[:-5]
252
+ if not cache or HOMO:
253
+ # use 'cache' from installation base directory
254
+ cache = os.path.join(self.InstallDir, 'cache')
255
+ self.__log("Conectando a wsdl=%s cache=%s proxy=%s" % (wsdl, cache, proxy_dict))
256
+ self.client = SoapClient(
257
+ wsdl=wsdl,
258
+ cache=cache,
259
+ proxy=proxy_dict,
260
+ ns="coc",
261
+ cacert=cacert,
262
+ soap_ns="soapenv",
263
+ soap_server="jbossas6",
264
+ trace="--trace" in sys.argv)
265
+ # corrijo ubicaciοΏ½n del servidor (http en el WSDL)
266
+ self.client.services['COCService']['ports']['COCServiceHttpSoap11Endpoint']['location'] = location
267
+ self.__log("Corrigiendo location=%s" % (location, ))
268
+ return True
269
+
270
+ @inicializar_y_capturar_excepciones
271
+ def Dummy(self):
272
+ "Obtener el estado de los servidores de la AFIP"
273
+ result = self.client.dummy()
274
+ ret = result['dummyReturn']
275
+ self.AppServerStatus = ret['appserver']
276
+ self.DbServerStatus = ret['dbserver']
277
+ self.AuthServerStatus = ret['authserver']
278
+ return True
279
+
280
+ @inicializar_y_capturar_excepciones
281
+ def GenerarSolicitudCompraDivisa(self, cuit_comprador, codigo_moneda,
282
+ cotizacion_moneda, monto_pesos,
283
+ cuit_representante, codigo_destino,
284
+ djai=None, codigo_excepcion_djai=None,
285
+ djas=None, codigo_excepcion_djas=None,
286
+ tipo=None, codigo=None,
287
+ ):
288
+ "Generar una Solicitud de operaciοΏ½n cambiaria"
289
+ if tipo and codigo:
290
+ referencia = {'tipo': tipo, 'codigo': codigo}
291
+ else:
292
+ referencia = None
293
+ res = self.client.generarSolicitudCompraDivisa(
294
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
295
+ cuitComprador=cuit_comprador,
296
+ codigoMoneda=codigo_moneda,
297
+ cotizacionMoneda=cotizacion_moneda,
298
+ montoPesos=monto_pesos,
299
+ cuitRepresentante=cuit_representante,
300
+ codigoDestino=codigo_destino,
301
+ djai=djai, codigoExcepcionDJAI=codigo_excepcion_djai,
302
+ djas=djas, codigoExcepcionDJAS=codigo_excepcion_djas,
303
+ referencia=referencia,
304
+ )
305
+
306
+ self.Resultado = ""
307
+ ret = res.get('generarSolicitudCompraDivisaReturn', {})
308
+ self.Resultado = ret.get('resultado')
309
+ det = ret.get('detalleSolicitud', {})
310
+ self.__analizar_solicitud(det)
311
+ self.__analizar_inconsistencias(det)
312
+ self.__analizar_errores(ret)
313
+
314
+ return True
315
+
316
+ @inicializar_y_capturar_excepciones
317
+ def GenerarSolicitudCompraDivisaTurExt(self, tipo_doc, numero_doc, apellido_nombre,
318
+ codigo_moneda, cotizacion_moneda, monto_pesos,
319
+ cuit_representante, codigo_destino,
320
+ ):
321
+ "Generar una Solicitud de operaciοΏ½n cambiaria"
322
+ res = self.client.generarSolicitudCompraDivisaTurExt(
323
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
324
+ detalleTurExtComprador={
325
+ 'tipoNumeroDoc': {'tipoDoc': tipo_doc, 'numeroDoc': numero_doc},
326
+ 'apellidoNombre': apellido_nombre},
327
+ codigoMoneda=codigo_moneda,
328
+ cotizacionMoneda=cotizacion_moneda,
329
+ montoPesos=monto_pesos,
330
+ cuitRepresentante=cuit_representante,
331
+ codigoDestino=codigo_destino,
332
+ )
333
+
334
+ self.Resultado = ""
335
+ ret = res.get('generarSolicitudCompraDivisaTurExtReturn', {})
336
+ self.Resultado = ret.get('resultado')
337
+ det = ret.get('detalleSolicitud', {})
338
+ self.__analizar_solicitud(det)
339
+ self.__analizar_inconsistencias(det)
340
+ self.__analizar_errores(ret)
341
+
342
+ return True
343
+
344
+ @inicializar_y_capturar_excepciones
345
+ def InformarSolicitudCompraDivisa(self, codigo_solicitud, nuevo_estado):
346
+ "Informar la aceptaciοΏ½n o desistir una solicitud generada con anterioridad"
347
+
348
+ res = self.client.informarSolicitudCompraDivisa(
349
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
350
+ codigoSolicitud=codigo_solicitud,
351
+ nuevoEstado=nuevo_estado,
352
+ )
353
+
354
+ self.Resultado = ""
355
+ ret = res.get('informarSolicitudCompraDivisaReturn', {})
356
+ self.Resultado = ret.get('resultado')
357
+ self.__analizar_solicitud(ret)
358
+ self.__analizar_errores(ret)
359
+ return True
360
+
361
+ @inicializar_y_capturar_excepciones
362
+ def ConsultarCUIT(self, numero_doc, tipo_doc=80, sep="|"):
363
+ "Consultar la CUIT, CDI οΏ½ CUIL, segοΏ½n corresponda, para un determinado tipo y nοΏ½mero de documento."
364
+
365
+ res = self.client.consultarCUIT(
366
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
367
+ tipoNumeroDoc={'tipoDoc': tipo_doc, 'numeroDoc': numero_doc}
368
+ )
369
+
370
+ self.__detalles_cuit = []
371
+
372
+ if 'consultarCUITReturn' in res:
373
+ ret = res['consultarCUITReturn']
374
+ self.__analizar_errores(ret)
375
+ if 'tipoNumeroDoc' in ret:
376
+ self.TipoDoc = ret['tipoNumeroDoc']['tipoDoc']
377
+ self.NumeroDoc = ret['tipoNumeroDoc']['numeroDoc']
378
+ for detalle in ret.get('arrayDetallesCUIT', []):
379
+ # agrego el detalle para consultarlo luego (LeerCUITConsultado)
380
+ det = detalle['detalleCUIT']
381
+ self.__detalles_cuit.append(det)
382
+ # devuelvo una lista de cuit/denominaciοΏ½n
383
+ return [("%(cuit)s\t%(denominacion)s" %
384
+ d['detalleCUIT']).replace("\t", sep)
385
+ for d in ret.get('arrayDetallesCUIT', [])]
386
+ else:
387
+ return []
388
+ else:
389
+ self.TipoDoc = None
390
+ self.NumeroDoc = None
391
+ return [""]
392
+
393
+ def LeerCUITConsultado(self):
394
+ "Recorro los CUIT devueltos por ConsultarCUIT"
395
+
396
+ if self.__detalles_cuit:
397
+ # extraigo el primer item
398
+ det = self.__detalles_cuit.pop(0)
399
+ self.CUITConsultada = str(det['cuit'])
400
+ self.DenominacionConsultada = str(det['denominacion'])
401
+ return True
402
+ else:
403
+ return False
404
+
405
+ @inicializar_y_capturar_excepciones
406
+ def ConsultarCOC(self, coc):
407
+ "Obtener los datos de un COC existente"
408
+
409
+ res = self.client.consultarCOC(
410
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
411
+ coc=coc,
412
+ )
413
+ ret = res.get('consultarCOCReturn', {})
414
+ det = ret.get('detalleSolicitud', {})
415
+ self.__analizar_solicitud(det)
416
+ self.__analizar_inconsistencias(det)
417
+ self.__analizar_errores(ret)
418
+ return True
419
+
420
+ @inicializar_y_capturar_excepciones
421
+ def AnularCOC(self, coc, cuit_comprador):
422
+ "Anular COC existente (estado CO 24hs)"
423
+ res = self.client.anularCOC(
424
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
425
+ coc=coc,
426
+ cuitComprador=cuit_comprador,
427
+ )
428
+
429
+ ret = res.get('anularCOCReturn', {})
430
+ self.__analizar_solicitud(ret)
431
+ self.__analizar_inconsistencias(ret)
432
+ self.__analizar_errores(ret)
433
+
434
+ return True
435
+
436
+ @inicializar_y_capturar_excepciones
437
+ def ConsultarSolicitudCompraDivisa(self, codigo_solicitud):
438
+ "Consultar una Solicitud de OperaciοΏ½n Cambiaria"
439
+ res = self.client.consultarSolicitudCompraDivisa(
440
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
441
+ codigoSolicitud=codigo_solicitud,
442
+ )
443
+
444
+ ret = res.get('consultarSolicitudCompraDivisaReturn', {})
445
+ det = ret.get('detalleSolicitud', {})
446
+ self.__analizar_solicitud(det)
447
+ self.__analizar_errores(ret)
448
+
449
+ return True
450
+
451
+ @inicializar_y_capturar_excepciones
452
+ def ConsultarSolicitudesCompraDivisas(self, cuit_comprador,
453
+ estado_solicitud,
454
+ fecha_emision_desde,
455
+ fecha_emision_hasta,
456
+ ):
457
+ "Consultar Solicitudes de operaciones cambiarias"
458
+ res = self.client.consultarSolicitudesCompraDivisas(
459
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
460
+ cuitComprador=cuit_comprador,
461
+ estadoSolicitud=estado_solicitud,
462
+ fechaEmisionDesde=fecha_emision_desde,
463
+ fechaEmisionHasta=fecha_emision_hasta,
464
+ )
465
+
466
+ self.__analizar_errores(res)
467
+
468
+ ret = res.get('consultarSolicitudesCompraDivisasReturn', {})
469
+ solicitudes = [] # cοΏ½digos a devolver
470
+ self.__detalles_solicitudes = [] # diccionario para recorrerlo luego
471
+ for array in ret.get('arrayDetallesSolicitudes', []):
472
+ det = array['detalleSolicitudes']
473
+ # guardo el detalle para procesarlo luego (LeerSolicitudConsultada)
474
+ self.__detalles_solicitudes.append(det)
475
+ # devuelvo solo el cοΏ½digo de solicitud
476
+ solicitudes.append(det.get("codigoSolicitud"))
477
+ return solicitudes
478
+
479
+ def LeerSolicitudConsultada(self):
480
+ "Proceso de a una solicitud los detalles devueltos por ConsultarSolicitudesCompraDivisas"
481
+ if self.__detalles_solicitudes:
482
+ # extraigo el primer item
483
+ det = self.__detalles_solicitudes.pop(0)
484
+ self.__analizar_solicitud(det)
485
+ self.__analizar_errores(det)
486
+ self.__analizar_inconsistencias(det)
487
+ return True
488
+ else:
489
+ return False
490
+
491
+ @inicializar_y_capturar_excepciones
492
+ def ConsultarDJAI(self, djai, cuit):
493
+ "Consultar DeclaraciοΏ½n Jurada Anticipada de ImportaciοΏ½n"
494
+ res = self.client.consultarDJAI(
495
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
496
+ djai=djai, cuit=cuit,
497
+ )
498
+
499
+ ret = res.get('consultarDJAIReturn', {})
500
+ self.__analizar_errores(ret)
501
+ self.DJAI = ret.get('djai')
502
+ self.MontoFOB = ret.get('montoFOB')
503
+ self.CodigoMoneda = ret.get('codigoMoneda')
504
+ self.EstadoDJAI = ret.get('estadoDJAI')
505
+ return True
506
+
507
+ @inicializar_y_capturar_excepciones
508
+ def ConsultarDJAS(self, djas, cuit):
509
+ "Consultar DeclaraciοΏ½n Jurada Anticipada de Servicios"
510
+ res = self.client.consultarDJAS(
511
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
512
+ djas=djas, cuit=cuit,
513
+ )
514
+
515
+ ret = res.get('consultarDJASReturn', {})
516
+ self.__analizar_errores(ret)
517
+ self.DJAS = ret.get('djas')
518
+ self.MontoFOB = ret.get('montoFOB')
519
+ self.CodigoMoneda = ret.get('codigoMoneda')
520
+ self.EstadoDJAS = ret.get('estadoDJAS')
521
+ return True
522
+
523
+ @inicializar_y_capturar_excepciones
524
+ def ConsultarReferencia(self, tipo, codigo):
525
+ "Consultar una determinada referencia segοΏ½n su tipo (1: DJAI, 2: DJAS, 3: DJAT)"
526
+ res = self.client.consultarReferencia(
527
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
528
+ referencia={'tipo': tipo, 'codigo': codigo},
529
+ )
530
+
531
+ ret = res.get('consultarReferenciaReturn', {})
532
+ self.__analizar_errores(ret)
533
+ #self.Codigo = ret.get('codigo')
534
+ self.MontoFOB = ret.get('monto')
535
+ self.CodigoMoneda = ret.get('codigoMoneda')
536
+ self.Estado = ret.get('estado')
537
+ return True
538
+
539
+ @inicializar_y_capturar_excepciones
540
+ def ConsultarMonedas(self, sep="|"):
541
+ "Este mοΏ½todo retorna el universo de Monedas disponibles en el presente WS, indicando cοΏ½digo y descripciοΏ½n de cada una"
542
+ res = self.client.consultarMonedas(
543
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
544
+ )
545
+ ret = res['consultarMonedasReturn']
546
+ return [("%(codigo)s\t%(descripcion)s"
547
+ % p['codigoDescripcion']).replace("\t", sep)
548
+ for p in ret['arrayMonedas']]
549
+
550
+ @inicializar_y_capturar_excepciones
551
+ def ConsultarDestinosCompra(self, sep="|"):
552
+ "Consultar Tipos de Destinos de compra de divisas"
553
+ res = self.client.consultarDestinosCompra(
554
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
555
+ )
556
+ # <consultarDestinosCompraReturn>22.02 m
557
+ # <arrayDestinos>
558
+ # <destinos>
559
+ # <tipoDestino>TipoDestinoSimpleType</tipoDestino>
560
+ # <arrayCodigosDescripciones>
561
+ # <codigoDescripcion>
562
+ # <codigo>short</codigo>
563
+ # <descripcion>string</descripcion>
564
+ # </codigoDescripcion>
565
+ # </arrayCodigosDescripciones>
566
+ # </destinos>
567
+ # </arrayDestinos>
568
+ ret = res['consultarDestinosCompraReturn']
569
+ dest = []
570
+ for array in ret['arrayDestinos']:
571
+ destino = array['destinos']
572
+ codigos = [("%s\t%s\t%s"
573
+ % (destino['tipoDestino'],
574
+ p['codigoDescripcion']['codigo'],
575
+ p['codigoDescripcion']['descripcion'],
576
+ )).replace("\t", sep)
577
+ for p in destino['arrayCodigosDescripciones']]
578
+ dest.extend(codigos)
579
+ return dest
580
+
581
+ @inicializar_y_capturar_excepciones
582
+ def ConsultarTiposDocumento(self, sep="|"):
583
+ "Consultar Tipos de Documentos"
584
+ res = self.client.consultarTiposDocumento(
585
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
586
+ )
587
+ ret = res['consultarTiposDocumentoReturn']
588
+ return [("%(codigo)s\t%(descripcion)s"
589
+ % p['codigoDescripcion']).replace("\t", sep)
590
+ for p in ret['arrayTiposDocumento']]
591
+
592
+ @inicializar_y_capturar_excepciones
593
+ def ConsultarTiposEstadoSolicitud(self, sep="|"):
594
+ "Este mοΏ½todo devuelve los diferentes tipos de estado que puede tener una solicitud."
595
+ res = self.client.consultarTiposEstadoSolicitud(
596
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
597
+ )
598
+ ret = res['consultarTiposEstadoSolicitudReturn']
599
+ return [("%(codigo)s\t%(descripcion)s"
600
+ % p['codigoDescripcionString']).replace("\t", sep)
601
+ for p in ret['arrayTiposEstadoSolicitud']]
602
+
603
+ @inicializar_y_capturar_excepciones
604
+ def ConsultarMotivosExcepcionDJAI(self, sep='|'):
605
+ "Este mοΏ½todo retorna el universo de motivos de excepciones a la DeclaraciοΏ½n Jurada Anticipada de ImportaciοΏ½n"
606
+ res = self.client.consultarMotivosExcepcionDJAI(
607
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
608
+ )
609
+ ret = res['consultarMotivosExcepcionDJAIReturn']
610
+ return [("%(codigo)s\t%(descripcion)s"
611
+ % p['codigoDescripcion']).replace("\t", sep)
612
+ for p in ret['arrayMotivosExcepcion']]
613
+
614
+ @inicializar_y_capturar_excepciones
615
+ def ConsultarDestinosCompraDJAI(self, sep='|'):
616
+ "Este mοΏ½todo retorna el subconjunto de los destinos de compra de divisas alcanzados por las normativas de la DeclaraciοΏ½n Jurada Anticipada de ImportaciοΏ½n."
617
+ res = self.client.consultarDestinosCompraDJAI(
618
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
619
+ )
620
+ ret = res['consultarDestinosCompraDJAIReturn']
621
+ return [("%(codigo)s\t%(descripcion)s"
622
+ % p['codigoDescripcion']).replace("\t", sep)
623
+ for p in ret['arrayCodigosDescripciones']]
624
+
625
+ @inicializar_y_capturar_excepciones
626
+ def ConsultarMotivosExcepcionDJAS(self, sep='|'):
627
+ "Este mοΏ½todo retorna el universo de motivos de excepciones a la DeclaraciοΏ½n Jurada Anticipada de Servicios"
628
+ res = self.client.consultarMotivosExcepcionDJAS(
629
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
630
+ )
631
+ ret = res['consultarMotivosExcepcionDJASReturn']
632
+ return [("%(codigo)s\t%(descripcion)s"
633
+ % p['codigoDescripcion']).replace("\t", sep)
634
+ for p in ret['arrayMotivosExcepcion']]
635
+
636
+ @inicializar_y_capturar_excepciones
637
+ def ConsultarDestinosCompraDJAS(self, sep='|'):
638
+ "Este mοΏ½todo retorna el subconjunto de los destinos de compra de divisas alcanzados por las normativas de la DeclaraciοΏ½n Jurada Anticipada de Servicios"
639
+ res = self.client.consultarDestinosCompraDJAS(
640
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
641
+ )
642
+ ret = res['consultarDestinosCompraDJASReturn']
643
+ return [("%(codigo)s\t%(descripcion)s"
644
+ % p['codigoDescripcion']).replace("\t", sep)
645
+ for p in ret['arrayCodigosDescripciones']]
646
+
647
+ @inicializar_y_capturar_excepciones
648
+ def ConsultarTiposReferencia(self, sep='|'):
649
+ "Este mοΏ½todo retorna el conjunto de los tipos de referencia que pueden ser utilizados en la generaciοΏ½n de una solicitud de compra de divisas segοΏ½n corresponda."
650
+ res = self.client.consultarTiposReferencia(
651
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
652
+ )
653
+ ret = res['consultarTiposReferenciaReturn']
654
+ return [("%(codigo)s\t%(descripcion)s"
655
+ % p['codigoDescripcion']).replace("\t", sep)
656
+ for p in ret['arrayCodigosDescripciones']]
657
+
658
+ @inicializar_y_capturar_excepciones
659
+ def ConsultarDestinosCompraTipoReferencia(self, tipo, sep='|'):
660
+ "Este mοΏ½todo retorna el subconjunto de los destinos de compra de divisas alcanzados por algunas de las normativas vigentes segοΏ½n el tipo de referencia requerido"
661
+ res = self.client.consultarDestinosCompraTipoReferencia(
662
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
663
+ tipo=tipo,
664
+ )
665
+ ret = res['consultarDestinosCompraTipoReferenciaReturn']
666
+ return [("%(codigo)s\t%(descripcion)s"
667
+ % p['codigoDescripcion']).replace("\t", sep)
668
+ for p in ret['arrayCodigosDescripciones']]
669
+
670
+ def LeerError(self):
671
+ "Recorro los errores devueltos y devuelvo el primero si existe"
672
+
673
+ if self.Errores:
674
+ # extraigo el primer item
675
+ er = self.Errores.pop(0)
676
+ return er
677
+ else:
678
+ return ""
679
+
680
+ def LeerErrorFormato(self):
681
+ "Recorro los errores de formatos devueltos y devuelvo el primero si existe"
682
+
683
+ if self.ErroresFormato:
684
+ # extraigo el primer item
685
+ er = self.ErroresFormato.pop(0)
686
+ return er
687
+ else:
688
+ return ""
689
+
690
+ def LeerInconsistencia(self):
691
+ "Recorro las inconsistencias devueltas y devuelvo la primera si existe"
692
+
693
+ if self.Inconsistencias:
694
+ # extraigo el primer item
695
+ er = self.Inconsistencias.pop(0)
696
+ return er
697
+ else:
698
+ return ""
699
+
700
+ def LoadTestXML(self, xml_file):
701
+ class DummyHTTP:
702
+ def __init__(self, xml_response):
703
+ self.xml_response = xml_response
704
+
705
+ def request(self, location, method, body, headers):
706
+ return {}, self.xml_response
707
+ self.client.http = DummyHTTP(open(xml_file).read())
708
+
709
+ def AnalizarXml(self, xml=""):
710
+ "Analiza un mensaje XML (por defecto la respuesta)"
711
+ try:
712
+ if not xml or xml == 'XmlResponse':
713
+ xml = self.XmlResponse
714
+ elif xml == 'XmlRequest':
715
+ xml = self.XmlRequest
716
+ self.xml = SimpleXMLElement(xml)
717
+ return True
718
+ except Exception as e:
719
+ self.Excepcion = "%s" % (e)
720
+ return False
721
+
722
+ def ObtenerTagXml(self, *tags):
723
+ "Busca en el Xml analizado y devuelve el tag solicitado"
724
+ # convierto el xml a un objeto
725
+ try:
726
+ if self.xml:
727
+ xml = self.xml
728
+ # por cada tag, lo busco segun su nombre o posiciοΏ½n
729
+ for tag in tags:
730
+ xml = xml(tag) # atajo a getitem y getattr
731
+ # vuelvo a convertir a string el objeto xml encontrado
732
+ return str(xml)
733
+ except Exception as e:
734
+ self.Excepcion = "%s" % (e)
735
+
736
+
737
+ def p_assert_eq(a, b):
738
+ print(a, a == b and '==' or '!=', b)
739
+
740
+
741
+ def main():
742
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
743
+ import os
744
+ import time
745
+
746
+ DEBUG = '--debug' in sys.argv
747
+
748
+ # obteniendo el TA
749
+ TA = "TA-wscoc.xml"
750
+ if not os.path.exists(TA) or os.path.getmtime(TA) + (60 * 60 * 5) < time.time():
751
+ from . import wsaa
752
+ tra = wsaa.create_tra(service="wscoc")
753
+ cms = wsaa.sign_tra(tra, "olano.crt", "olanoycia.key")
754
+ ta_string = wsaa.call_wsaa(cms, trace='--trace' in sys.argv)
755
+ open(TA, "w").write(ta_string)
756
+
757
+ # fin TA
758
+
759
+ ws = WSCOC()
760
+ ws.Cuit = "20267565393"
761
+
762
+ ta_string = open(TA).read()
763
+ ta = SimpleXMLElement(ta_string)
764
+ ws.Token = str(ta.credentials.token)
765
+ ws.Sign = str(ta.credentials.sign)
766
+
767
+ ws.LanzarExcepciones = True
768
+ ws.Conectar(wsdl=WSDL)
769
+
770
+ if "--dummy" in sys.argv:
771
+ # print ws.client.help("dummy")
772
+ try:
773
+ ws.Dummy()
774
+ print("AppServerStatus", ws.AppServerStatus)
775
+ print("DbServerStatus", ws.DbServerStatus)
776
+ print("AuthServerStatus", ws.AuthServerStatus)
777
+ except Exception as e:
778
+ raise
779
+ print("Exception", e)
780
+ print(ws.XmlRequest)
781
+ print(ws.XmlResponse)
782
+
783
+ if "--monedas" in sys.argv:
784
+ print(ws.client.help("consultarMonedas"))
785
+ try:
786
+ for moneda in ws.ConsultarMonedas():
787
+ print(moneda)
788
+ except Exception as e:
789
+ raise
790
+ print("Exception", e)
791
+ print(ws.XmlRequest)
792
+ print(ws.XmlResponse)
793
+
794
+ if "--motivos_ex_djai" in sys.argv:
795
+ print(ws.ConsultarMotivosExcepcionDJAI())
796
+ if "--destinos_djai" in sys.argv:
797
+ print(ws.ConsultarDestinosCompraDJAI())
798
+
799
+ if "--consultar_cuit" in sys.argv:
800
+ print(ws.client.help("consultarCUIT"))
801
+ try:
802
+ print("Consultado CUITs....")
803
+ nro_doc = 26756539
804
+ tipo_doc = 96
805
+ print(ws.ConsultarCUIT(nro_doc, tipo_doc))
806
+ # recorro el detalle de los cuit devueltos:
807
+ while ws.LeerCUITConsultado():
808
+ print("CUIT", ws.CUITConsultada)
809
+ print("DenominaciοΏ½n", ws.DenominacionConsultada)
810
+ except Exception as e:
811
+ raise
812
+ print("Exception", e)
813
+ print(ws.XmlRequest)
814
+ print(ws.XmlResponse)
815
+
816
+ if "--prueba" in sys.argv:
817
+ print(ws.client.help("generarSolicitudCompraDivisa").encode("latin1"))
818
+ try:
819
+ cuit_comprador = 20267565393
820
+ codigo_moneda = 1
821
+ cotizacion_moneda = 4.26
822
+ monto_pesos = 100
823
+ cuit_representante = None
824
+ codigo_destino = 625
825
+
826
+ if "--loadxml" in sys.argv:
827
+ ws.LoadTestXML("wscoc_response.xml")
828
+
829
+ if not "--tur" in sys.argv:
830
+ djai = "--djai" in sys.argv and "12345DJAI000001N" or None
831
+ djas = "--djas" in sys.argv and "12001DJAS000901N" or None
832
+ cod_ex_djai = "--no-djai" and 3 or None
833
+ cod_ex_djas = "--no-djas" and 1 or None
834
+ if '--ref' in sys.argv:
835
+ tipo, codigo = 1, '12345DJAI000067C'
836
+ else:
837
+ tipo, codigo = None, None
838
+ print("djai", djai)
839
+ ok = ws.GenerarSolicitudCompraDivisa(cuit_comprador, codigo_moneda,
840
+ cotizacion_moneda, monto_pesos,
841
+ cuit_representante, codigo_destino,
842
+ djai=djai, codigo_excepcion_djai=cod_ex_djai,
843
+ djas=djas, codigo_excepcion_djas=cod_ex_djas,
844
+ tipo=tipo, codigo=codigo,)
845
+ else:
846
+ print("Turista!")
847
+ tipo_doc = 91
848
+ numero_doc = 1234567
849
+ apellido_nombre = "Nombre y Apellido del turista extranjero"
850
+ codigo_destino = 985
851
+ ok = ws.GenerarSolicitudCompraDivisaTurExt(
852
+ tipo_doc, numero_doc, apellido_nombre,
853
+ codigo_moneda, cotizacion_moneda, monto_pesos,
854
+ cuit_representante, codigo_destino,
855
+ )
856
+ while True:
857
+ i = ws.LeerInconsistencia()
858
+ if not i:
859
+ break
860
+ print("Inconsistencia...", i)
861
+
862
+ assert ok
863
+ print('Resultado', ws.Resultado)
864
+ assert ws.Resultado == 'A'
865
+ print('COC', ws.COC)
866
+ assert len(str(ws.COC)) == 12
867
+ print("FechaEmisionCOC", ws.FechaEmisionCOC)
868
+ print('CodigoSolicitud', ws.CodigoSolicitud)
869
+ assert ws.CodigoSolicitud is not None
870
+ print("EstadoSolicitud", ws.EstadoSolicitud)
871
+ assert ws.EstadoSolicitud == 'OT'
872
+ print("FechaEstado", ws.FechaEstado)
873
+ print("DetalleCUITComprador", ws.CUITComprador, ws.DenominacionComprador)
874
+ print("CodigoMoneda", ws.CodigoMoneda)
875
+ assert ws.CodigoMoneda == 1
876
+ print("CotizacionMoneda", ws.CotizacionMoneda)
877
+ assert round(ws.CotizacionMoneda, 2) == 4.26
878
+ print("MontoPesos", ws.MontoPesos)
879
+ assert ws.MontoPesos - monto_pesos <= 0.01
880
+ print("CodigoDestino", ws.CodigoDestino)
881
+ assert ws.CodigoDestino == codigo_destino
882
+
883
+ coc = ws.COC
884
+ codigo_solicitud = ws.CodigoSolicitud
885
+ # CO: confirmar, o 'DC' (desistio cliente) 'DB' (desistio banco)
886
+ nuevo_estado = 'CO'
887
+ ok = ws.InformarSolicitudCompraDivisa(codigo_solicitud, nuevo_estado)
888
+ assert ok
889
+ print('Resultado', ws.Resultado)
890
+ assert ws.Resultado == 'A'
891
+ print('COC', ws.COC)
892
+ assert ws.COC == coc
893
+ print("EstadoSolicitud", ws.EstadoSolicitud)
894
+ assert ws.EstadoSolicitud == nuevo_estado
895
+
896
+ ok = ws.AnularCOC(coc, cuit_comprador)
897
+ assert ok
898
+ print('Resultado', ws.Resultado)
899
+ assert ws.Resultado == 'A'
900
+ print('COC', ws.COC)
901
+ assert ws.COC == coc
902
+ print("EstadoSolicitud", ws.EstadoSolicitud)
903
+ assert ws.EstadoSolicitud == 'AN'
904
+
905
+ ok = ws.ConsultarSolicitudCompraDivisa(codigo_solicitud)
906
+ assert ok
907
+ print('CodigoSolicitud', ws.CodigoSolicitud)
908
+ assert ws.CodigoSolicitud == codigo_solicitud
909
+ print("EstadoSolicitud", ws.EstadoSolicitud)
910
+ assert ws.EstadoSolicitud == 'AN'
911
+
912
+ except BaseException:
913
+ print(ws.XmlRequest)
914
+ print(ws.XmlResponse)
915
+ print(ws.ErrCode)
916
+ print(ws.ErrMsg)
917
+ raise
918
+
919
+ if "--consultar_solicitudes" in sys.argv:
920
+ cuit_comprador = None
921
+ estado_solicitud = None
922
+ fecha_emision_desde = '2011-11-01'
923
+ fecha_emision_hasta = '2011-11-30'
924
+ sols = ws.ConsultarSolicitudesCompraDivisas(cuit_comprador,
925
+ estado_solicitud,
926
+ fecha_emision_desde,
927
+ fecha_emision_hasta,)
928
+ # muestro los resultados de la bοΏ½squeda
929
+ print("Solicitudes consultadas:")
930
+ for sol in sols:
931
+ print("CοΏ½digo de Solicitud:", sol)
932
+ # podrοΏ½a llamar a ws.ConsultarSolicitudCompraDivisa
933
+ print("hecho.")
934
+
935
+ ws.AnalizarXml("XmlResponse")
936
+
937
+ # recorro las solicitudes devueltas
938
+ i = 0
939
+ while ws.LeerSolicitudConsultada():
940
+ print("-" * 80)
941
+ coc = ws.ObtenerTagXml('arrayDetallesSolicitudes', 'detalleSolicitudes', i, 'coc')
942
+ cuit = ws.ObtenerTagXml('arrayDetallesSolicitudes', 'detalleSolicitudes', i, 'cuit')
943
+ p_assert_eq(coc, ws.COC)
944
+ print('CUIT', cuit)
945
+ print("FechaEmisionCOC", ws.FechaEmisionCOC)
946
+ print('CodigoSolicitud', ws.CodigoSolicitud)
947
+ print("EstadoSolicitud", ws.EstadoSolicitud)
948
+ print("FechaEstado", ws.FechaEstado)
949
+ print("DetalleCUITComprador", ws.CUITComprador, ws.DenominacionComprador)
950
+ print("CodigoMoneda", ws.CodigoMoneda)
951
+ print("CotizacionMoneda", ws.CotizacionMoneda)
952
+ print("MontoPesos", ws.MontoPesos)
953
+ print("CodigoDestino", ws.CodigoDestino)
954
+ print("=" * 80)
955
+ i = i + 1
956
+
957
+ if "--parametros" in sys.argv:
958
+ print("=== Tipos de Estado Solicitud ===")
959
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarTiposEstadoSolicitud(sep="||")]))
960
+ print("=== Monedas ===")
961
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarMonedas(sep="||")]))
962
+ print("=== Destinos de Compra ===")
963
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarDestinosCompra(sep="||")]))
964
+ print("=== Tipos de Documento ===")
965
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarTiposDocumento(sep="||")]))
966
+ print("=== Tipos Estado Solicitud ===")
967
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarTiposEstadoSolicitud(sep="||")]))
968
+ print("=== Motivos Excepcion DJAI ===")
969
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarMotivosExcepcionDJAI(sep="||")]))
970
+ print("=== Destinos Compra DJAI ===")
971
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarDestinosCompraDJAI(sep="||")]))
972
+ print("=== Motivos Excepcion DJAS ===")
973
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarMotivosExcepcionDJAS(sep="||")]))
974
+ print("=== Destinos Compra DJAS ===")
975
+ print('\n'.join(["||%s||" % s for s in ws.ConsultarDestinosCompraDJAS(sep="||")]))
976
+
977
+ if "--consultar_djai" in sys.argv:
978
+ djai = "12345DJAI000001N"
979
+ cuit = 20267565393
980
+ ws.ConsultarDJAI(djai, cuit)
981
+ print("DJAI", ws.DJAI)
982
+ print("Monto FOB", ws.MontoFOB)
983
+ print("Codigo Moneda", ws.CodigoMoneda)
984
+ print("Estado DJAI", ws.EstadoDJAI)
985
+ print("Errores", ws.Errores)
986
+ print("ErroresFormato", ws.ErroresFormato)
987
+
988
+ if "--consultar_djas" in sys.argv:
989
+ djas = "12001DJAS000901N"
990
+ cuit = 20267565393
991
+ ws.ConsultarDJAS(djas, cuit)
992
+ print("DJAS", ws.DJAS)
993
+ print("Monto FOB", ws.MontoFOB)
994
+ print("Codigo Moneda", ws.CodigoMoneda)
995
+ print("Estado DJAI", ws.EstadoDJAI)
996
+ print("Errores", ws.Errores)
997
+ print("ErroresFormato", ws.ErroresFormato)
998
+
999
+ if "--consultar_ref" in sys.argv:
1000
+ codigo = "12345DJAI000067C"
1001
+ cuit = 20267565393
1002
+ ws.ConsultarReferencia(1, codigo)
1003
+ print("Monto FOB", ws.MontoFOB)
1004
+ print("Codigo Moneda", ws.CodigoMoneda)
1005
+ print("Estado", ws.Estado)
1006
+ print("Errores", ws.Errores)
1007
+ print("ErroresFormato", ws.ErroresFormato)
1008
+
1009
+ if "--consultar_dest_ref" in sys.argv:
1010
+ print(ws.ConsultarDestinosCompraTipoReferencia(1))
1011
+
1012
+ if "--consultar_tipos_ref" in sys.argv:
1013
+ print(ws.ConsultarTiposReferencia())
1014
+
1015
+
1016
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
1017
+ if not hasattr(sys, "frozen"):
1018
+ basepath = __file__
1019
+ elif sys.frozen == 'dll':
1020
+ import win32api
1021
+ basepath = win32api.GetModuleFileName(sys.frozendllhandle)
1022
+ else:
1023
+ basepath = sys.executable
1024
+ INSTALL_DIR = os.path.dirname(os.path.abspath(basepath))
1025
+
1026
+ if __name__ == '__main__':
1027
+
1028
+ if "--register" in sys.argv or "--unregister" in sys.argv:
1029
+ import win32com.server.register
1030
+ win32com.server.register.UseCommandLine(WSCOC)
1031
+ else:
1032
+ main()
app/pyafipws/wsct.py ADDED
@@ -0,0 +1,820 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para obtener cοΏ½digo de autorizaciοΏ½n electrοΏ½nico CAE webservice
14
+ WSCT de AFIP (Factura ElectrοΏ½nica Comprobantes de Turismo)
15
+ ResoluciοΏ½n Conjunta General 3971 y ResoluciοΏ½n 566/2016.
16
+ """
17
+
18
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
19
+ __copyright__ = "Copyright (C) 2017 Mariano Reingart"
20
+ __license__ = "GPL 3.0"
21
+ __version__ = "1.02c"
22
+
23
+ import datetime
24
+ import decimal
25
+ import os
26
+ import sys
27
+ from .utils import verifica, inicializar_y_capturar_excepciones, BaseWS, get_install_dir
28
+
29
+ HOMO = False
30
+ LANZAR_EXCEPCIONES = True
31
+ WSDL = "https://fwshomo.afip.gov.ar/wsct/CTService?wsdl"
32
+
33
+
34
+ class WSCT(BaseWS):
35
+ "Interfaz para el WebService de Factura ElectrοΏ½nica Comprobantes Turismo"
36
+ _public_methods_ = ['CrearFactura', 'EstablecerCampoFactura', 'AgregarIva', 'AgregarItem',
37
+ 'AgregarTributo', 'AgregarCmpAsoc', 'EstablecerCampoItem',
38
+ 'AgregarDatoAdicional', 'AgregarFormaPago',
39
+ 'AutorizarComprobante', 'CAESolicitar',
40
+ 'InformarCAEANoUtilizado', 'InformarCAEANoUtilizadoPtoVta',
41
+ 'ConsultarUltimoComprobanteAutorizado', 'CompUltimoAutorizado',
42
+ 'ConsultarPtosVtaCAEANoInformados',
43
+ 'ConsultarComprobante',
44
+ 'ConsultarTiposComprobante', 'ConsultarTiposDocumento',
45
+ 'consultarTiposIVA', 'ConsultarCondicionesIVA',
46
+ 'ConsultarMonedas', 'ConsultarCotizacion',
47
+ 'ConsultarTiposItem', 'ConsultarCodigosItemTurismo',
48
+ 'ConsultarTiposTributo',
49
+ 'ConsultarCUITsPaises', 'ConsultarPaises',
50
+ 'ConsultarTiposDatosAdicionales', 'ConsultarFomasPago',
51
+ 'ConsultarTiposTarjeta', 'ConsultarTiposCuenta',
52
+ 'ConsultarPuntosVenta',
53
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
54
+ 'SetParametros', 'SetTicketAcceso', 'GetParametro',
55
+ 'Dummy', 'Conectar', 'DebugLog', 'SetTicketAcceso']
56
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
57
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
58
+ 'XmlRequest', 'XmlResponse', 'Version', 'InstallDir', 'LanzarExcepciones',
59
+ 'Resultado', 'Obs', 'Observaciones', 'ErrCode', 'ErrMsg',
60
+ 'EmisionTipo', 'Reproceso', 'Reprocesar', 'Evento',
61
+ 'CAE', 'Vencimiento', 'Evento', 'Errores', 'Traceback', 'Excepcion',
62
+ 'CAEA', 'Periodo', 'Orden', 'FchVigDesde', 'FchVigHasta', 'FchTopeInf', 'FchProceso',
63
+ 'CbteNro', 'FechaCbte', 'PuntoVenta', 'ImpTotal']
64
+
65
+ _reg_progid_ = "WSCT"
66
+ _reg_clsid_ = "{5DE7917D-CE97-4C88-B6C7-DAF8CEB54E93}"
67
+
68
+ # Variables globales para BaseWS:
69
+ HOMO = HOMO
70
+ WSDL = WSDL
71
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
72
+ Reprocesar = True # recuperar automaticamente CAE emitidos
73
+ LanzarExcepciones = LANZAR_EXCEPCIONES
74
+ factura = None
75
+
76
+ def inicializar(self):
77
+ BaseWS.inicializar(self)
78
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
79
+ self.Resultado = self.Motivo = self.Reproceso = ''
80
+ self.LastID = self.LastCMP = self.CAE = self.Vencimiento = ''
81
+ self.CAEA = None
82
+ self.Periodo = self.Orden = ""
83
+ self.FchVigDesde = self.FchVigHasta = ""
84
+ self.FchTopeInf = self.FchProceso = ""
85
+ self.CbteNro = self.FechaCbte = self.ImpTotal = None
86
+ self.PuntoVenta = self.EmisionTipo = self.Evento = ''
87
+ self.Reproceso = '' # no implementado
88
+
89
+ def __analizar_errores(self, ret):
90
+ "Comprueba y extrae errores si existen en la respuesta XML"
91
+ for key in ('arrayErrores', 'arrayErroresFormato'):
92
+ errores = ret.get(key, [])
93
+ for error in errores:
94
+ for k in ('codigoDescripcion', 'codigoDescripcionString'):
95
+ err = error.get(k)
96
+ if err:
97
+ break
98
+ self.Errores.append("%s: %s" % (
99
+ err['codigo'],
100
+ err['descripcion'],
101
+ ))
102
+ self.ErrMsg = '\n'.join(self.Errores)
103
+
104
+ def Dummy(self):
105
+ "Obtener el estado de los servidores de la AFIP"
106
+ ret = self.client.dummy()
107
+ result = ret['dummyReturn']
108
+ self.AppServerStatus = result['appserver']
109
+ self.DbServerStatus = result['dbserver']
110
+ self.AuthServerStatus = result['authserver']
111
+ return True
112
+
113
+ def CrearFactura(self, tipo_doc=None, nro_doc=None, tipo_cbte=None, punto_vta=None,
114
+ cbte_nro=None, imp_total=None, imp_tot_conc=None, imp_neto=None,
115
+ imp_subtotal=None, imp_trib=None, imp_op_ex=None, imp_reintegro=None,
116
+ fecha_cbte=None, id_impositivo="", cod_pais=None, domicilio="", cod_relacion="",
117
+ moneda_id=None, moneda_ctz=None, observaciones=None,
118
+ **kwargs
119
+ ):
120
+ "Creo un objeto factura (interna)"
121
+ # Creo una factura electronica de exportaciοΏ½n
122
+ fact = {'tipo_doc': tipo_doc, 'nro_doc': nro_doc,
123
+ 'tipo_cbte': tipo_cbte, 'punto_vta': punto_vta,
124
+ 'cbte_nro': cbte_nro,
125
+ 'id_impositivo': id_impositivo,
126
+ 'cod_pais': cod_pais,
127
+ 'domicilio': domicilio,
128
+ 'cod_relacion': cod_relacion,
129
+ 'imp_total': imp_total, 'imp_tot_conc': imp_tot_conc,
130
+ 'imp_neto': imp_neto,
131
+ 'imp_subtotal': imp_subtotal, # 'imp_iva': imp_iva,
132
+ 'imp_trib': imp_trib, 'imp_op_ex': imp_op_ex,
133
+ 'imp_reintegro': imp_reintegro,
134
+ 'fecha_cbte': fecha_cbte,
135
+ 'moneda_id': moneda_id, 'moneda_ctz': moneda_ctz,
136
+ 'observaciones': observaciones,
137
+ 'cbtes_asoc': [],
138
+ 'tributos': [],
139
+ 'iva': [],
140
+ 'detalles': [],
141
+ 'adicionales': [],
142
+ 'formas_pago': [],
143
+ }
144
+
145
+ self.factura = fact
146
+ return True
147
+
148
+ def EstablecerCampoFactura(self, campo, valor):
149
+ if campo in self.factura or campo in ('fecha_serv_desde', 'fecha_serv_hasta', 'caea', 'fch_venc_cae'):
150
+ self.factura[campo] = valor
151
+ return True
152
+ else:
153
+ return False
154
+
155
+ def AgregarCmpAsoc(self, tipo=1, pto_vta=0, nro=0, cuit=None, **kwargs):
156
+ "Agrego un comprobante asociado a una factura (interna)"
157
+ cmp_asoc = {
158
+ 'tipo': tipo,
159
+ 'pto_vta': pto_vta,
160
+ 'nro': nro}
161
+ if cuit is not None:
162
+ cmp_asoc['cuit'] = cuit
163
+ self.factura['cbtes_asoc'].append(cmp_asoc)
164
+ return True
165
+
166
+ def AgregarTributo(self, tributo_id, desc, base_imp, alic, importe, **kwargs):
167
+ "Agrego un tributo a una factura (interna)"
168
+ tributo = {
169
+ 'tributo_id': tributo_id,
170
+ 'desc': desc,
171
+ 'base_imp': base_imp,
172
+ 'importe': importe,
173
+ }
174
+ self.factura['tributos'].append(tributo)
175
+ return True
176
+
177
+ def AgregarIva(self, iva_id, base_imp, importe, **kwargs):
178
+ "Agrego un tributo a una factura (interna)"
179
+ iva = {
180
+ 'iva_id': iva_id,
181
+ 'importe': importe,
182
+ }
183
+ self.factura['iva'].append(iva)
184
+ return True
185
+
186
+ def AgregarItem(self, tipo=None, cod_tur=None,
187
+ codigo=None, ds=None,
188
+ iva_id=None, imp_iva=None, imp_subtotal=None, **kwargs):
189
+ "Agrego un item a una factura (interna)"
190
+ # ds = unicode(ds, "latin1") # convierto a latin1
191
+ # Nota: no se calcula neto, iva, etc (deben venir calculados!)
192
+ tipo = int(tipo)
193
+ if tipo == 99:
194
+ imp_subtotal = -abs(float(imp_subtotal))
195
+ imp_iva = -abs(float(imp_iva))
196
+ item = {
197
+ 'tipo': tipo,
198
+ 'cod_tur': cod_tur,
199
+ 'codigo': codigo,
200
+ 'ds': ds,
201
+ 'iva_id': iva_id,
202
+ 'imp_iva': imp_iva,
203
+ 'imp_subtotal': imp_subtotal,
204
+ }
205
+ self.factura['detalles'].append(item)
206
+ return True
207
+
208
+ def AgregarDatoAdicional(self, t, c1, c2, c3, c4, c5, c6, **kwarg):
209
+ "Agrego un tipo de dato adicional a una factura (interna)"
210
+ op = {'t': t,
211
+ 'c1': c1, 'c2': c2, 'c3': c3, 'c4': c4, 'c5': c5, 'c6': c6, }
212
+ self.factura['adicionales'].append(op)
213
+ return True
214
+
215
+ def AgregarFormaPago(self, codigo, tipo_tarjeta=None, numero_tarjeta=None,
216
+ swift_code=None, tipo_cuenta=None, numero_cuenta=None,
217
+ **kwarg):
218
+ "Agrego una forma de pago a una factura (interna)"
219
+ fp = {'codigo': codigo, 'tipo_tarjeta': tipo_tarjeta,
220
+ 'numero_tarjeta': numero_tarjeta, 'swift_code': swift_code,
221
+ 'tipo_cuenta': tipo_cuenta, 'numero_cuenta': numero_cuenta}
222
+ self.factura['formas_pago'].append(fp)
223
+ return True
224
+
225
+ def EstablecerCampoItem(self, campo, valor):
226
+ if self.factura['detalles'] and campo in self.factura['detalles'][-1]:
227
+ self.factura['detalles'][-1][campo] = valor
228
+ return True
229
+ else:
230
+ return False
231
+
232
+ @inicializar_y_capturar_excepciones
233
+ def AutorizarComprobante(self):
234
+ f = self.factura
235
+ # contruyo la estructura a convertir en XML:
236
+ fact = {
237
+ 'codigoTipoDocumento': f['tipo_doc'], 'numeroDocumento': f['nro_doc'],
238
+ 'codigoTipoComprobante': f['tipo_cbte'], 'numeroPuntoVenta': f['punto_vta'],
239
+ 'codigoTipoAutorizacion': 'E',
240
+ 'numeroComprobante': f['cbte_nro'],
241
+ 'importeTotal': f['imp_total'], 'importeNoGravado': f['imp_tot_conc'],
242
+ 'idImpositivo': f['id_impositivo'],
243
+ 'codigoPais': f['cod_pais'], 'domicilioReceptor': f['domicilio'],
244
+ 'codigoRelacionEmisorReceptor': f['cod_relacion'],
245
+ 'importeGravado': f['imp_neto'],
246
+ 'importeSubtotal': f['imp_subtotal'], # 'imp_iva': imp_iva,
247
+ 'importeOtrosTributos': f['tributos'] and f['imp_trib'] or None,
248
+ 'importeExento': f['imp_op_ex'], 'importeReintegro': f['imp_reintegro'],
249
+ 'fechaEmision': f['fecha_cbte'],
250
+ 'codigoMoneda': f['moneda_id'], 'cotizacionMoneda': f['moneda_ctz'],
251
+ 'observaciones': f['observaciones'],
252
+ 'fechaVencimientoPago': f.get('fecha_venc_pago'),
253
+ 'fechaServicioDesde': f.get('fecha_serv_desde'),
254
+ 'fechaServicioHasta': f.get('fecha_serv_hasta'),
255
+ 'arrayComprobantesAsociados': f['cbtes_asoc'] and [{'comprobanteAsociado': {
256
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
257
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
258
+ 'numeroComprobante': cbte_asoc['nro'],
259
+ }} for cbte_asoc in f['cbtes_asoc']] or None,
260
+ 'arrayOtrosTributos': f['tributos'] and [{'otroTributo': {
261
+ 'codigo': tributo['tributo_id'],
262
+ 'descripcion': tributo['desc'],
263
+ 'baseImponible': tributo['base_imp'],
264
+ 'importe': tributo['importe'],
265
+ }} for tributo in f['tributos']] or None,
266
+ 'arraySubtotalesIVA': f['iva'] and [{'subtotalIVA': {
267
+ 'codigo': iva['iva_id'],
268
+ 'importe': iva['importe'],
269
+ }} for iva in f['iva']] or None,
270
+ 'arrayItems': f['detalles'] and [{'item': {
271
+ 'tipo': it['tipo'],
272
+ 'codigoTurismo': it['cod_tur'],
273
+ 'codigo': it['codigo'],
274
+ 'descripcion': it['ds'],
275
+ 'codigoAlicuotaIVA': it['iva_id'],
276
+ 'importeIVA': it['imp_iva'] if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
277
+ 'importeItem': it['imp_subtotal'],
278
+ }} for it in f['detalles']] or None,
279
+ 'arrayDatosAdicionales': [
280
+ {'tipoDatoAdicional': ta} for ta in f['adicionales']] or None,
281
+ 'arrayFormasPago': [
282
+ {'formaPago': {
283
+ 'codigo': fp['codigo'],
284
+ 'tipoTarjeta': fp['tipo_tarjeta'],
285
+ 'numeroTarjeta': fp['numero_tarjeta'],
286
+ 'swiftCode': fp['swift_code'],
287
+ 'tipoCuenta': fp['tipo_cuenta'],
288
+ 'numeroCuenta': fp['numero_cuenta'],
289
+ }} for fp in f['formas_pago']] or None,
290
+ }
291
+
292
+ res = self.client.autorizarComprobante(
293
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
294
+ comprobanteRequest=fact,
295
+ )
296
+ ret = res.get('autorizarComprobanteReturn', {})
297
+
298
+ # Reprocesar en caso de error (recuperar CAE emitido anteriormente)
299
+ if self.Reprocesar and ('arrayErrores' in ret):
300
+ for error in ret['arrayErrores']:
301
+ err_code = error['codigoDescripcion']['codigo']
302
+ if ret['resultado'] == 'R' and err_code == 102:
303
+ # guardo los mensajes xml originales
304
+ xml_request = self.client.xml_request
305
+ xml_response = self.client.xml_response
306
+ cae = self.ConsultarComprobante(f['tipo_cbte'], f['punto_vta'], f['cbt_desde'], reproceso=True)
307
+ if cae and self.EmisionTipo == 'CAE':
308
+ self.Reproceso = 'S'
309
+ self.Resultado = 'A' # verificar O
310
+ return cae
311
+ self.Reproceso = 'N'
312
+ # reestablesco los mensajes xml originales
313
+ self.client.xml_request = xml_request
314
+ self.client.xml_response = xml_response
315
+
316
+ self.Resultado = ret.get('resultado', "") # u'A'
317
+ if self.Resultado in ("A", "O"):
318
+ cbteresp = ret['comprobanteResponse']
319
+ self.FechaCbte = cbteresp['fechaEmision'].strftime("%Y/%m/%d")
320
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
321
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
322
+ # self. = cbteresp['cuit'] # 20267565393L
323
+ # self. = cbteresp['codigoTipoComprobante']
324
+ self.Vencimiento = cbteresp['fechaVencimientoCAE'].strftime("%Y/%m/%d")
325
+ self.CAE = str(cbteresp['CAE']) # 60423794871430L
326
+ self.__analizar_errores(ret)
327
+
328
+ for error in ret.get('arrayObservaciones', []):
329
+ self.Observaciones.append("%(codigo)s: %(descripcion)s" % (
330
+ error['codigoDescripcion']))
331
+ self.Obs = '\n'.join(self.Observaciones)
332
+ self.EmisionTipo = 'CAE'
333
+
334
+ if 'evento' in ret:
335
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
336
+ return self.CAE
337
+
338
+ @inicializar_y_capturar_excepciones
339
+ def CAESolicitar(self):
340
+ try:
341
+ cae = self.AutorizarComprobante() or ''
342
+ self.Excepcion = "OK!"
343
+ except BaseException:
344
+ cae = "ERR"
345
+ finally:
346
+ return cae
347
+
348
+ @inicializar_y_capturar_excepciones
349
+ def ConsultarUltimoComprobanteAutorizado(self, tipo_cbte, punto_vta):
350
+ res = self.client.consultarUltimoComprobanteAutorizado(
351
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
352
+ codigoTipoComprobante=tipo_cbte,
353
+ numeroPuntoVenta=punto_vta,
354
+ )
355
+ ret = res.get('consultarUltimoComprobanteAutorizadoReturn', {})
356
+ nro = ret.get('numeroComprobante')
357
+ self.__analizar_errores(ret)
358
+ self.CbteNro = nro
359
+ return nro is not None and str(nro) or 0
360
+
361
+ CompUltimoAutorizado = ConsultarUltimoComprobanteAutorizado
362
+
363
+ @inicializar_y_capturar_excepciones
364
+ def ConsultarComprobante(self, tipo_cbte, punto_vta, cbte_nro, reproceso=False):
365
+ "Recuperar los datos completos de un comprobante ya autorizado"
366
+ res = self.client.consultarComprobanteTipoPVentaNro(
367
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
368
+ codigoTipoComprobante=tipo_cbte,
369
+ numeroPuntoVenta=punto_vta,
370
+ numeroComprobante=cbte_nro,
371
+ )
372
+ ret = res.get('consultarComprobanteReturn', {})
373
+ # diferencias si hay reproceso:
374
+ difs = []
375
+ # analizo el resultado:
376
+ if 'comprobante' in ret:
377
+ cbteresp = ret['comprobante']
378
+ if reproceso:
379
+ # verifico los campos registrados coincidan con los enviados:
380
+ f = self.factura
381
+ verificaciones = {
382
+ 'codigoTipoComprobante': f['tipo_cbte'],
383
+ 'numeroPuntoVenta': f['punto_vta'],
384
+ 'codigoTipoDocumento': f['tipo_doc'],
385
+ 'numeroDocumento': f['nro_doc'],
386
+ 'numeroComprobante': f['cbt_desde'],
387
+ 'numeroComprobante': f['cbt_hasta'],
388
+ 'fechaEmision': f['fecha_cbte'],
389
+ 'idImpositivo': f['id_impositivo'],
390
+ 'codigoPais': f['cod_pais'], 'domicilioReceptor': f['domicilio'],
391
+ 'codigoRelacionEmisorReceptor': f['cod_relacion'],
392
+ 'importeTotal': decimal.Decimal(str(f['imp_total'])),
393
+ 'importeNoGravado': decimal.Decimal(str(f['imp_tot_conc'])),
394
+ 'importeGravado': decimal.Decimal(str(f['imp_neto'])),
395
+ 'importeExento': decimal.Decimal(str(f['imp_op_ex'])),
396
+ 'importeOtrosTributos': f['tributos'] and decimal.Decimal(str(f['imp_trib'])) or None,
397
+ 'importeSubtotal': f['imp_subtotal'],
398
+ 'importeReintegro': f['imp_reintegro'],
399
+ 'codigoMoneda': f['moneda_id'],
400
+ 'cotizacionMoneda': str(decimal.Decimal(str(f['moneda_ctz']))),
401
+ 'arrayItems': [
402
+ {'item': {
403
+ 'tipo': it['tipo'],
404
+ 'codigoTurismo': it['cod_tur'],
405
+ 'codigo': it['codigo'],
406
+ 'descripcion': it['ds'],
407
+ 'codigoAlicuotaIVA': decimal.Decimal(str(it['iva_id'])),
408
+ 'importeIVA': decimal.Decimal(str(it['imp_iva'])) if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
409
+ 'importeItem': decimal.Decimal(str(it['imp_subtotal'])),
410
+ }}
411
+ for it in f['detalles']],
412
+ 'arrayComprobantesAsociados': [
413
+ {'comprobanteAsociado': {
414
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
415
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
416
+ 'numeroComprobante': cbte_asoc['nro']}}
417
+ for cbte_asoc in f['cbtes_asoc']],
418
+ 'arrayOtrosTributos': [
419
+ {'otroTributo': {
420
+ 'codigo': tributo['tributo_id'],
421
+ 'descripcion': tributo['desc'],
422
+ 'baseImponible': decimal.Decimal(str(tributo['base_imp'])),
423
+ 'importe': decimal.Decimal(str(tributo['importe'])),
424
+ }}
425
+ for tributo in f['tributos']],
426
+ 'arraySubtotalesIVA': [
427
+ {'subtotalIVA': {
428
+ 'codigo': iva['iva_id'],
429
+ 'importe': decimal.Decimal(str(iva['importe'])),
430
+ }}
431
+ for iva in f['iva']],
432
+ }
433
+ verifica(verificaciones, cbteresp, difs)
434
+ if difs:
435
+ print("Diferencias:", difs)
436
+ self.log("Diferencias: %s" % difs)
437
+ self.FechaCbte = cbteresp['fechaEmision'].strftime("%Y/%m/%d")
438
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
439
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
440
+ self.Vencimiento = cbteresp['fechaVencimiento'].strftime("%Y/%m/%d")
441
+ self.ImpTotal = str(cbteresp['importeTotal'])
442
+ self.CAE = str(cbteresp['codigoAutorizacion']) # 60423794871430L
443
+ self.EmisionTipo = cbteresp['codigoTipoAutorizacion'] == 'A' and 'CAEA' or 'CAE'
444
+ self.__analizar_errores(ret)
445
+ if not difs:
446
+ return self.CAE
447
+
448
+ @inicializar_y_capturar_excepciones
449
+ def ConsultarTiposComprobante(self):
450
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
451
+ res = self.client.consultarTiposComprobantes(
452
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
453
+ )
454
+ ret = res['consultarTiposComprobantesReturn']
455
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
456
+ for p in ret['arrayTiposComprobantes']]
457
+
458
+ @inicializar_y_capturar_excepciones
459
+ def ConsultarTiposDocumento(self):
460
+ res = self.client.consultarTiposDocumento(
461
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
462
+ )
463
+ ret = res['consultarTiposDocumentoReturn']
464
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
465
+ for p in ret['arrayTiposDocumento']]
466
+
467
+ @inicializar_y_capturar_excepciones
468
+ def ConsultarTiposIVA(self):
469
+ "Este mοΏ½todo permite consultar los tipos de IVA habilitados en este ws"
470
+ res = self.client.consultarTiposIVA(
471
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
472
+ )
473
+ ret = res['consultarTiposIVAReturn']
474
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcionString']
475
+ for p in ret['arrayTiposIVA']]
476
+
477
+ @inicializar_y_capturar_excepciones
478
+ def ConsultarCondicionesIVA(self):
479
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
480
+ res = self.client.consultarCondicionesIVA(
481
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
482
+ )
483
+ ret = res['consultarCondicionesIVAReturn']
484
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcionString']
485
+ for p in ret['arrayCondicionesIVA']]
486
+
487
+ @inicializar_y_capturar_excepciones
488
+ def ConsultarMonedas(self):
489
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
490
+ res = self.client.consultarMonedas(
491
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
492
+ )
493
+ ret = res['consultarMonedasReturn']
494
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcionString']
495
+ for p in ret['arrayTiposMoneda']]
496
+
497
+ @inicializar_y_capturar_excepciones
498
+ def ConsultarTiposItem(self):
499
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
500
+ res = self.client.consultarTiposItem(
501
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
502
+ )
503
+ ret = res['consultarTiposItemReturn']
504
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
505
+ for p in ret['arrayTiposItem']]
506
+
507
+ @inicializar_y_capturar_excepciones
508
+ def ConsultarCodigosItemTurismo(self):
509
+ "Este mοΏ½todo permite consultar los cοΏ½digos de los οΏ½tems de Turismo"
510
+ res = self.client.consultarCodigosItemTurismo(
511
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
512
+ )
513
+ ret = res['consultarCodigosItemTurismoReturn']
514
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
515
+ for p in ret['arrayCodigosItem']]
516
+
517
+ @inicializar_y_capturar_excepciones
518
+ def ConsultarTiposTributo(self):
519
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
520
+ res = self.client.consultarTiposTributo(
521
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
522
+ )
523
+ ret = res['consultarTiposTributoReturn']
524
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcionString']
525
+ for p in ret['arrayTiposTributo']]
526
+
527
+ @inicializar_y_capturar_excepciones
528
+ def ConsultarCotizacion(self, moneda_id):
529
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
530
+ ret = self.client.consultarCotizacion(
531
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
532
+ codigoMoneda=moneda_id,
533
+ )
534
+ self.__analizar_errores(ret)
535
+ if 'cotizacionMoneda' in ret:
536
+ return str(ret['cotizacionMoneda'])
537
+
538
+ @inicializar_y_capturar_excepciones
539
+ def ConsultarPuntosVenta(self, fmt="%(numeroPuntoVenta)s: bloqueado=%(bloqueado)s baja=%(fechaBaja)s"):
540
+ "Este mοΏ½todo permite consultar los puntos de venta habilitados para CAE en este WS"
541
+ res = self.client.consultarPuntosVenta(
542
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
543
+ )
544
+ ret = []
545
+ self.__analizar_errores(ret)
546
+ for p in res['consultarPuntosVentaReturn'].get("arrayPuntosVenta", {}):
547
+ p = p['puntoVenta']
548
+ if 'fechaBaja' not in p:
549
+ p['fechaBaja'] = ""
550
+ ret.append(fmt % p if fmt else p)
551
+ return ret
552
+
553
+ @inicializar_y_capturar_excepciones
554
+ def ConsultarPaises(self, sep="|"):
555
+ "Recuperador de valores referenciales de cοΏ½digos de PaοΏ½ses"
556
+ ret = self.client.consultarPaises(
557
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit, })
558
+ result = ret['consultarPaisesReturn']
559
+ self.__analizar_errores(result)
560
+
561
+ ret = []
562
+ for u in result['arrayPaises']:
563
+ u = u['codigoDescripcionString']
564
+ try:
565
+ r = {'codigo': u.get('codigo'), 'ds': u.get('descripcion'), }
566
+ except Exception as e:
567
+ print(e)
568
+
569
+ ret.append(r)
570
+ if sep:
571
+ return [("\t%(codigo)s\t%(ds)s\t"
572
+ % it).replace("\t", sep) for it in ret]
573
+ else:
574
+ return ret
575
+
576
+ @inicializar_y_capturar_excepciones
577
+ def ConsultarCUITsPaises(self, sep="|"):
578
+ "Recuperar lista de valores referenciales de CUIT de PaοΏ½ses"
579
+ ret = self.client.consultarCUITsPaises(
580
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit, })
581
+ result = ret['consultarCUITsPaisesReturn']
582
+ self.__analizar_errores(result)
583
+
584
+ ret = []
585
+ for u in result['arrayCuitPaises']:
586
+ u = u['codigoDescripcionString']
587
+ try:
588
+ r = {'codigo': u.get('codigo'), 'ds': u.get('descripcion'), }
589
+ except Exception as e:
590
+ print(e)
591
+
592
+ ret.append(r)
593
+ if sep:
594
+ return [("\t%(codigo)s\t%(ds)s\t"
595
+ % it).replace("\t", sep) for it in ret]
596
+ else:
597
+ return ret
598
+
599
+ @inicializar_y_capturar_excepciones
600
+ def ConsultarTiposDatosAdicionales(self, sep="|"):
601
+ "Recuperar lista de los datos adicionales a informar segοΏ½n RG."
602
+ ret = self.client.consultarTiposDatosAdicionales(
603
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit, })
604
+ result = ret['consultarTiposDatosAdicionalesReturn']
605
+ self.__analizar_errores(result)
606
+ ret = []
607
+ for u in result['arrayTiposDatosAdicionales']:
608
+ u = u['codigoDescripcionString']
609
+ r = {'codigo': u.get('codigo'), 'ds': u.get('descripcion'), }
610
+ ret.append(r)
611
+ return [("\t%(codigo)s\t%(ds)s\t"
612
+ % it).replace("\t", sep) for it in ret] if sep else ret
613
+
614
+ @inicializar_y_capturar_excepciones
615
+ def ConsultarFomasPago(self, sep="|"):
616
+ "Recuperar lista de las formas de pago"
617
+ ret = self.client.consultarFormasPago(
618
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit, })
619
+ result = ret['consultarFormasPagoReturn']
620
+ self.__analizar_errores(result)
621
+ ret = []
622
+ for u in result['arrayFormasPago']:
623
+ u = u['codigoDescripcion']
624
+ r = {'codigo': u.get('codigo'), 'ds': u.get('descripcion'), }
625
+ ret.append(r)
626
+ return [("\t%(codigo)s\t%(ds)s\t"
627
+ % it).replace("\t", sep) for it in ret] if sep else ret
628
+
629
+ @inicializar_y_capturar_excepciones
630
+ def ConsultarTiposTarjeta(self, forma_pago=None, sep="|"):
631
+ "Recuperar lista de los tipos de tarjeta habilitados"
632
+ ret = self.client.consultarTiposTarjeta(
633
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit, },
634
+ formaPago=forma_pago)
635
+ result = ret['consultarTiposTarjetaReturn']
636
+ self.__analizar_errores(result)
637
+ ret = []
638
+ for u in result['arrayTiposTarjeta']:
639
+ u = u['codigoDescripcion']
640
+ r = {'codigo': u.get('codigo'), 'ds': u.get('descripcion'), }
641
+ ret.append(r)
642
+ return [("\t%(codigo)s\t%(ds)s\t"
643
+ % it).replace("\t", sep) for it in ret] if sep else ret
644
+
645
+ @inicializar_y_capturar_excepciones
646
+ def ConsultarTiposCuenta(self, sep="|"):
647
+ "Recuperar lista de los tipos de tarjeta habilitados"
648
+ ret = self.client.consultarTiposCuenta(
649
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit, })
650
+ result = ret['consultarTiposCuentaReturn']
651
+ self.__analizar_errores(result)
652
+ ret = []
653
+ for u in result['arrayTiposCuenta']:
654
+ u = u['codigoDescripcion']
655
+ r = {'codigo': u.get('codigo'), 'ds': u.get('descripcion'), }
656
+ ret.append(r)
657
+ return [("\t%(codigo)s\t%(ds)s\t"
658
+ % it).replace("\t", sep) for it in ret] if sep else ret
659
+
660
+
661
+ def main():
662
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
663
+ import os
664
+ import time
665
+
666
+ DEBUG = '--debug' in sys.argv
667
+
668
+ # obteniendo el TA para pruebas
669
+ from .wsaa import WSAA
670
+ ta = WSAA().Autenticar("wsct", "reingart.crt", "reingart.key")
671
+
672
+ wsct = WSCT()
673
+ wsct.SetTicketAcceso(ta)
674
+ wsct.Cuit = "20267565393"
675
+
676
+ cache = ""
677
+ if "--prod" in sys.argv:
678
+ wsdl = "https://serviciosjava.afip.gob.ar/wsct/CTService?wsdl"
679
+ else:
680
+ wsdl = WSDL
681
+ wsct.Conectar(cache, wsdl, cacert="conf/afip_ca_info.crt")
682
+
683
+ if "--dummy" in sys.argv:
684
+ print(wsct.client.help("dummy"))
685
+ wsct.Dummy()
686
+ print("AppServerStatus", wsct.AppServerStatus)
687
+ print("DbServerStatus", wsct.DbServerStatus)
688
+ print("AuthServerStatus", wsct.AuthServerStatus)
689
+
690
+ if "--prueba" in sys.argv:
691
+ # print wsct.client.help("autorizarComprobante").encode("latin1")
692
+ try:
693
+ tipo_cbte = 195
694
+ punto_vta = 4000
695
+ cbte_nro = wsct.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
696
+ fecha = datetime.datetime.now().strftime("%Y-%m-%d")
697
+ tipo_doc = 80
698
+ nro_doc = "50000000059"
699
+ cbte_nro = int(cbte_nro) + 1
700
+ id_impositivo = 9 # "Cliente del Exterior"
701
+ cod_relacion = 3 # Alojamiento Directo a Turista No Residente
702
+ imp_total = "101.00"
703
+ imp_tot_conc = "0.00"
704
+ imp_neto = "100.00"
705
+ imp_trib = "1.00"
706
+ imp_op_ex = "0.00"
707
+ imp_subtotal = "100.00"
708
+ imp_reintegro = -21.00 # validaciοΏ½n AFIP 346
709
+ cod_pais = 203
710
+ domicilio = "Rua N.76 km 34.5 Alagoas"
711
+ fecha_cbte = fecha
712
+ moneda_id = 'PES'
713
+ moneda_ctz = '1.000'
714
+ obs = "Observaciones Comerciales, libre"
715
+
716
+ wsct.CrearFactura(tipo_doc, nro_doc, tipo_cbte, punto_vta,
717
+ cbte_nro, imp_total, imp_tot_conc, imp_neto,
718
+ imp_subtotal, imp_trib, imp_op_ex, imp_reintegro,
719
+ fecha_cbte, id_impositivo, cod_pais, domicilio,
720
+ cod_relacion, moneda_id, moneda_ctz, obs)
721
+
722
+ tributo_id = 99
723
+ desc = 'Impuesto Municipal Matanza'
724
+ base_imp = "100.00"
725
+ alic = "1.00"
726
+ importe = "1.00"
727
+ wsct.AgregarTributo(tributo_id, desc, base_imp, alic, importe)
728
+
729
+ iva_id = 5 # 21%
730
+ base_imp = 100
731
+ importe = 21
732
+ wsct.AgregarIva(iva_id, base_imp, importe)
733
+
734
+ tipo = 0 # Item General
735
+ cod_tur = 1 # Servicio de hotelerοΏ½a - alojamiento sin desayuno
736
+ codigo = "T0001"
737
+ ds = "Descripcion del producto P0001"
738
+ iva_id = 5
739
+ imp_iva = 21.00
740
+ imp_subtotal = 121.00
741
+ wsct.AgregarItem(tipo, cod_tur, codigo, ds,
742
+ iva_id, imp_iva, imp_subtotal)
743
+
744
+ codigo = 68 # tarjeta de crοΏ½dito
745
+ tipo_tarjeta = 99 # otra (ver tabla de parοΏ½metros)
746
+ numero_tarjeta = "999999"
747
+ swift_code = None
748
+ tipo_cuenta = None
749
+ numero_cuenta = None
750
+ wsct.AgregarFormaPago(codigo, tipo_tarjeta, numero_tarjeta,
751
+ swift_code, tipo_cuenta, numero_cuenta)
752
+
753
+ print(wsct.factura)
754
+
755
+ wsct.AutorizarComprobante()
756
+
757
+ print("Resultado", wsct.Resultado)
758
+ print("CAE", wsct.CAE)
759
+ print("Vencimiento", wsct.Vencimiento)
760
+ print("Reproceso", wsct.Reproceso)
761
+ print("Errores", wsct.ErrMsg)
762
+
763
+ print(wsct.Excepcion)
764
+ print(wsct.ErrMsg)
765
+
766
+ cae = wsct.CAE
767
+
768
+ if cae:
769
+
770
+ wsct.ConsultarComprobante(tipo_cbte, punto_vta, cbte_nro)
771
+ print("CAE consulta", wsct.CAE, wsct.CAE == cae)
772
+ print("NRO consulta", wsct.CbteNro, wsct.CbteNro == cbte_nro)
773
+ print("TOTAL consulta", wsct.ImpTotal, wsct.ImpTotal == imp_total)
774
+
775
+ wsct.AnalizarXml("XmlResponse")
776
+ assert wsct.ObtenerTagXml('codigoAutorizacion') == str(wsct.CAE)
777
+ assert wsct.ObtenerTagXml('codigoConcepto') == str(concepto)
778
+ assert wsct.ObtenerTagXml('arrayItems', 0, 'item', 'unidadesMtx') == '123456'
779
+
780
+ except BaseException:
781
+ print(wsct.XmlRequest)
782
+ print(wsct.XmlResponse)
783
+ print(wsct.ErrCode)
784
+ print(wsct.ErrMsg)
785
+
786
+ if "--ptosventa" in sys.argv:
787
+ print(wsct.ConsultarPuntosVenta())
788
+
789
+ if "--parametros" in sys.argv:
790
+ print(wsct.ConsultarTiposDatosAdicionales())
791
+ print(wsct.ConsultarTiposComprobante())
792
+ print(wsct.ConsultarTiposDocumento())
793
+ print(wsct.ConsultarTiposIVA())
794
+ print(wsct.ConsultarCondicionesIVA())
795
+ print(wsct.ConsultarMonedas())
796
+ print(wsct.ConsultarTiposItem())
797
+ print(wsct.ConsultarCodigosItemTurismo())
798
+ print(wsct.ConsultarTiposTributo())
799
+ print(wsct.ConsultarFomasPago())
800
+ for forma_pago in wsct.ConsultarFomasPago(sep=None)[:2]:
801
+ print(wsct.ConsultarTiposTarjeta(forma_pago["codigo"]))
802
+ print(wsct.ConsultarTiposCuenta())
803
+ print("\n".join(wsct.ConsultarPaises()))
804
+ print("\n".join(wsct.ConsultarCUITsPaises()))
805
+
806
+ if "--cotizacion" in sys.argv:
807
+ print(wsct.ConsultarCotizacionMoneda('DOL'))
808
+
809
+
810
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
811
+ INSTALL_DIR = WSCT.InstallDir = get_install_dir()
812
+
813
+
814
+ if __name__ == '__main__':
815
+
816
+ if "--register" in sys.argv or "--unregister" in sys.argv:
817
+ import win32com.server.register
818
+ win32com.server.register.UseCommandLine(WSCT)
819
+ else:
820
+ main()
app/pyafipws/wsctg.py ADDED
@@ -0,0 +1,1198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ import base64
14
+ import time
15
+ import sys
16
+ import os
17
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, BaseWS, inicializar_y_capturar_excepciones, get_install_dir
18
+ from . import utils
19
+ from pysimplesoap.client import SoapFault
20
+ import traceback
21
+ from .utils import date
22
+ """MΓ³dulo para obtener CΓ³digo de Trazabilidad de Granos
23
+ del web service WSCTG versiΓ³n 4.0 de AFIP (RG3593/14)
24
+ """
25
+
26
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
27
+ __copyright__ = "Copyright (C) 2010-2014 Mariano Reingart"
28
+ __license__ = "LGPL 3.0"
29
+ __version__ = "1.14e"
30
+
31
+ LICENCIA = """
32
+ wsctg.py: Interfaz para generar CΓ³digo de Trazabilidad de Granos AFIP v1.1
33
+ Copyright (C) 2014-2015 Mariano Reingart reingart@gmail.com
34
+ http://www.sistemasagiles.com.ar/trac/wiki/CodigoTrazabilidadGranos
35
+
36
+ Este progarma es software libre, se entrega ABSOLUTAMENTE SIN GARANTIA
37
+ y es bienvenido a redistribuirlo bajo la licencia GPLv3.
38
+
39
+ Para informaciΓ³n adicional sobre garantΓ­a, soporte tΓ©cnico comercial
40
+ e incorporaciΓ³n/distribuciΓ³n en programas propietarios ver PyAfipWs:
41
+ http://www.sistemasagiles.com.ar/trac/wiki/PyAfipWs
42
+ """
43
+
44
+ AYUDA = """
45
+ Opciones:
46
+ --ayuda: este mensaje
47
+
48
+ --debug: modo depuraciΓ³n (detalla y confirma las operaciones)
49
+ --formato: muestra el formato de los archivos de entrada/salida
50
+ --prueba: genera y autoriza una CTG de prueba (no usar en producciΓ³n!)
51
+ --xml: almacena los requerimientos y respuestas XML (depuraciΓ³n)
52
+
53
+ --dummy: consulta estado de servidores
54
+ --solicitar: obtiene el CTG (segΓΊn archivo de entrada en TXT o CSV)
55
+ --confirmar: confirma el CTG (segΓΊn archivo de entrada en TXT o CSV)
56
+ --anular: anula el CTG
57
+ --rechazar: permite al destino rechazar el CTG
58
+ --confirmar_arribo: confirma el arribo de un CTG
59
+ --confirmar_definitivo: confirma el arribo definitivo de un CTG
60
+ --regresar_a_origen_rechazado: tomar la acciΓ³n de "Regresar a Origen"
61
+ --cambiar_destino_destinatario_rechazado: "Cambio de Destino y Destinatario"
62
+
63
+ --consultar: consulta las CTG generadas
64
+ --consultar_excel: consulta las CTG generadas (genera un excel)
65
+ --consultar_detalle: obtiene el detalle de una CTG
66
+ --consultar_constancia_pdf: descarga el documento PDF de una CTG
67
+ --pendientes: consulta CTGs otorgados, rechazados, confirmados a resolver
68
+ --consultar_rechazados: obtener CTGs rechazados para darles un nuevo curso
69
+ --consultar_activos_por_patente: consulta de CTGs activos por patente
70
+
71
+ --provincias: obtiene el listado de provincias
72
+ --localidades: obtiene el listado de localidades por provincia
73
+ --especies: obtiene el listado de especies
74
+ --cosechas: obtiene el listado de cosechas
75
+
76
+ Ver wsctg.ini para parΓ‘metros de configuraciΓ³n (URL, certificados, etc.)"
77
+ """
78
+
79
+
80
+ # importo funciones compartidas:
81
+
82
+
83
+ # constantes de configuraciΓ³n (homologaciΓ³n):
84
+
85
+ WSDL = "https://fwshomo.afip.gov.ar/wsctg/services/CTGService_v4.0?wsdl"
86
+
87
+ DEBUG = False
88
+ XML = False
89
+ CONFIG_FILE = "wsctg.ini"
90
+ HOMO = False
91
+
92
+ # definiciΓ³n del formato del archivo de intercambio:
93
+
94
+ ENCABEZADO = [
95
+ # datos enviados
96
+ ('tipo_reg', 1, A), # 0: encabezado
97
+ ('numero_carta_de_porte', 13, N),
98
+ ('codigo_especie', 5, N),
99
+ ('cuit_canjeador', 11, N),
100
+ ('cuit_destino', 11, N),
101
+ ('cuit_destinatario', 11, N),
102
+ ('codigo_localidad_origen', 6, N),
103
+ ('codigo_localidad_destino', 6, N),
104
+ ('codigo_cosecha', 4, N),
105
+ ('peso_neto_carga', 5, N),
106
+ ('cant_horas', 2, N),
107
+ ('reservado1', 6, A),
108
+ ('cuit_transportista', 11, N),
109
+ ('km_a_recorrer', 4, N), # km_recorridos (en consulta WSCTGv2)
110
+ ('establecimiento', 6, N), # confirmar arribo
111
+ ('remitente_comercial_como_canjeador', 1, A), # S/N solicitar CTG inicial (WSCTGv2)
112
+ ('consumo_propio', 1, A), # S/N confirmar arribo (WSCTGv2)
113
+
114
+ # datos devueltos
115
+ ('numero_ctg', 8, N),
116
+ ('fecha_hora', 19, A),
117
+ ('vigencia_desde', 10, A),
118
+ ('vigencia_hasta', 10, A),
119
+ ('transaccion', 12, N),
120
+ ('tarifa_referencia', 6, I, 2), # consultar detalle
121
+ ('estado', 20, A),
122
+ ('imprime_constancia', 5, A),
123
+ ('observaciones', 200, A),
124
+ ('errores', 1000, A),
125
+ ('controles', 1000, A),
126
+ ('detalle', 1000, A), # consultar detalle (WSCTGv2)
127
+
128
+ # nuevos campos agregados:
129
+ ('cuit_chofer', 11, N),
130
+
131
+ # nuevos campos agregados WSCTGv3:
132
+ ('cuit_corredor', 12, N),
133
+ ('remitente_comercial_como_productor', 1, A),
134
+ ('patente_vehiculo', 10, A),
135
+
136
+ # nuevos campos agregados WSCTGv4:
137
+ ('ctc_codigo', 2, A),
138
+ ('turno', 50, A),
139
+
140
+ ]
141
+
142
+
143
+ class WSCTG(BaseWS):
144
+ "Interfaz para el WebService de CΓ³digo de Trazabilidad de Granos (Version 3)"
145
+ _public_methods_ = ['Conectar', 'Dummy', 'SetTicketAcceso', 'DebugLog',
146
+ 'SolicitarCTGInicial', 'SolicitarCTGDatoPendiente',
147
+ 'ConfirmarArribo', 'ConfirmarDefinitivo',
148
+ 'AnularCTG', 'RechazarCTG', 'CTGsPendientesResolucion',
149
+ 'ConsultarCTG', 'LeerDatosCTG', 'ConsultarDetalleCTG',
150
+ 'ConsultarCTGExcel', 'ConsultarConstanciaCTGPDF',
151
+ 'ConsultarCTGRechazados',
152
+ 'RegresarAOrigenCTGRechazado',
153
+ 'CambiarDestinoDestinatarioCTGRechazado',
154
+ 'ConsultarCTGActivosPorPatente',
155
+ 'ConsultarProvincias',
156
+ 'ConsultarLocalidadesPorProvincia',
157
+ 'ConsultarEstablecimientos',
158
+ 'ConsultarCosechas',
159
+ 'ConsultarEspecies',
160
+ 'SetParametros', 'SetParametro', 'GetParametro',
161
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
162
+ ]
163
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
164
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
165
+ 'Excepcion', 'ErrCode', 'ErrMsg', 'LanzarExcepciones', 'Errores',
166
+ 'XmlRequest', 'XmlResponse', 'Version', 'Traceback',
167
+ 'NumeroCTG', 'CartaPorte', 'FechaHora', 'CodigoOperacion',
168
+ 'CodigoTransaccion', 'Observaciones', 'Controles', 'DatosCTG',
169
+ 'VigenciaHasta', 'VigenciaDesde', 'Estado', 'ImprimeConstancia',
170
+ 'TarifaReferencia', 'Destino', 'Destinatario', 'Detalle',
171
+ 'Patente', 'PesoNeto', 'FechaVencimiento',
172
+ 'UsuarioSolicitante', 'UsuarioReal', 'CtcCodigo', 'Turno',
173
+ ]
174
+ _reg_progid_ = "WSCTG"
175
+ _reg_clsid_ = "{4383E947-57C4-47C5-8419-85221580CB48}"
176
+
177
+ # Variables globales para BaseWS:
178
+ HOMO = HOMO
179
+ WSDL = WSDL
180
+ LanzarExcepciones = False
181
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
182
+
183
+ def Conectar(self, *args, **kwargs):
184
+ ret = BaseWS.Conectar(self, *args, **kwargs)
185
+ # corregir descripciΓ³n de servicio WSDL publicado por AFIP
186
+ # kmARecorrer -> kmRecorridos (ConsultarDetalleCTG)
187
+ port = self.client.services['CTGService_v4.0']['ports']['CTGServiceHttpSoap20Endpoint']
188
+ msg = port['operations']['consultarDetalleCTG']['output']['consultarDetalleCTGResponse']
189
+ msg['response']['consultarDetalleCTGDatos']['kmRecorridos'] = int
190
+ return ret
191
+
192
+ def inicializar(self):
193
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
194
+ self.CodError = self.DescError = ''
195
+ self.NumeroCTG = self.CartaPorte = ""
196
+ self.CodigoTransaccion = self.Observaciones = ''
197
+ self.FechaHora = self.CodigoOperacion = ""
198
+ self.VigenciaDesde = self.VigenciaHasta = ""
199
+ self.Controles = []
200
+ self.DatosCTG = self.TarifaReferencia = None
201
+ self.CodigoTransaccion = self.Observaciones = ''
202
+ self.Detalle = self.Destino = self.Destinatario = ''
203
+ self.Patente = self.PesoNeto = self.FechaVencimiento = ''
204
+ self.UsuarioSolicitante = self.UsuarioReal = ''
205
+ self.CtcCodigo = self.Turno = ""
206
+
207
+ def __analizar_errores(self, ret):
208
+ "Comprueba y extrae errores si existen en la respuesta XML"
209
+ if 'arrayErrores' in ret:
210
+ errores = ret['arrayErrores'] or []
211
+ self.Errores = [err['error'] for err in errores]
212
+ self.ErrCode = ' '.join(self.Errores)
213
+ self.ErrMsg = '\n'.join(self.Errores)
214
+
215
+ def __analizar_controles(self, ret):
216
+ "Comprueba y extrae controles si existen en la respuesta XML"
217
+ if 'arrayControles' in ret:
218
+ controles = ret['arrayControles']
219
+ self.Controles = ["%(tipo)s: %(descripcion)s" % ctl['control']
220
+ for ctl in controles]
221
+
222
+ @inicializar_y_capturar_excepciones
223
+ def Dummy(self):
224
+ "Obtener el estado de los servidores de la AFIP"
225
+ results = self.client.dummy()['response']
226
+ self.AppServerStatus = str(results['appserver'])
227
+ self.DbServerStatus = str(results['dbserver'])
228
+ self.AuthServerStatus = str(results['authserver'])
229
+
230
+ @inicializar_y_capturar_excepciones
231
+ def AnularCTG(self, carta_porte, ctg):
232
+ "Anular el CTG si se creΓ³ el mismo por error"
233
+ response = self.client.anularCTG(request=dict(
234
+ auth={
235
+ 'token': self.Token, 'sign': self.Sign,
236
+ 'cuitRepresentado': self.Cuit, },
237
+ datosAnularCTG={
238
+ 'cartaPorte': carta_porte,
239
+ 'ctg': ctg, }))['response']
240
+ datos = response.get('datosResponse')
241
+ self.__analizar_errores(response)
242
+ if datos:
243
+ self.CartaPorte = str(datos['cartaPorte'])
244
+ self.NumeroCTG = str(datos['ctg'])
245
+ self.FechaHora = str(datos['fechaHora'])
246
+ self.CodigoOperacion = str(datos['codigoOperacion'])
247
+
248
+ @inicializar_y_capturar_excepciones
249
+ def RechazarCTG(self, carta_porte, ctg, motivo):
250
+ "El Destino puede rechazar el CTG a travΓ©s de la siguiente operatoria"
251
+ response = self.client.rechazarCTG(request=dict(
252
+ auth={
253
+ 'token': self.Token, 'sign': self.Sign,
254
+ 'cuitRepresentado': self.Cuit, },
255
+ datosRechazarCTG={
256
+ 'cartaPorte': carta_porte,
257
+ 'ctg': ctg, 'motivoRechazo': motivo,
258
+ }))['response']
259
+ datos = response.get('datosResponse')
260
+ self.__analizar_errores(response)
261
+ if datos:
262
+ self.CartaPorte = str(datos['cartaPorte'])
263
+ self.NumeroCTG = str(datos['CTG'])
264
+ self.FechaHora = str(datos['fechaHora'])
265
+ self.CodigoOperacion = str(datos['codigoOperacion'])
266
+
267
+ @inicializar_y_capturar_excepciones
268
+ def SolicitarCTGInicial(self, numero_carta_de_porte, codigo_especie,
269
+ cuit_canjeador, cuit_destino, cuit_destinatario, codigo_localidad_origen,
270
+ codigo_localidad_destino, codigo_cosecha, peso_neto_carga,
271
+ cant_horas=None, patente_vehiculo=None, cuit_transportista=None,
272
+ km_a_recorrer=None, remitente_comercial_como_canjeador=None,
273
+ cuit_corredor=None, remitente_comercial_como_productor=None,
274
+ turno=None,
275
+ **kwargs):
276
+ "Solicitar CTG Desde el Inicio"
277
+ # ajusto parΓ‘metros segΓΊn validaciones de AFIP:
278
+ if not cuit_canjeador or int(cuit_canjeador) == 0:
279
+ cuit_canjeador = None # nulo
280
+ if not cuit_corredor or int(cuit_corredor) == 0:
281
+ cuit_corredor = None # nulo
282
+ if not remitente_comercial_como_canjeador:
283
+ remitente_comercial_como_canjeador = None
284
+ if not remitente_comercial_como_productor:
285
+ remitente_comercial_como_productor = None
286
+ if turno == '':
287
+ turno = None # nulo
288
+
289
+ ret = self.client.solicitarCTGInicial(request=dict(
290
+ auth={
291
+ 'token': self.Token, 'sign': self.Sign,
292
+ 'cuitRepresentado': self.Cuit, },
293
+ datosSolicitarCTGInicial=dict(
294
+ cartaPorte=numero_carta_de_porte,
295
+ codigoEspecie=codigo_especie,
296
+ cuitCanjeador=cuit_canjeador or None,
297
+ remitenteComercialComoCanjeador=remitente_comercial_como_canjeador,
298
+ cuitDestino=cuit_destino,
299
+ cuitDestinatario=cuit_destinatario,
300
+ codigoLocalidadOrigen=codigo_localidad_origen,
301
+ codigoLocalidadDestino=codigo_localidad_destino,
302
+ codigoCosecha=codigo_cosecha,
303
+ pesoNeto=peso_neto_carga,
304
+ cuitTransportista=cuit_transportista,
305
+ cantHoras=cant_horas,
306
+ patente=patente_vehiculo,
307
+ kmARecorrer=km_a_recorrer,
308
+ cuitCorredor=cuit_corredor,
309
+ remitenteComercialcomoProductor=remitente_comercial_como_productor,
310
+ turno=turno,
311
+ )))['response']
312
+ self.__analizar_errores(ret)
313
+ self.Observaciones = ret['observacion']
314
+ datos = ret.get('datosSolicitarCTGResponse')
315
+ if datos:
316
+ self.CartaPorte = str(datos['cartaPorte'])
317
+ datos_ctg = datos.get('datosSolicitarCTG')
318
+ if datos_ctg:
319
+ self.NumeroCTG = str(datos_ctg['ctg'])
320
+ self.FechaHora = str(datos_ctg['fechaEmision'])
321
+ self.VigenciaDesde = str(datos_ctg['fechaVigenciaDesde'])
322
+ self.VigenciaHasta = str(datos_ctg['fechaVigenciaHasta'])
323
+ self.TarifaReferencia = str(datos_ctg.get('tarifaReferencia'))
324
+ self.__analizar_controles(datos)
325
+ return self.NumeroCTG or 0
326
+
327
+ @inicializar_y_capturar_excepciones
328
+ def SolicitarCTGDatoPendiente(self, numero_carta_de_porte, cant_horas,
329
+ patente_vehiculo, cuit_transportista, patente=None, turno=None):
330
+ "Solicitud que permite completar los datos faltantes de un Pre-CTG "
331
+ "generado anteriormente a travΓ©s de la operaciΓ³n solicitarCTGInicial"
332
+ ret = self.client.solicitarCTGDatoPendiente(request=dict(
333
+ auth={
334
+ 'token': self.Token, 'sign': self.Sign,
335
+ 'cuitRepresentado': self.Cuit, },
336
+ datosSolicitarCTGDatoPendiente=dict(
337
+ cartaPorte=numero_carta_de_porte,
338
+ cuitTransportista=cuit_transportista,
339
+ cantHoras=cant_horas,
340
+ patente=patente,
341
+ turno=turno,
342
+ )))['response']
343
+ self.__analizar_errores(ret)
344
+ self.Observaciones = ret['observacion']
345
+ datos = ret.get('datosSolicitarCTGResponse')
346
+ if datos:
347
+ self.CartaPorte = str(datos['cartaPorte'])
348
+ datos_ctg = datos.get('datosSolicitarCTG')
349
+ if datos_ctg:
350
+ self.NumeroCTG = str(datos_ctg['ctg'])
351
+ self.FechaHora = str(datos_ctg['fechaEmision'])
352
+ self.VigenciaDesde = str(datos_ctg['fechaVigenciaDesde'])
353
+ self.VigenciaHasta = str(datos_ctg['fechaVigenciaHasta'])
354
+ self.TarifaReferencia = str(datos_ctg.get('tarifaReferencia'))
355
+ self.__analizar_controles(datos)
356
+ return self.NumeroCTG
357
+
358
+ @inicializar_y_capturar_excepciones
359
+ def ConfirmarArribo(self, numero_carta_de_porte, numero_ctg,
360
+ cuit_transportista, peso_neto_carga,
361
+ consumo_propio, establecimiento=None, cuit_chofer=None,
362
+ **kwargs):
363
+ "Confirma arribo CTG"
364
+ ret = self.client.confirmarArribo(request=dict(
365
+ auth={
366
+ 'token': self.Token, 'sign': self.Sign,
367
+ 'cuitRepresentado': self.Cuit, },
368
+ datosConfirmarArribo=dict(
369
+ cartaPorte=numero_carta_de_porte,
370
+ ctg=numero_ctg,
371
+ cuitTransportista=cuit_transportista,
372
+ cuitChofer=cuit_chofer,
373
+ cantKilosCartaPorte=peso_neto_carga,
374
+ consumoPropio=consumo_propio,
375
+ establecimiento=establecimiento,
376
+ )))['response']
377
+ self.__analizar_errores(ret)
378
+ datos = ret.get('datosResponse')
379
+ if datos:
380
+ self.CartaPorte = str(datos['cartaPorte'])
381
+ self.NumeroCTG = str(datos['ctg'])
382
+ self.FechaHora = str(datos['fechaHora'])
383
+ self.CodigoTransaccion = str(datos['codigoOperacion'])
384
+ self.Observaciones = ""
385
+ return self.CodigoTransaccion
386
+
387
+ @inicializar_y_capturar_excepciones
388
+ def ConfirmarDefinitivo(self, numero_carta_de_porte, numero_ctg,
389
+ establecimiento=None, codigo_cosecha=None, peso_neto_carga=None,
390
+ **kwargs):
391
+ "Confirma arribo definitivo CTG"
392
+ ret = self.client.confirmarDefinitivo(request=dict(
393
+ auth={
394
+ 'token': self.Token, 'sign': self.Sign,
395
+ 'cuitRepresentado': self.Cuit, },
396
+ datosConfirmarDefinitivo=dict(
397
+ cartaPorte=numero_carta_de_porte,
398
+ ctg=numero_ctg,
399
+ establecimiento=establecimiento,
400
+ codigoCosecha=codigo_cosecha,
401
+ pesoNeto=peso_neto_carga,
402
+ )))['response']
403
+ self.__analizar_errores(ret)
404
+ datos = ret.get('datosResponse')
405
+ if datos:
406
+ self.CartaPorte = str(datos['cartaPorte'])
407
+ self.NumeroCTG = str(datos['ctg'])
408
+ self.FechaHora = str(datos['fechaHora'])
409
+ self.CodigoTransaccion = str(datos.get('codigoOperacion', ""))
410
+ self.Observaciones = ""
411
+ return self.CodigoTransaccion
412
+
413
+ @inicializar_y_capturar_excepciones
414
+ def RegresarAOrigenCTGRechazado(self, numero_carta_de_porte, numero_ctg,
415
+ km_a_recorrer=None,
416
+ **kwargs):
417
+ "Al consultar los CTGs rechazados se puede Regresar a Origen"
418
+ ret = self.client.regresarAOrigenCTGRechazado(request=dict(
419
+ auth={
420
+ 'token': self.Token, 'sign': self.Sign,
421
+ 'cuitRepresentado': self.Cuit, },
422
+ datosRegresarAOrigenCTGRechazado=dict(
423
+ cartaPorte=numero_carta_de_porte,
424
+ ctg=numero_ctg, kmARecorrer=km_a_recorrer,
425
+ )))['response']
426
+ self.__analizar_errores(ret)
427
+ datos = ret.get('datosResponse')
428
+ if datos:
429
+ self.CartaPorte = str(datos['cartaPorte'])
430
+ self.NumeroCTG = str(datos['ctg'])
431
+ self.FechaHora = str(datos['fechaHora'])
432
+ self.CodigoTransaccion = str(datos['codigoOperacion'])
433
+ self.Observaciones = ""
434
+ return self.CodigoTransaccion
435
+
436
+ @inicializar_y_capturar_excepciones
437
+ def CambiarDestinoDestinatarioCTGRechazado(self, numero_carta_de_porte,
438
+ numero_ctg, codigo_localidad_destino=None,
439
+ cuit_destino=None, cuit_destinatario=None,
440
+ km_a_recorrer=None, turno=None,
441
+ **kwargs):
442
+ "Tomar acciΓ³n de Cambio de Destino y Destinatario para CTG rechazado"
443
+ ret = self.client.cambiarDestinoDestinatarioCTGRechazado(request=dict(
444
+ auth={
445
+ 'token': self.Token, 'sign': self.Sign,
446
+ 'cuitRepresentado': self.Cuit, },
447
+ datosCambiarDestinoDestinatarioCTGRechazado=dict(
448
+ cartaPorte=numero_carta_de_porte,
449
+ ctg=numero_ctg,
450
+ codigoLocalidadDestino=codigo_localidad_destino,
451
+ cuitDestino=cuit_destino,
452
+ cuitDestinatario=cuit_destinatario,
453
+ kmARecorrer=km_a_recorrer,
454
+ turno=turno,
455
+ )))['response']
456
+ self.__analizar_errores(ret)
457
+ datos = ret.get('datosResponse')
458
+ if datos:
459
+ self.CartaPorte = str(datos['cartaPorte'])
460
+ self.NumeroCTG = str(datos['ctg'])
461
+ self.FechaHora = str(datos['fechaHora'])
462
+ self.CodigoTransaccion = str(datos['codigoOperacion'])
463
+ self.Observaciones = ""
464
+ return self.CodigoTransaccion
465
+
466
+ @inicializar_y_capturar_excepciones
467
+ def ConsultarCTG(self, numero_carta_de_porte=None, numero_ctg=None,
468
+ patente=None, cuit_solicitante=None, cuit_destino=None,
469
+ fecha_emision_desde=None, fecha_emision_hasta=None):
470
+ "OperaciΓ³n que realiza consulta de CTGs segΓΊn el criterio ingresado."
471
+ ret = self.client.consultarCTG(request=dict(
472
+ auth={
473
+ 'token': self.Token, 'sign': self.Sign,
474
+ 'cuitRepresentado': self.Cuit, },
475
+ consultarCTGDatos=dict(
476
+ cartaPorte=numero_carta_de_porte,
477
+ ctg=numero_ctg,
478
+ patente=patente,
479
+ cuitSolicitante=cuit_solicitante,
480
+ cuitDestino=cuit_destino,
481
+ fechaEmisionDesde=fecha_emision_desde,
482
+ fechaEmisionHasta=fecha_emision_hasta,
483
+ )))['response']
484
+ self.__analizar_errores(ret)
485
+ datos = ret.get('arrayDatosConsultarCTG')
486
+ if datos:
487
+ self.DatosCTG = datos
488
+ self.LeerDatosCTG(pop=False)
489
+ return True
490
+ else:
491
+ self.DatosCTG = []
492
+ return ''
493
+
494
+ @inicializar_y_capturar_excepciones
495
+ def ConsultarCTGRechazados(self):
496
+ "Consulta de CTGs Otorgados, CTGs Rechazados y CTGs Confirmados"
497
+ ret = self.client.consultarCTGRechazados(request=dict(
498
+ auth={
499
+ 'token': self.Token, 'sign': self.Sign,
500
+ 'cuitRepresentado': self.Cuit, },
501
+ ))['response']
502
+ self.__analizar_errores(ret)
503
+ datos = ret.get('arrayConsultarCTGRechazados')
504
+ if datos:
505
+ self.DatosCTG = datos
506
+ self.LeerDatosCTG(pop=False)
507
+ return True
508
+ else:
509
+ self.DatosCTG = []
510
+ return False
511
+
512
+ @inicializar_y_capturar_excepciones
513
+ def ConsultarCTGActivosPorPatente(self, patente="ZZZ999"):
514
+ "Consulta de CTGs activos por patente"
515
+ ret = self.client.consultarCTGActivosPorPatente(request=dict(
516
+ auth={
517
+ 'token': self.Token, 'sign': self.Sign,
518
+ 'cuitRepresentado': self.Cuit, },
519
+ patente=patente,
520
+ ))['response']
521
+ self.__analizar_errores(ret)
522
+ datos = ret.get('arrayConsultarCTGActivosPorPatenteResponse')
523
+ if datos:
524
+ self.DatosCTG = datos
525
+ self.LeerDatosCTG(pop=False)
526
+ return True
527
+ else:
528
+ self.DatosCTG = []
529
+ return False
530
+
531
+ @inicializar_y_capturar_excepciones
532
+ def CTGsPendientesResolucion(self):
533
+ "Consulta de CTGs Otorgados, CTGs Rechazados y CTGs Confirmados"
534
+ ret = self.client.CTGsPendientesResolucion(request=dict(
535
+ auth={
536
+ 'token': self.Token, 'sign': self.Sign,
537
+ 'cuitRepresentado': self.Cuit, },
538
+ ))['response']
539
+ self.__analizar_errores(ret)
540
+ if ret:
541
+ self.DatosCTG = ret
542
+ return True
543
+ else:
544
+ self.DatosCTG = {}
545
+ return False
546
+
547
+ def LeerDatosCTG(self, clave='', pop=True):
548
+ "Recorro los datos devueltos y devuelvo el primero si existe"
549
+
550
+ if clave and self.DatosCTG:
551
+ # obtengo la lista por estado pendiente de resoluciΓ³n ("array")
552
+ datos = self.DatosCTG[clave]
553
+ else:
554
+ # uso directamente la lista devuelta por la consulta
555
+ datos = self.DatosCTG
556
+ if datos:
557
+ # extraigo el primer item
558
+ if pop:
559
+ datos = datos.pop(0)
560
+ else:
561
+ datos = datos[0]
562
+ for det in ('datosConsultarCTG', 'detalleConsultaCTGRechazado',
563
+ 'detalleConsultaCTGActivo'):
564
+ if det in datos:
565
+ datos_ctg = datos[det]
566
+ break
567
+ else:
568
+ # elemento del array no encontrado:
569
+ return ""
570
+ self.CartaPorte = str(datos_ctg['cartaPorte'])
571
+ self.NumeroCTG = str(datos_ctg['ctg'])
572
+ self.Estado = str(datos_ctg.get('estado', ""))
573
+ self.ImprimeConstancia = str(datos_ctg.get('imprimeConstancia', ""))
574
+ for campo in ("fechaRechazo", "fechaEmision", "fechaSolicitud",
575
+ "fechaConfirmacionArribo"):
576
+ if campo in datos_ctg:
577
+ self.FechaHora = str(datos_ctg.get(campo))
578
+ self.Destino = datos_ctg.get("destino", "")
579
+ self.Destinatario = datos_ctg.get("destinatario", "")
580
+ self.Observaciones = datos_ctg.get("observaciones", "")
581
+ self.Patente = datos_ctg.get("patente")
582
+ self.PesoNeto = datos_ctg.get("pesoNeto")
583
+ self.FechaVencimiento = datos_ctg.get("fechaVencimiento")
584
+ self.UsuarioSolicitante = datos_ctg.get("usuarioSolicitante")
585
+ self.UsuarioReal = datos_ctg.get("usuarioReal")
586
+ self.CtcCodigo = datos_ctg.get("ctcCodigo")
587
+ self.Turno = datos_ctg.get("turno")
588
+ return self.NumeroCTG
589
+ else:
590
+ return ""
591
+
592
+ @inicializar_y_capturar_excepciones
593
+ def ConsultarCTGExcel(self, numero_carta_de_porte=None, numero_ctg=None,
594
+ patente=None, cuit_solicitante=None, cuit_destino=None,
595
+ fecha_emision_desde=None, fecha_emision_hasta=None,
596
+ archivo="planilla.xls"):
597
+ "OperaciΓ³n que realiza consulta de CTGs, graba una planilla xls"
598
+ ret = self.client.consultarCTGExcel(request=dict(
599
+ auth={
600
+ 'token': self.Token, 'sign': self.Sign,
601
+ 'cuitRepresentado': self.Cuit, },
602
+ consultarCTGDatos=dict(
603
+ cartaPorte=numero_carta_de_porte,
604
+ ctg=numero_ctg,
605
+ patente=patente,
606
+ cuitSolicitante=cuit_solicitante,
607
+ cuitDestino=cuit_destino,
608
+ fechaEmisionDesde=fecha_emision_desde,
609
+ fechaEmisionHasta=fecha_emision_hasta,
610
+ )))['response']
611
+ self.__analizar_errores(ret)
612
+ datos = base64.b64decode(ret.get('archivo') or "")
613
+ f = open(archivo, "wb")
614
+ f.write(datos)
615
+ f.close()
616
+ return True
617
+
618
+ @inicializar_y_capturar_excepciones
619
+ def ConsultarDetalleCTG(self, numero_ctg=None):
620
+ "OperaciΓ³n mostrar este detalle de la solicitud de CTG seleccionada."
621
+ ret = self.client.consultarDetalleCTG(request=dict(
622
+ auth={
623
+ 'token': self.Token, 'sign': self.Sign,
624
+ 'cuitRepresentado': self.Cuit, },
625
+ ctg=numero_ctg,
626
+ ))['response']
627
+ self.__analizar_errores(ret)
628
+ datos = ret.get('consultarDetalleCTGDatos')
629
+ if datos:
630
+ self.NumeroCTG = str(datos['ctg'])
631
+ self.CartaPorte = str(datos['cartaPorte'])
632
+ self.Estado = str(datos['estado'])
633
+ self.FechaHora = str(datos['fechaEmision'])
634
+ self.VigenciaDesde = str(datos['fechaVigenciaDesde'])
635
+ self.VigenciaHasta = str(datos['fechaVigenciaHasta'])
636
+ self.TarifaReferencia = str(datos['tarifaReferencia'])
637
+ self.Detalle = str(datos.get('detalle', ""))
638
+ return True
639
+
640
+ @inicializar_y_capturar_excepciones
641
+ def ConsultarConstanciaCTGPDF(self, numero_ctg=None,
642
+ archivo="constancia.pdf"):
643
+ "OperaciΓ³n Consultar Constancia de CTG en PDF"
644
+ ret = self.client.consultarConstanciaCTGPDF(request=dict(
645
+ auth={
646
+ 'token': self.Token, 'sign': self.Sign,
647
+ 'cuitRepresentado': self.Cuit, },
648
+ ctg=numero_ctg,
649
+ ))['response']
650
+ self.__analizar_errores(ret)
651
+ datos = base64.b64decode(ret.get('archivo', ""))
652
+ f = open(archivo, "wb")
653
+ f.write(datos)
654
+ f.close()
655
+ return True
656
+
657
+ @inicializar_y_capturar_excepciones
658
+ def ConsultarProvincias(self, sep="||"):
659
+ ret = self.client.consultarProvincias(request=dict(
660
+ auth={
661
+ 'token': self.Token, 'sign': self.Sign,
662
+ 'cuitRepresentado': self.Cuit, },
663
+ ))['consultarProvinciasResponse']
664
+ self.__analizar_errores(ret)
665
+ array = ret.get('arrayProvincias', [])
666
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
667
+ (it['provincia']['codigo'],
668
+ it['provincia']['descripcion'])
669
+ for it in array]
670
+
671
+ @inicializar_y_capturar_excepciones
672
+ def ConsultarLocalidadesPorProvincia(self, codigo_provincia, sep="||"):
673
+ ret = self.client.consultarLocalidadesPorProvincia(request=dict(
674
+ auth={
675
+ 'token': self.Token, 'sign': self.Sign,
676
+ 'cuitRepresentado': self.Cuit, },
677
+ codigoProvincia=codigo_provincia,
678
+ ))['response']
679
+ self.__analizar_errores(ret)
680
+ array = ret.get('arrayLocalidades', [])
681
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
682
+ (it['localidad']['codigo'],
683
+ it['localidad']['descripcion'])
684
+ for it in array]
685
+
686
+ @inicializar_y_capturar_excepciones
687
+ def ConsultarEstablecimientos(self, sep="||"):
688
+ ret = self.client.consultarEstablecimientos(request=dict(
689
+ auth={
690
+ 'token': self.Token, 'sign': self.Sign,
691
+ 'cuitRepresentado': self.Cuit, },
692
+ ))['response']
693
+ self.__analizar_errores(ret)
694
+ array = ret.get('arrayEstablecimientos', [])
695
+ return [("%s" %
696
+ (it['establecimiento'],))
697
+ for it in array]
698
+
699
+ @inicializar_y_capturar_excepciones
700
+ def ConsultarEspecies(self, sep="||"):
701
+ ret = self.client.consultarEspecies(request=dict(
702
+ auth={
703
+ 'token': self.Token, 'sign': self.Sign,
704
+ 'cuitRepresentado': self.Cuit, },
705
+ ))['response']
706
+ self.__analizar_errores(ret)
707
+ array = ret.get('arrayEspecies', [])
708
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
709
+ (it['especie']['codigo'],
710
+ it['especie']['descripcion'])
711
+ for it in array]
712
+
713
+ @inicializar_y_capturar_excepciones
714
+ def ConsultarCosechas(self, sep="||"):
715
+ ret = self.client.consultarCosechas(request=dict(
716
+ auth={
717
+ 'token': self.Token, 'sign': self.Sign,
718
+ 'cuitRepresentado': self.Cuit, },
719
+ ))['response']
720
+ self.__analizar_errores(ret)
721
+ array = ret.get('arrayCosechas', [])
722
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
723
+ (it['cosecha']['codigo'],
724
+ it['cosecha']['descripcion'])
725
+ for it in array]
726
+
727
+
728
+ def leer_archivo(nombre_archivo):
729
+ archivo = open(nombre_archivo, "r")
730
+ items = []
731
+ ext = os.path.splitext(nombre_archivo)[1]
732
+ if ext == '.csv':
733
+ csv_reader = csv.reader(open(ENTRADA), dialect='excel', delimiter=";")
734
+ for row in csv_reader:
735
+ items.append(row)
736
+ cols = [str(it).strip() for it in items[0]]
737
+ # armar diccionario por cada linea
738
+ items = [dict([(cols[i], str(v).strip()) for i, v in enumerate(item)]) for item in items[1:]]
739
+ return cols, items
740
+ elif ext == '.json':
741
+ items = json.load(archivo)
742
+ elif ext == '.dbf':
743
+ dic = {}
744
+ formatos = [('Encabezado', ENCABEZADO, dic), ]
745
+ leer_dbf(formatos, conf_dbf)
746
+ items = [dic]
747
+ elif ext == '.txt':
748
+ dic = {}
749
+ for linea in archivo:
750
+ if str(linea[0]) == '0':
751
+ dic.update(leer(linea, ENCABEZADO))
752
+ else:
753
+ print("Tipo de registro incorrecto:", linea[0])
754
+ items.append(dic)
755
+ else:
756
+ raise RuntimeError("Extension de archivo desconocida: %s" % ext)
757
+ archivo.close()
758
+ cols = [k[0] for k in ENCABEZADO]
759
+ return cols, items
760
+
761
+
762
+ def escribir_archivo(cols, items, nombre_archivo, agrega=False):
763
+ archivo = open(nombre_archivo, agrega and "a" or "w")
764
+ ext = os.path.splitext(nombre_archivo)[1]
765
+ if ext == '.csv':
766
+ csv_writer = csv.writer(archivo, dialect='excel', delimiter=";")
767
+ csv_writer.writerows([cols])
768
+ csv_writer.writerows([[item[k] for k in cols] for item in items])
769
+ elif ext == '.json':
770
+ json.dump(items, archivo, sort_keys=True, indent=4)
771
+ elif ext == '.dbf':
772
+ formatos = [('Encabezado', ENCABEZADO, items), ]
773
+ guardar_dbf(formatos, True, conf_dbf)
774
+ elif ext == '.txt':
775
+ for dic in items:
776
+ dic['tipo_reg'] = 0
777
+ archivo.write(escribir(dic, ENCABEZADO))
778
+ else:
779
+ raise RuntimeError("Extension de archivo desconocida: %s" % ext)
780
+ archivo.close()
781
+
782
+
783
+ class WSCTGv2(BaseWS):
784
+ _reg_progid_ = "WSCTGv2"
785
+ _reg_clsid_ = "{ACDEFB8A-34E1-48CF-94E8-6AF6ADA0717A}"
786
+
787
+
788
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
789
+ if not hasattr(sys, "frozen"):
790
+ basepath = __file__
791
+ elif sys.frozen == 'dll':
792
+ import win32api
793
+ basepath = win32api.GetModuleFileName(sys.frozendllhandle)
794
+ else:
795
+ basepath = sys.executable
796
+ INSTALL_DIR = WSCTG.InstallDir = WSCTGv2.InstallDir = get_install_dir()
797
+
798
+
799
+ if __name__ == '__main__':
800
+ if '--ayuda' in sys.argv:
801
+ print(LICENCIA)
802
+ print(AYUDA)
803
+ sys.exit(0)
804
+ if '--formato' in sys.argv:
805
+ print("Formato:")
806
+ for msg, formato in [('Encabezado', ENCABEZADO), ]:
807
+ comienzo = 1
808
+ print("=== %s ===" % msg)
809
+ for fmt in formato:
810
+ clave, longitud, tipo = fmt[0:3]
811
+ dec = len(fmt) > 3 and fmt[3] or (tipo == 'I' and '2' or '')
812
+ print(" * Campo: %-20s PosiciΓ³n: %3d Longitud: %4d Tipo: %s Decimales: %s" % (
813
+ clave, comienzo, longitud, tipo, dec))
814
+ comienzo += longitud
815
+ sys.exit(0)
816
+
817
+ if "--register" in sys.argv or "--unregister" in sys.argv:
818
+ import win32com.server.register
819
+ win32com.server.register.UseCommandLine(WSCTG)
820
+ # Compatibilidad hacia atrΓ‘s:
821
+ win32com.server.register.UseCommandLine(WSCTGv2)
822
+ sys.exit(0)
823
+
824
+ import csv
825
+ from configparser import SafeConfigParser
826
+
827
+ try:
828
+
829
+ if "--version" in sys.argv:
830
+ print("VersiΓ³n: ", __version__)
831
+
832
+ for arg in sys.argv[1:]:
833
+ if arg.startswith("--"):
834
+ break
835
+ print("Usando configuraciΓ³n:", arg)
836
+ CONFIG_FILE = arg
837
+
838
+ config = SafeConfigParser()
839
+ config.read(CONFIG_FILE)
840
+ CERT = config.get('WSAA', 'CERT')
841
+ PRIVATEKEY = config.get('WSAA', 'PRIVATEKEY')
842
+ CUIT = config.get('WSCTG', 'CUIT')
843
+ ENTRADA = config.get('WSCTG', 'ENTRADA')
844
+ SALIDA = config.get('WSCTG', 'SALIDA')
845
+
846
+ if config.has_option('WSAA', 'URL') and not HOMO:
847
+ wsaa_url = config.get('WSAA', 'URL')
848
+ else:
849
+ wsaa_url = None
850
+ if config.has_option('WSCTG', 'URL') and not HOMO:
851
+ wsctg_url = config.get('WSCTG', 'URL')
852
+ else:
853
+ wsctg_url = WSDL
854
+
855
+ if config.has_section('DBF'):
856
+ conf_dbf = dict(config.items('DBF'))
857
+ if DEBUG:
858
+ print("conf_dbf", conf_dbf)
859
+ else:
860
+ conf_dbf = {}
861
+
862
+ DEBUG = '--debug' in sys.argv
863
+ XML = '--xml' in sys.argv
864
+
865
+ if DEBUG:
866
+ print("Usando ConfiguraciΓ³n:")
867
+ print("wsaa_url:", wsaa_url)
868
+ print("wsctg_url:", wsctg_url)
869
+
870
+ # obteniendo el TA
871
+ from .wsaa import WSAA
872
+ wsaa = WSAA()
873
+ ta = wsaa.Autenticar("wsctg", CERT, PRIVATEKEY, wsaa_url, debug=DEBUG)
874
+ if not ta:
875
+ sys.exit("Imposible autenticar con WSAA: %s" % wsaa.Excepcion)
876
+
877
+ # cliente soap del web service
878
+ wsctg = WSCTG()
879
+ wsctg.Conectar(wsdl=wsctg_url)
880
+ wsctg.SetTicketAcceso(ta)
881
+ wsctg.Cuit = CUIT
882
+
883
+ if '--dummy' in sys.argv:
884
+ ret = wsctg.Dummy()
885
+ print("AppServerStatus", wsctg.AppServerStatus)
886
+ print("DbServerStatus", wsctg.DbServerStatus)
887
+ print("AuthServerStatus", wsctg.AuthServerStatus)
888
+ sys.exit(0)
889
+
890
+ if '--anular' in sys.argv:
891
+ i = sys.argv.index("--anular")
892
+ # print wsctg.client.help("anularCTG")
893
+ if i + 2 > len(sys.argv) or sys.argv[i + 1].startswith("--"):
894
+ carta_porte = input("Ingrese Carta de Porte: ")
895
+ ctg = input("Ingrese CTG: ")
896
+ else:
897
+ carta_porte = sys.argv[i + 1]
898
+ ctg = sys.argv[i + 2]
899
+ ret = wsctg.AnularCTG(carta_porte, ctg)
900
+ print("Carta Porte", wsctg.CartaPorte)
901
+ print("Numero CTG", wsctg.NumeroCTG)
902
+ print("Fecha y Hora", wsctg.FechaHora)
903
+ print("Codigo Anulacion de CTG", wsctg.CodigoOperacion)
904
+ print("Errores:", wsctg.Errores)
905
+ sys.exit(0)
906
+
907
+ if '--rechazar' in sys.argv:
908
+ i = sys.argv.index("--rechazar")
909
+ # print wsctg.client.help("rechazarCTG")
910
+ if i + 3 > len(sys.argv) or sys.argv[i + 1].startswith("--"):
911
+ carta_porte = input("Ingrese Carta de Porte: ")
912
+ ctg = input("Ingrese CTG: ")
913
+ motivo = input("Motivo: ")
914
+ else:
915
+ carta_porte = sys.argv[i + 1]
916
+ ctg = sys.argv[i + 2]
917
+ motivo = sys.argv[i + 3]
918
+ ret = wsctg.RechazarCTG(carta_porte, ctg, motivo)
919
+ print("Carta Porte", wsctg.CartaPorte)
920
+ print("Numero CTG", wsctg.NumeroCTG)
921
+ print("Fecha y Hora", wsctg.FechaHora)
922
+ print("Codigo Anulacion de CTG", wsctg.CodigoOperacion)
923
+ print("Errores:", wsctg.Errores)
924
+ sys.exit(0)
925
+
926
+ # Recuperar parΓ‘metros:
927
+
928
+ if '--provincias' in sys.argv:
929
+ ret = wsctg.ConsultarProvincias()
930
+ print("\n".join(ret))
931
+
932
+ if '--localidades' in sys.argv:
933
+ ret = wsctg.ConsultarLocalidadesPorProvincia(16)
934
+ print("\n".join(ret))
935
+
936
+ if '--especies' in sys.argv:
937
+ ret = wsctg.ConsultarEspecies()
938
+ print("\n".join(ret))
939
+
940
+ if '--cosechas' in sys.argv:
941
+ ret = wsctg.ConsultarCosechas()
942
+ print("\n".join(ret))
943
+
944
+ if '--establecimientos' in sys.argv:
945
+ ret = wsctg.ConsultarEstablecimientos()
946
+ print("\n".join(ret))
947
+
948
+ if '--prueba' in sys.argv or '--formato' in sys.argv:
949
+ prueba = dict(numero_carta_de_porte=512345679, codigo_especie=23,
950
+ cuit_canjeador=0, # 30660685908,
951
+ cuit_destino=20111111112, cuit_destinatario=20222222223,
952
+ codigo_localidad_origen=3058, codigo_localidad_destino=3059,
953
+ codigo_cosecha='1314', peso_neto_carga=1000,
954
+ km_a_recorrer=1234,
955
+ observaciones='', establecimiento=1,
956
+ )
957
+ if [argv for argv in sys.argv if argv.startswith(("--confirmar",
958
+ "--regresar", '--cambiar'))]:
959
+ prueba.update(dict(
960
+ numero_ctg="49241727", transaccion='10000001681',
961
+ consumo_propio='S',
962
+ ))
963
+ parcial = dict(
964
+ cant_horas=1,
965
+ patente_vehiculo='APE652', cuit_transportista=20333333334,
966
+ )
967
+ if not '--parcial' in sys.argv:
968
+ prueba.update(parcial)
969
+
970
+ escribir_archivo(list(prueba.keys()), [prueba], ENTRADA)
971
+
972
+ cols, items = leer_archivo(ENTRADA)
973
+ ctg = None
974
+
975
+ if '--solicitar' in sys.argv:
976
+ wsctg.LanzarExcepciones = True
977
+ for it in items:
978
+ print("solicitando...", ' '.join(['%s=%s' % (k, v) for k, v in list(it.items())]))
979
+ ctg = wsctg.SolicitarCTGInicial(**it)
980
+ print("numero CTG: ", ctg)
981
+ print("Observiacion: ", wsctg.Observaciones)
982
+ print("Carta Porte", wsctg.CartaPorte)
983
+ print("Numero CTG", wsctg.NumeroCTG)
984
+ print("Fecha y Hora", wsctg.FechaHora)
985
+ print("Vigencia Desde", wsctg.VigenciaDesde)
986
+ print("Vigencia Hasta", wsctg.VigenciaHasta)
987
+ print("Tarifa Referencia: ", wsctg.TarifaReferencia)
988
+ print("Errores:", wsctg.Errores)
989
+ print("Controles:", wsctg.Controles)
990
+ it['numero_ctg'] = wsctg.NumeroCTG
991
+ it['tarifa_referencia'] = wsctg.TarifaReferencia
992
+ it['observaciones'] = wsctg.Observaciones
993
+ it['fecha_hora'] = wsctg.FechaHora
994
+ it['vigencia_desde'] = wsctg.VigenciaDesde
995
+ it['vigencia_hasta'] = wsctg.VigenciaHasta
996
+ it['errores'] = '|'.join(wsctg.Errores)
997
+ it['controles'] = '|'.join(wsctg.Controles)
998
+
999
+ if '--parcial' in sys.argv:
1000
+ wsctg.LanzarExcepciones = True
1001
+ for it in items:
1002
+ print("solicitando dato pendiente...", ' '.join(['%s=%s' % (k, v) for k, v in list(parcial.items())]))
1003
+ ctg = wsctg.SolicitarCTGDatoPendiente(
1004
+ numero_carta_de_porte=wsctg.CartaPorte,
1005
+ **parcial)
1006
+ print("numero CTG: ", ctg)
1007
+ print("Observiacion: ", wsctg.Observaciones)
1008
+ print("Carta Porte", wsctg.CartaPorte)
1009
+ print("Numero CTG", wsctg.NumeroCTG)
1010
+ print("Fecha y Hora", wsctg.FechaHora)
1011
+ print("Vigencia Desde", wsctg.VigenciaDesde)
1012
+ print("Vigencia Hasta", wsctg.VigenciaHasta)
1013
+ print("Tarifa Referencia: ", wsctg.TarifaReferencia)
1014
+ print("Errores:", wsctg.Errores)
1015
+ print("Controles:", wsctg.Controles)
1016
+ it['numero_ctg'] = wsctg.NumeroCTG
1017
+ it['tarifa_referencia'] = wsctg.TarifaReferencia
1018
+ it['errores'] = '|'.join(wsctg.Errores)
1019
+ it['controles'] = '|'.join(wsctg.Controles)
1020
+
1021
+ if '--confirmar_arribo' in sys.argv:
1022
+ for it in items:
1023
+ print("confirmando...", ' '.join(['%s=%s' % (k, v) for k, v in list(it.items())]))
1024
+ transaccion = wsctg.ConfirmarArribo(**it)
1025
+ print("transaccion: %s" % (transaccion, ))
1026
+ print("Fecha y Hora", wsctg.FechaHora)
1027
+ print("Errores:", wsctg.Errores)
1028
+ it['transaccion'] = transaccion
1029
+ it['errores'] = '|'.join(wsctg.Errores)
1030
+ it['controles'] = '|'.join(wsctg.Controles)
1031
+
1032
+ if '--confirmar_definitivo' in sys.argv:
1033
+ if '--testing' in sys.argv:
1034
+ wsctg.LoadTestXML("wsctg_confirmar_def.xml") # cargo respuesta
1035
+ for it in items:
1036
+ print("confirmando...", ' '.join(['%s=%s' % (k, v) for k, v in list(it.items())]))
1037
+ transaccion = wsctg.ConfirmarDefinitivo(**it)
1038
+ print("transaccion: %s" % (transaccion, ))
1039
+ print("Fecha y Hora", wsctg.FechaHora)
1040
+ print("Errores:", wsctg.Errores)
1041
+ it['transaccion'] = transaccion
1042
+ it['errores'] = '|'.join(wsctg.Errores)
1043
+ it['controles'] = '|'.join(wsctg.Errores)
1044
+
1045
+ if '--regresar_a_origen_rechazado' in sys.argv:
1046
+ for it in items:
1047
+ print("regresando...", ' '.join(['%s=%s' % (k, v) for k, v in list(it.items())]))
1048
+ transaccion = wsctg.RegresarAOrigenCTGRechazado(**it)
1049
+ print("transaccion: %s" % (transaccion, ))
1050
+ print("Fecha y Hora", wsctg.FechaHora)
1051
+ print("Errores:", wsctg.Errores)
1052
+ it['transaccion'] = transaccion
1053
+ it['errores'] = '|'.join(wsctg.Errores)
1054
+ it['controles'] = '|'.join(wsctg.Errores)
1055
+
1056
+ if '--cambiar_destino_destinatario_rechazado' in sys.argv:
1057
+ for it in items:
1058
+ print("cambiando...", ' '.join(['%s=%s' % (k, v) for k, v in list(it.items())]))
1059
+ transaccion = wsctg.CambiarDestinoDestinatarioCTGRechazado(**it)
1060
+ print("transaccion: %s" % (transaccion, ))
1061
+ print("Fecha y Hora", wsctg.FechaHora)
1062
+ print("Errores:", wsctg.Errores)
1063
+ it['transaccion'] = transaccion
1064
+ it['errores'] = '|'.join(wsctg.Errores)
1065
+ it['controles'] = '|'.join(wsctg.Errores)
1066
+
1067
+ if '--consultar_detalle' in sys.argv:
1068
+ i = sys.argv.index("--consultar_detalle")
1069
+ if len(sys.argv) > i + 1 and not sys.argv[i + 1].startswith("--"):
1070
+ ctg = int(sys.argv[i + 1])
1071
+ elif not ctg:
1072
+ ctg = int(input("Numero de CTG: ") or '0') or 73714620
1073
+
1074
+ wsctg.LanzarExcepciones = True
1075
+ for i, it in enumerate(items):
1076
+ print("consultando detalle...", ctg)
1077
+ ok = wsctg.ConsultarDetalleCTG(ctg)
1078
+ print("Numero CTG: ", wsctg.NumeroCTG)
1079
+ print("Tarifa Referencia: ", wsctg.TarifaReferencia)
1080
+ print("Observiacion: ", wsctg.Observaciones)
1081
+ print("Carta Porte", wsctg.CartaPorte)
1082
+ print("Numero CTG", wsctg.NumeroCTG)
1083
+ print("Fecha y Hora", wsctg.FechaHora)
1084
+ print("Vigencia Desde", wsctg.VigenciaDesde)
1085
+ print("Vigencia Hasta", wsctg.VigenciaHasta)
1086
+ print("Errores:", wsctg.Errores)
1087
+ print("Controles:", wsctg.Controles)
1088
+ print("Detalle:", wsctg.Detalle)
1089
+ it['numero_ctg'] = wsctg.NumeroCTG
1090
+ it['observaciones'] = wsctg.Observaciones
1091
+ it['fecha_hora'] = wsctg.FechaHora
1092
+ it['vigencia_desde'] = wsctg.VigenciaDesde
1093
+ it['vigencia_hasta'] = wsctg.VigenciaHasta
1094
+ wsctg.AnalizarXml("XmlResponse")
1095
+ for k, ki in list({'ctg': 'numero_ctg', 'solicitante': '',
1096
+ 'estado': 'estado',
1097
+ 'especie': '', # 'codigo_especie', no devuelve codigo!
1098
+ 'cosecha': '', # 'codigo_cosecha', no devuelve codigo!
1099
+ 'cuitCanjeador': 'cuit_canjeador',
1100
+ 'cuitDestino': 'cuit_destino',
1101
+ 'cuitDestinatario': 'cuit_destinatario',
1102
+ 'cuitTransportista': 'cuit_transportista',
1103
+ 'establecimiento': 'establecimiento',
1104
+ 'localidadOrigen': 'localidad_origen',
1105
+ 'localidadDestino': 'localidad_destino',
1106
+ 'cantidadHoras': 'cantidad_horas',
1107
+ 'patenteVehiculo': 'patente_vehiculo',
1108
+ 'pesoNetoCarga': 'peso_neto_carga',
1109
+ 'kmRecorridos': 'km_recorridos',
1110
+ 'tarifaReferencia': 'tarifa_referencia',
1111
+ 'ctcCodigo': 'ctc_codigo',
1112
+ 'turno': 'turno',
1113
+ }.items()):
1114
+ v = wsctg.ObtenerTagXml('consultarDetalleCTGDatos', k)
1115
+ print(k, v)
1116
+ if ki.startswith("cuit") and v:
1117
+ v = v[:11]
1118
+ it[ki] = v
1119
+
1120
+ escribir_archivo(cols, items, SALIDA)
1121
+
1122
+ if "--consultar" in sys.argv:
1123
+ wsctg.LanzarExcepciones = True
1124
+ wsctg.ConsultarCTG(fecha_emision_desde="01/04/2012")
1125
+ print("Numero CTG - Carta de Porte - Imprime Constancia - Estado")
1126
+ while wsctg.LeerDatosCTG():
1127
+ print(wsctg.NumeroCTG, wsctg.CartaPorte, end=' ')
1128
+ print(wsctg.ImprimeConstancia, wsctg.Estado, wsctg.FechaHora)
1129
+
1130
+ if "--consultar_rechazados" in sys.argv:
1131
+ wsctg.LanzarExcepciones = True
1132
+ wsctg.ConsultarCTGRechazados()
1133
+ print("Numero CTG - Carta de Porte - Fecha - Destino/Dest./Obs.")
1134
+ while wsctg.LeerDatosCTG():
1135
+ print(wsctg.NumeroCTG, wsctg.CartaPorte, wsctg.FechaHora, end=' ')
1136
+ print(wsctg.Destino, wsctg.Destinatario, wstcg.Observaciones)
1137
+
1138
+ if "--consultar_activos_por_patente" in sys.argv:
1139
+ i = sys.argv.index("--consultar_activos_por_patente")
1140
+ if len(sys.argv) > i + 1 and not sys.argv[i + 1].startswith("--"):
1141
+ patente = sys.argv[i + 1]
1142
+ elif not ctg:
1143
+ patente = input("Patente: ") or 'APE652'
1144
+ wsctg.LanzarExcepciones = True
1145
+ if '--testing' in sys.argv:
1146
+ wsctg.LoadTestXML("wsctgv2_activos.xml")
1147
+ wsctg.ConsultarCTGActivosPorPatente(patente=patente)
1148
+ print("Numero CTG - Carta de Porte - Fecha - Peso Neto - Usuario")
1149
+ while wsctg.LeerDatosCTG():
1150
+ print(wsctg.NumeroCTG, wsctg.CartaPorte, wsctg.Patente, end=' ')
1151
+ print(wsctg.FechaHora, wsctg.FechaVencimiento, wsctg.PesoNeto, end=' ')
1152
+ print(wsctg.UsuarioSolicitante, wsctg.UsuarioReal)
1153
+
1154
+ if '--consultar_excel' in sys.argv:
1155
+ archivo = input("Archivo a generar (planilla.xls): ") or \
1156
+ 'planilla.xls'
1157
+ wsctg.LanzarExcepciones = True
1158
+ ok = wsctg.ConsultarCTGExcel(fecha_emision_desde="01/04/2012",
1159
+ archivo=archivo)
1160
+ print("Errores:", wsctg.Errores)
1161
+
1162
+ if '--consultar_constancia_pdf' in sys.argv:
1163
+ i = sys.argv.index("--consultar_constancia_pdf")
1164
+ if len(sys.argv) > i + 2 and not sys.argv[i + 1].startswith("--"):
1165
+ ctg = int(sys.argv[i + 1])
1166
+ archivo = sys.argv[i + 2]
1167
+ elif not ctg:
1168
+ ctg = int(input("Numero de CTG: ") or '0') or 83139794
1169
+ archivo = input("Archivo a generar (constancia.pdf): ") or \
1170
+ 'constancia.pdf'
1171
+
1172
+ wsctg.LanzarExcepciones = True
1173
+ ok = wsctg.ConsultarConstanciaCTGPDF(ctg, archivo)
1174
+ print("Errores:", wsctg.Errores)
1175
+
1176
+ if "--pendientes" in sys.argv:
1177
+ wsctg.LanzarExcepciones = True
1178
+ wsctg.CTGsPendientesResolucion()
1179
+ for clave in ("arrayCTGsRechazadosAResolver",
1180
+ "arrayCTGsOtorgadosAResolver",
1181
+ "arrayCTGsConfirmadosAResolver", ):
1182
+ print(clave[6:])
1183
+ print("Numero CTG - Carta de Porte - Imprime Constancia - Estado")
1184
+ while wsctg.LeerDatosCTG(clave):
1185
+ print(wsctg.NumeroCTG, wsctg.CartaPorte, wsctg.FechaHora)
1186
+ print(wsctg.Destino, wsctg.Destinatario, wsctg.Observaciones)
1187
+
1188
+ print("hecho.")
1189
+
1190
+ except SoapFault as e:
1191
+ print("Falla SOAP:", e.faultcode, e.faultstring.encode("ascii", "ignore"))
1192
+ sys.exit(3)
1193
+ except Exception as e:
1194
+ ex = utils.exception_info()
1195
+ print(ex)
1196
+ if DEBUG:
1197
+ raise
1198
+ sys.exit(5)
app/pyafipws/wsfev1.py ADDED
@@ -0,0 +1,1349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para obtener CAE/CAEA, cοΏ½digo de autorizaciοΏ½n electrοΏ½nico webservice
14
+ WSFEv1 de AFIP (Factura ElectrοΏ½nica Nacional - Proyecto Version 1 - 2.10)
15
+ SegοΏ½n RG 2485/08, RG 2757/2010, RG 2904/2010 y RG2926/10 (CAE anticipado),
16
+ RG 3067/2011 (RS - Monotributo), RG 3571/2013 (Responsables inscriptos IVA),
17
+ RG 3668/2014 (Factura A IVA F.8001), RG 3749/2015 (R.I. y exentos)
18
+ RG 4004-E Alquiler de inmuebles con destino casa habitaciοΏ½n).
19
+ RG 4109-E Venta de bienes muebles registrables.
20
+ RG 4291/2018 RοΏ½gimen especial de emisiοΏ½n y almacenamiento electrοΏ½nico
21
+ MοΏ½s info: http://www.sistemasagiles.com.ar/trac/wiki/ProyectoWSFEv1
22
+ """
23
+
24
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
25
+ __copyright__ = "Copyright (C) 2010-2017 Mariano Reingart"
26
+ __license__ = "GPL 3.0"
27
+ __version__ = "1.22b"
28
+
29
+ import datetime
30
+ import decimal
31
+ import os
32
+ import sys
33
+ from .utils import verifica, inicializar_y_capturar_excepciones, BaseWS, get_install_dir
34
+
35
+ HOMO = False # solo homologaciοΏ½n
36
+ TYPELIB = False # usar librerοΏ½a de tipos (TLB)
37
+ LANZAR_EXCEPCIONES = False # valor por defecto: True
38
+
39
+ #WSDL = "https://www.sistemasagiles.com.ar/simulador/wsfev1/call/soap?WSDL=None"
40
+ WSDL = "https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL"
41
+ #WSDL = "file:///home/reingart/tmp/service.asmx.xml"
42
+
43
+
44
+ class WSFEv1(BaseWS):
45
+ "Interfaz para el WebService de Factura ElectrοΏ½nica Version 1 - 2.10"
46
+ _public_methods_ = ['CrearFactura', 'AgregarIva', 'CAESolicitar',
47
+ 'AgregarTributo', 'AgregarCmpAsoc', 'AgregarOpcional',
48
+ 'AgregarComprador',
49
+ 'CompUltimoAutorizado', 'CompConsultar',
50
+ 'CAEASolicitar', 'CAEAConsultar', 'CAEARegInformativo',
51
+ 'CAEASinMovimientoInformar',
52
+ 'CAESolicitarX', 'CompTotXRequest',
53
+ 'IniciarFacturasX', 'AgregarFacturaX', 'LeerFacturaX',
54
+ 'ParamGetTiposCbte',
55
+ 'ParamGetTiposConcepto',
56
+ 'ParamGetTiposDoc',
57
+ 'ParamGetTiposIva',
58
+ 'ParamGetTiposMonedas',
59
+ 'ParamGetTiposOpcional',
60
+ 'ParamGetTiposTributos',
61
+ 'ParamGetTiposPaises',
62
+ 'ParamGetCotizacion',
63
+ 'ParamGetPtosVenta',
64
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
65
+ 'SetParametros', 'SetTicketAcceso', 'GetParametro',
66
+ 'EstablecerCampoFactura', 'ObtenerCampoFactura',
67
+ 'Dummy', 'Conectar', 'DebugLog', 'SetTicketAcceso']
68
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
69
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
70
+ 'XmlRequest', 'XmlResponse', 'Version', 'Excepcion', 'LanzarExcepciones',
71
+ 'Resultado', 'Obs', 'Observaciones', 'Traceback', 'InstallDir',
72
+ 'CAE', 'Vencimiento', 'Eventos', 'Errores', 'ErrCode', 'ErrMsg',
73
+ 'Reprocesar', 'Reproceso', 'EmisionTipo', 'CAEA',
74
+ 'CbteNro', 'CbtDesde', 'CbtHasta', 'FechaCbte',
75
+ 'ImpTotal', 'ImpNeto', 'ImptoLiq',
76
+ 'ImpIVA', 'ImpOpEx', 'ImpTrib', 'FchCotiz',]
77
+
78
+ _reg_progid_ = "WSFEv1"
79
+ _reg_clsid_ = "{CA0E604D-E3D7-493A-8880-F6CDD604185E}"
80
+
81
+ if TYPELIB:
82
+ _typelib_guid_ = '{B1D7283C-3EC2-463E-89B4-11F5228E2A15}'
83
+ _typelib_version_ = 1, 18
84
+ _com_interfaces_ = ['IWSFEv1']
85
+ ##_reg_class_spec_ = "wsfev1.WSFEv1"
86
+
87
+ # Variables globales para BaseWS:
88
+ HOMO = HOMO
89
+ WSDL = WSDL
90
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
91
+ Reprocesar = True # recuperar automaticamente CAE emitidos
92
+ LanzarExcepciones = LANZAR_EXCEPCIONES
93
+ factura = None
94
+ facturas = None
95
+
96
+ def inicializar(self):
97
+ BaseWS.inicializar(self)
98
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
99
+ self.Resultado = self.Motivo = self.Reproceso = ''
100
+ self.LastID = self.LastCMP = self.CAE = self.CAEA = self.Vencimiento = ''
101
+ self.CbteNro = self.CbtDesde = self.CbtHasta = self.PuntoVenta = None
102
+ self.ImpTotal = self.ImpIVA = self.ImpOpEx = self.ImpNeto = self.ImptoLiq = self.ImpTrib = None
103
+ self.EmisionTipo = self.Periodo = self.Orden = ""
104
+ self.FechaCbte = self.FchVigDesde = self.FchVigHasta = self.FchTopeInf = self.FchProceso = ""
105
+ self.FchCotiz = None
106
+
107
+ def __analizar_errores(self, ret):
108
+ "Comprueba y extrae errores si existen en la respuesta XML"
109
+ if 'Errors' in ret:
110
+ errores = ret['Errors']
111
+ for error in errores:
112
+ self.Errores.append("%s: %s" % (
113
+ error['Err']['Code'],
114
+ error['Err']['Msg'],
115
+ ))
116
+ self.ErrCode = ' '.join([str(error['Err']['Code']) for error in errores])
117
+ self.ErrMsg = '\n'.join(self.Errores)
118
+ if 'Events' in ret:
119
+ events = ret['Events']
120
+ self.Eventos = ['%s: %s' % (evt['Evt']['Code'], evt['Evt']['Msg']) for evt in events]
121
+
122
+ @inicializar_y_capturar_excepciones
123
+ def Dummy(self):
124
+ "Obtener el estado de los servidores de la AFIP"
125
+ result = self.client.FEDummy()['FEDummyResult']
126
+ self.AppServerStatus = result.get('AppServer')
127
+ self.DbServerStatus = result.get('DbServer')
128
+ self.AuthServerStatus = result.get('AuthServer')
129
+ return True
130
+
131
+ # los siguientes mοΏ½todos no estοΏ½n decorados para no limpiar propiedades
132
+
133
+ def CrearFactura(self, concepto=1, tipo_doc=80, nro_doc="", tipo_cbte=1, punto_vta=0,
134
+ cbt_desde=0, cbt_hasta=0, imp_total=0.00, imp_tot_conc=0.00, imp_neto=0.00,
135
+ imp_iva=0.00, imp_trib=0.00, imp_op_ex=0.00, fecha_cbte="", fecha_venc_pago=None,
136
+ fecha_serv_desde=None, fecha_serv_hasta=None, #--
137
+ moneda_id="PES", moneda_ctz="1.0000", caea=None, fecha_hs_gen=None, **kwargs
138
+ ):
139
+
140
+ "Creo un objeto factura (interna)"
141
+ # Creo una factura electronica de exportaciοΏ½n
142
+ fact = {'tipo_doc': tipo_doc, 'nro_doc': nro_doc,
143
+ 'tipo_cbte': tipo_cbte, 'punto_vta': punto_vta,
144
+ 'cbt_desde': cbt_desde, 'cbt_hasta': cbt_hasta,
145
+ 'imp_total': imp_total, 'imp_tot_conc': imp_tot_conc,
146
+ 'imp_neto': imp_neto, 'imp_iva': imp_iva,
147
+ 'imp_trib': imp_trib, 'imp_op_ex': imp_op_ex,
148
+ 'fecha_cbte': fecha_cbte,
149
+ 'fecha_venc_pago': fecha_venc_pago,
150
+ 'moneda_id': moneda_id, 'moneda_ctz': moneda_ctz,
151
+ 'concepto': concepto, 'fecha_hs_gen': fecha_hs_gen,
152
+ 'cbtes_asoc': [],
153
+ 'tributos': [],
154
+ 'iva': [],
155
+ 'opcionales': [],
156
+ 'compradores': [],
157
+ }
158
+ if fecha_serv_desde:
159
+ fact['fecha_serv_desde'] = fecha_serv_desde
160
+ if fecha_serv_hasta:
161
+ fact['fecha_serv_hasta'] = fecha_serv_hasta
162
+ if caea:
163
+ fact['caea'] = caea
164
+
165
+ self.factura = fact
166
+ return True
167
+
168
+ def EstablecerCampoFactura(self, campo, valor):
169
+ if campo in self.factura or campo in ('fecha_serv_desde', 'fecha_serv_hasta', 'caea', 'fch_venc_cae', 'fecha_hs_gen'):
170
+ self.factura[campo] = valor
171
+ return True
172
+ else:
173
+ return False
174
+
175
+ def AgregarCmpAsoc(self, tipo=1, pto_vta=0, nro=0, cuit=None, fecha=None, **kwarg):
176
+ "Agrego un comprobante asociado a una factura (interna)"
177
+ cmp_asoc = {'tipo': tipo, 'pto_vta': pto_vta, 'nro': nro}
178
+ if cuit is not None:
179
+ cmp_asoc['cuit'] = cuit
180
+ if fecha is not None:
181
+ cmp_asoc['fecha'] = fecha
182
+ self.factura['cbtes_asoc'].append(cmp_asoc)
183
+ return True
184
+
185
+ def AgregarTributo(self, tributo_id=0, desc="", base_imp=0.00, alic=0, importe=0.00, **kwarg):
186
+ "Agrego un tributo a una factura (interna)"
187
+ tributo = {'tributo_id': tributo_id, 'desc': desc, 'base_imp': base_imp,
188
+ 'alic': alic, 'importe': importe}
189
+ self.factura['tributos'].append(tributo)
190
+ return True
191
+
192
+ def AgregarIva(self, iva_id=0, base_imp=0.0, importe=0.0, **kwarg):
193
+ "Agrego un tributo a una factura (interna)"
194
+ iva = {'iva_id': iva_id, 'base_imp': base_imp, 'importe': importe}
195
+ self.factura['iva'].append(iva)
196
+ return True
197
+
198
+ def AgregarOpcional(self, opcional_id=0, valor="", **kwarg):
199
+ "Agrego un dato opcional a una factura (interna)"
200
+ op = {'opcional_id': opcional_id, 'valor': valor}
201
+ self.factura['opcionales'].append(op)
202
+ return True
203
+
204
+ def AgregarComprador(self, doc_tipo=80, doc_nro=0, porcentaje=100.00, **kwarg):
205
+ "Agrego un comprador a una factura (interna) RG 4109-E bienes muebles"
206
+ comp = {'doc_tipo': doc_tipo, 'doc_nro': doc_nro,
207
+ 'porcentaje': porcentaje}
208
+ self.factura['compradores'].append(comp)
209
+ return True
210
+
211
+ def ObtenerCampoFactura(self, *campos):
212
+ "Obtener el valor devuelto de AFIP para un campo de factura"
213
+ # cada campo puede ser una clave string (dict) o una posiciοΏ½n (list)
214
+ ret = self.factura
215
+ for campo in campos:
216
+ if isinstance(ret, dict) and isinstance(campo, str):
217
+ ret = ret.get(campo)
218
+ elif isinstance(ret, list) and len(ret) > campo:
219
+ ret = ret[campo]
220
+ else:
221
+ self.Excepcion = "El campo %s solicitado no existe" % campo
222
+ ret = None
223
+ if ret is None:
224
+ break
225
+ return str(ret)
226
+
227
+ # metodos principales para llamar remotamente a AFIP:
228
+
229
+ @inicializar_y_capturar_excepciones
230
+ def CAESolicitar(self):
231
+ f = self.factura
232
+ ret = self.client.FECAESolicitar(
233
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
234
+ FeCAEReq={
235
+ 'FeCabReq': {'CantReg': 1,
236
+ 'PtoVta': f['punto_vta'],
237
+ 'CbteTipo': f['tipo_cbte']},
238
+ 'FeDetReq': [{'FECAEDetRequest': {
239
+ 'Concepto': f['concepto'],
240
+ 'DocTipo': f['tipo_doc'],
241
+ 'DocNro': f['nro_doc'],
242
+ 'CbteDesde': f['cbt_desde'],
243
+ 'CbteHasta': f['cbt_hasta'],
244
+ 'CbteFch': f['fecha_cbte'],
245
+ 'ImpTotal': f['imp_total'],
246
+ 'ImpTotConc': f['imp_tot_conc'],
247
+ 'ImpNeto': f['imp_neto'],
248
+ 'ImpOpEx': f['imp_op_ex'],
249
+ 'ImpTrib': f['imp_trib'],
250
+ 'ImpIVA': f['imp_iva'],
251
+ # Fechas solo se informan si Concepto in (2,3)
252
+ 'FchServDesde': f.get('fecha_serv_desde'),
253
+ 'FchServHasta': f.get('fecha_serv_hasta'),
254
+ 'FchVtoPago': f.get('fecha_venc_pago'),
255
+ 'MonId': f['moneda_id'],
256
+ 'MonCotiz': f['moneda_ctz'],
257
+ 'CbtesAsoc': f['cbtes_asoc'] and [
258
+ {'CbteAsoc': {
259
+ 'Tipo': cbte_asoc['tipo'],
260
+ 'PtoVta': cbte_asoc['pto_vta'],
261
+ 'Nro': cbte_asoc['nro'],
262
+ 'Cuit': cbte_asoc.get('cuit'),
263
+ 'CbteFch': cbte_asoc.get('fecha'),
264
+ }}
265
+ for cbte_asoc in f['cbtes_asoc']] or None,
266
+ 'Tributos': f['tributos'] and [
267
+ {'Tributo': {
268
+ 'Id': tributo['tributo_id'],
269
+ 'Desc': tributo['desc'],
270
+ 'BaseImp': tributo['base_imp'],
271
+ 'Alic': tributo['alic'],
272
+ 'Importe': tributo['importe'],
273
+ }}
274
+ for tributo in f['tributos']] or None,
275
+ 'Iva': f['iva'] and [
276
+ {'AlicIva': {
277
+ 'Id': iva['iva_id'],
278
+ 'BaseImp': iva['base_imp'],
279
+ 'Importe': iva['importe'],
280
+ }}
281
+ for iva in f['iva']] or None,
282
+ 'Opcionales': [
283
+ {'Opcional': {
284
+ 'Id': opcional['opcional_id'],
285
+ 'Valor': opcional['valor'],
286
+ }} for opcional in f['opcionales']] or None,
287
+ 'Compradores': [
288
+ {'Comprador': {
289
+ 'DocTipo': comprador['doc_tipo'],
290
+ 'DocNro': comprador['doc_nro'],
291
+ 'Porcentaje': comprador['porcentaje'],
292
+ }} for comprador in f['compradores']] or None,
293
+ }
294
+ }]
295
+ })
296
+
297
+ result = ret['FECAESolicitarResult']
298
+ if 'FeCabResp' in result:
299
+ fecabresp = result['FeCabResp']
300
+ fedetresp = result['FeDetResp'][0]['FECAEDetResponse']
301
+
302
+ # Reprocesar en caso de error (recuperar CAE emitido anteriormente)
303
+ if self.Reprocesar and ('Errors' in result or 'Observaciones' in fedetresp):
304
+ for error in result.get('Errors', []) + fedetresp.get('Observaciones', []):
305
+ err_code = str(error.get('Err', error.get('Obs'))['Code'])
306
+ if fedetresp['Resultado'] == 'R' and err_code == '10016':
307
+ # guardo los mensajes xml originales
308
+ xml_request = self.client.xml_request
309
+ xml_response = self.client.xml_response
310
+ cae = self.CompConsultar(f['tipo_cbte'], f['punto_vta'], f['cbt_desde'], reproceso=True)
311
+ if cae and self.EmisionTipo == 'CAE':
312
+ self.Reproceso = 'S'
313
+ return cae
314
+ self.Reproceso = 'N'
315
+ # reestablesco los mensajes xml originales
316
+ self.client.xml_request = xml_request
317
+ self.client.xml_response = xml_response
318
+
319
+ self.Resultado = fecabresp['Resultado']
320
+ # Obs:
321
+ for obs in fedetresp.get('Observaciones', []):
322
+ self.Observaciones.append("%(Code)s: %(Msg)s" % (obs['Obs']))
323
+ self.Obs = '\n'.join(self.Observaciones)
324
+ self.CAE = fedetresp['CAE'] and str(fedetresp['CAE']) or ""
325
+ self.EmisionTipo = 'CAE'
326
+ self.Vencimiento = fedetresp['CAEFchVto']
327
+ self.FechaCbte = fedetresp.get('CbteFch', "") # .strftime("%Y/%m/%d")
328
+ self.CbteNro = fedetresp.get('CbteHasta', 0) # 1L
329
+ self.PuntoVenta = fecabresp.get('PtoVta', 0) # 4000
330
+ self.CbtDesde = fedetresp.get('CbteDesde', 0)
331
+ self.CbtHasta = fedetresp.get('CbteHasta', 0)
332
+ self.__analizar_errores(result)
333
+ return self.CAE
334
+
335
+ @inicializar_y_capturar_excepciones
336
+ def CompTotXRequest(self):
337
+ ret = self.client.FECompTotXRequest(
338
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
339
+ )
340
+
341
+ result = ret['FECompTotXRequestResult']
342
+ return result['RegXReq']
343
+
344
+ @inicializar_y_capturar_excepciones
345
+ def CompUltimoAutorizado(self, tipo_cbte, punto_vta):
346
+ ret = self.client.FECompUltimoAutorizado(
347
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
348
+ PtoVta=punto_vta,
349
+ CbteTipo=tipo_cbte,
350
+ )
351
+
352
+ result = ret['FECompUltimoAutorizadoResult']
353
+ self.CbteNro = result['CbteNro']
354
+ self.__analizar_errores(result)
355
+ return self.CbteNro is not None and str(self.CbteNro) or ''
356
+
357
+ @inicializar_y_capturar_excepciones
358
+ def CompConsultar(self, tipo_cbte, punto_vta, cbte_nro, reproceso=False):
359
+ difs = [] # si hay reproceso, verifico las diferencias con AFIP
360
+
361
+ ret = self.client.FECompConsultar(
362
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
363
+ FeCompConsReq={
364
+ 'CbteTipo': tipo_cbte,
365
+ 'CbteNro': cbte_nro,
366
+ 'PtoVta': punto_vta,
367
+ })
368
+
369
+ result = ret['FECompConsultarResult']
370
+ if 'ResultGet' in result:
371
+ resultget = result['ResultGet']
372
+
373
+ if reproceso:
374
+ # verifico los campos registrados coincidan con los enviados:
375
+ f = self.factura
376
+ verificaciones = {
377
+ 'Concepto': f['concepto'],
378
+ 'DocTipo': f['tipo_doc'],
379
+ 'DocNro': f['nro_doc'],
380
+ 'CbteTipo': f['tipo_cbte'],
381
+ 'CbteDesde': f['cbt_desde'],
382
+ 'CbteHasta': f['cbt_hasta'],
383
+ 'CbteFch': f['fecha_cbte'],
384
+ 'ImpTotal': f['imp_total'] and float(f['imp_total']) or 0.0,
385
+ 'ImpTotConc': f['imp_tot_conc'] and float(f['imp_tot_conc']) or 0.0,
386
+ 'ImpNeto': f['imp_neto'] and float(f['imp_neto']) or 0.0,
387
+ 'ImpOpEx': f['imp_op_ex'] and float(f['imp_op_ex']) or 0.0,
388
+ 'ImpTrib': f['imp_trib'] and float(f['imp_trib']) or 0.0,
389
+ 'ImpIVA': f['imp_iva'] and float(f['imp_iva']) or 0.0,
390
+ 'FchServDesde': f.get('fecha_serv_desde'),
391
+ 'FchServHasta': f.get('fecha_serv_hasta'),
392
+ 'FchVtoPago': f.get('fecha_venc_pago'),
393
+ 'MonId': f['moneda_id'],
394
+ 'MonCotiz': float(f['moneda_ctz']),
395
+ 'CbtesAsoc': [
396
+ {'CbteAsoc': {
397
+ 'Tipo': cbte_asoc['tipo'],
398
+ 'PtoVta': cbte_asoc['pto_vta'],
399
+ 'Nro': cbte_asoc['nro'],
400
+ 'Cuit': cbte_asoc.get('cuit'),
401
+ 'CbteFch': cbte_asoc.get('fecha') or None,
402
+ }}
403
+ for cbte_asoc in f['cbtes_asoc']],
404
+ 'Tributos': [
405
+ {'Tributo': {
406
+ 'Id': tributo['tributo_id'],
407
+ 'Desc': tributo['desc'],
408
+ 'BaseImp': float(tributo['base_imp'] or 0),
409
+ 'Alic': float(tributo['alic'] or 0),
410
+ 'Importe': float(tributo['importe']),
411
+ }}
412
+ for tributo in f['tributos']],
413
+ 'Iva': [
414
+ {'AlicIva': {
415
+ 'Id': iva['iva_id'],
416
+ 'BaseImp': float(iva['base_imp']),
417
+ 'Importe': float(iva['importe']),
418
+ }}
419
+ for iva in f['iva']],
420
+ 'Opcionales': [
421
+ {'Opcional': {
422
+ 'Id': opcional['opcional_id'],
423
+ 'Valor': opcional['valor'],
424
+ }} for opcional in f['opcionales']],
425
+ 'Compradores': [
426
+ {'Comprador': {
427
+ 'DocTipo': comprador['doc_tipo'],
428
+ 'DocNro': comprador['doc_nro'],
429
+ 'Porcentaje': comprador['porcentaje'],
430
+ }} for comprador in f['compradores']],
431
+ }
432
+ verifica(verificaciones, resultget.copy(), difs)
433
+ if difs:
434
+ print("Diferencias:", difs)
435
+ self.log("Diferencias: %s" % difs)
436
+ else:
437
+ # guardo los datos de AFIP (reconstruyo estructura interna)
438
+ self.factura = {
439
+ 'concepto': resultget.get('Concepto'),
440
+ 'tipo_doc': resultget.get('DocTipo'),
441
+ 'nro_doc': resultget.get('DocNro'),
442
+ 'tipo_cbte': resultget.get('CbteTipo'),
443
+ 'punto_vta': resultget.get('PtoVta'),
444
+ 'cbt_desde': resultget.get('CbteDesde'),
445
+ 'cbt_hasta': resultget.get('CbteHasta'),
446
+ 'fecha_cbte': resultget.get('CbteFch'),
447
+ 'imp_total': resultget.get('ImpTotal'),
448
+ 'imp_tot_conc': resultget.get('ImpTotConc'),
449
+ 'imp_neto': resultget.get('ImpNeto'),
450
+ 'imp_op_ex': resultget.get('ImpOpEx'),
451
+ 'imp_trib': resultget.get('ImpTrib'),
452
+ 'imp_iva': resultget.get('ImpIVA'),
453
+ 'fecha_serv_desde': resultget.get('FchServDesde'),
454
+ 'fecha_serv_hasta': resultget.get('FchServHasta'),
455
+ 'fecha_venc_pago': resultget.get('FchVtoPago'),
456
+ 'moneda_id': resultget.get('MonId'),
457
+ 'moneda_ctz': resultget.get('MonCotiz'),
458
+ 'cbtes_asoc': [
459
+ {
460
+ 'tipo': cbte_asoc['CbteAsoc']['Tipo'],
461
+ 'pto_vta': cbte_asoc['CbteAsoc']['PtoVta'],
462
+ 'nro': cbte_asoc['CbteAsoc']['Nro'],
463
+ 'cuit': cbte_asoc['CbteAsoc'].get('Cuit'),
464
+ 'fecha': cbte_asoc['CbteAsoc'].get('CbteFch'),
465
+ }
466
+ for cbte_asoc in resultget.get('CbtesAsoc', [])],
467
+ 'tributos': [
468
+ {
469
+ 'tributo_id': tributo['Tributo']['Id'],
470
+ 'desc': tributo['Tributo']['Desc'],
471
+ 'base_imp': tributo['Tributo'].get('BaseImp'),
472
+ 'alic': tributo['Tributo'].get('Alic'),
473
+ 'importe': tributo['Tributo']['Importe'],
474
+ }
475
+ for tributo in resultget.get('Tributos', [])],
476
+ 'iva': [
477
+ {
478
+ 'iva_id': iva['AlicIva']['Id'],
479
+ 'base_imp': iva['AlicIva']['BaseImp'],
480
+ 'importe': iva['AlicIva']['Importe'],
481
+ }
482
+ for iva in resultget.get('Iva', [])],
483
+ 'opcionales': [
484
+ {
485
+ 'opcional_id': obs['Opcional']['Id'],
486
+ 'valor': obs['Opcional']['Valor'],
487
+ }
488
+ for obs in resultget.get('Opcionales', [])],
489
+ 'compradores': [
490
+ {
491
+ 'doc_tipo': comp['Comprador']['DocTipo'],
492
+ 'doc_nro': comp['Comprador']['DocNro'],
493
+ 'porcentaje': comp['Comprador']['Porcentaje'],
494
+ }
495
+ for comp in resultget.get('Compradores', [])],
496
+ 'cae': resultget.get('CodAutorizacion'),
497
+ 'resultado': resultget.get('Resultado'),
498
+ 'fch_venc_cae': resultget.get('FchVto'),
499
+ 'obs': [
500
+ {
501
+ 'code': obs['Obs']['Code'],
502
+ 'msg': obs['Obs']['Msg'],
503
+ }
504
+ for obs in resultget.get('Observaciones', [])],
505
+ }
506
+
507
+ self.FechaCbte = resultget['CbteFch'] # .strftime("%Y/%m/%d")
508
+ self.CbteNro = resultget['CbteHasta'] # 1L
509
+ self.PuntoVenta = resultget['PtoVta'] # 4000
510
+ self.Vencimiento = resultget['FchVto'] # .strftime("%Y/%m/%d")
511
+ self.ImpTotal = str(resultget['ImpTotal'])
512
+ cod_aut = resultget['CodAutorizacion'] and str(resultget['CodAutorizacion']) or '' # 60423794871430L
513
+ self.Resultado = resultget['Resultado']
514
+ self.CbtDesde = resultget['CbteDesde']
515
+ self.CbtHasta = resultget['CbteHasta']
516
+ self.ImpTotal = resultget['ImpTotal']
517
+ self.ImpNeto = resultget['ImpNeto']
518
+ self.ImptoLiq = self.ImpIVA = resultget['ImpIVA']
519
+ self.ImpOpEx = resultget['ImpOpEx']
520
+ self.ImpTrib = resultget['ImpTrib']
521
+ self.EmisionTipo = resultget['EmisionTipo']
522
+ if self.EmisionTipo == 'CAE':
523
+ self.CAE = cod_aut
524
+ elif self.EmisionTipo == 'CAEA':
525
+ self.CAEA = cod_aut
526
+ # Obs:
527
+ for obs in resultget.get('Observaciones', []):
528
+ self.Observaciones.append("%(Code)s: %(Msg)s" % (obs['Obs']))
529
+ self.Obs = '\n'.join(self.Observaciones)
530
+
531
+ self.__analizar_errores(result)
532
+ if not difs:
533
+ return self.CAE or self.CAEA
534
+ else:
535
+ return ''
536
+
537
+ @inicializar_y_capturar_excepciones
538
+ def CAESolicitarX(self):
539
+ "Autorizar mοΏ½ltiples facturas (CAE) en una οΏ½nica solicitud"
540
+ # Ver CompTotXRequest -> cantidad maxima comprobantes (250)
541
+ # verificar que hay multiples facturas:
542
+ if not self.facturas:
543
+ raise RuntimeError("Llamar a IniciarFacturasX y AgregarFacturaX!")
544
+ # verificar que todas las facturas
545
+ puntos_vta = set([f['punto_vta'] for f in self.facturas])
546
+ tipos_cbte = set([f['tipo_cbte'] for f in self.facturas])
547
+ if len(puntos_vta) > 1:
548
+ raise RuntimeError("Los comprobantes deben ser del mismo pto_vta!")
549
+ if len(tipos_cbte) > 1:
550
+ raise RuntimeError("Los comprobantes deben tener el mismo tipo!")
551
+ # llamar al webservice:
552
+ ret = self.client.FECAESolicitar(
553
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
554
+ FeCAEReq={
555
+ 'FeCabReq': {'CantReg': len(self.facturas),
556
+ 'PtoVta': puntos_vta.pop(),
557
+ 'CbteTipo': tipos_cbte.pop()},
558
+ 'FeDetReq': [{'FECAEDetRequest': {
559
+ 'Concepto': f['concepto'],
560
+ 'DocTipo': f['tipo_doc'],
561
+ 'DocNro': f['nro_doc'],
562
+ 'CbteDesde': f['cbt_desde'],
563
+ 'CbteHasta': f['cbt_hasta'],
564
+ 'CbteFch': f['fecha_cbte'],
565
+ 'ImpTotal': f['imp_total'],
566
+ 'ImpTotConc': f['imp_tot_conc'],
567
+ 'ImpNeto': f['imp_neto'],
568
+ 'ImpOpEx': f['imp_op_ex'],
569
+ 'ImpTrib': f['imp_trib'],
570
+ 'ImpIVA': f['imp_iva'],
571
+ # Fechas solo se informan si Concepto in (2,3)
572
+ 'FchServDesde': f.get('fecha_serv_desde'),
573
+ 'FchServHasta': f.get('fecha_serv_hasta'),
574
+ 'FchVtoPago': f.get('fecha_venc_pago'),
575
+ 'MonId': f['moneda_id'],
576
+ 'MonCotiz': f['moneda_ctz'],
577
+ 'CbtesAsoc': [
578
+ {'CbteAsoc': {
579
+ 'Tipo': cbte_asoc['tipo'],
580
+ 'PtoVta': cbte_asoc['pto_vta'],
581
+ 'Nro': cbte_asoc['nro'],
582
+ 'Cuit': cbte_asoc.get('cuit'),
583
+ 'CbteFch': cbte_asoc.get('fecha'),
584
+ }}
585
+ for cbte_asoc in f['cbtes_asoc']] or None,
586
+ 'Tributos': [
587
+ {'Tributo': {
588
+ 'Id': tributo['tributo_id'],
589
+ 'Desc': tributo['desc'],
590
+ 'BaseImp': tributo['base_imp'],
591
+ 'Alic': tributo['alic'],
592
+ 'Importe': tributo['importe'],
593
+ }}
594
+ for tributo in f['tributos']] or None,
595
+ 'Iva': [
596
+ {'AlicIva': {
597
+ 'Id': iva['iva_id'],
598
+ 'BaseImp': iva['base_imp'],
599
+ 'Importe': iva['importe'],
600
+ }}
601
+ for iva in f['iva']] or None,
602
+ 'Opcionales': [
603
+ {'Opcional': {
604
+ 'Id': opcional['opcional_id'],
605
+ 'Valor': opcional['valor'],
606
+ }} for opcional in f['opcionales']] or None,
607
+ }
608
+ } for f in self.facturas]
609
+ })
610
+
611
+ result = ret['FECAESolicitarResult']
612
+ if 'FeCabResp' in result:
613
+ fecabresp = result['FeCabResp']
614
+ for i, fedetresp in enumerate(result['FeDetResp']):
615
+ fedetresp = fedetresp['FECAEDetResponse']
616
+ f = self.facturas[i]
617
+ # actualizar los campos devueltos por AFIP para cada comp.
618
+ f["resultado"] = fedetresp['Resultado']
619
+ f["cae"] = fedetresp['CAE'] and str(fedetresp['CAE']) or ""
620
+ f["emision_tipo"] = 'CAE'
621
+ f["fch_venc_cae"] = fedetresp['CAEFchVto']
622
+ f["obs"] = [
623
+ {'code': obs['Obs']['Code'], 'msg': obs['Obs']['Msg']}
624
+ for obs in fedetresp.get('Observaciones', [])]
625
+ # sanity checks:
626
+ assert str(f["fecha_cbte"]) == str(fedetresp['CbteFch'])
627
+ assert str(f["cbt_desde"]) == str(fedetresp['CbteDesde'])
628
+ assert str(f["cbt_hasta"]) == str(fedetresp['CbteHasta'])
629
+ assert str(f["punto_vta"]) == str(fecabresp['PtoVta'])
630
+ assert str(f["tipo_cbte"]) == str(fecabresp['CbteTipo'])
631
+ assert str(f["tipo_doc"]) == str(fedetresp['DocTipo'])
632
+ assert str(f["nro_doc"]) == str(fedetresp['DocNro'])
633
+ assert str(f["concepto"]) == str(fedetresp['Concepto'])
634
+
635
+ self.__analizar_errores(result)
636
+ assert fecabresp['CantReg'] == len(self.facturas)
637
+ return fecabresp['CantReg']
638
+
639
+ # metodos auxiliares para soporte de multiples comprobantes por solicitud:
640
+
641
+ def IniciarFacturasX(self):
642
+ "Inicializa lista de facturas para Solicitar multiples CAE"
643
+ self.facturas = []
644
+ return True
645
+
646
+ def AgregarFacturaX(self):
647
+ "Agrega una factura a la lista para Solicitar multiples CAE"
648
+ self.facturas.append(self.factura)
649
+ return True
650
+
651
+ def LeerFacturaX(self, i):
652
+ "Activa internamente una factura para usar ObtenerCampoFactura"
653
+ try:
654
+ # obtengo la factura segun el indice en la lista:
655
+ f = self.factura = self.facturas[i]
656
+ # completar propiedades para retro-compatibilidad:
657
+ self.FechaCbte = f['fecha_cbte']
658
+ self.PuntoVenta = f['punto_vta']
659
+ self.Vencimiento = f['fch_venc_cae']
660
+ self.Resultado = f['resultado']
661
+ self.CbtDesde = f['cbt_desde']
662
+ self.CbtHasta = f['cbt_hasta']
663
+ self.ImpTotal = str(f['imp_total'])
664
+ self.ImpNeto = str(f.get('imp_neto'))
665
+ self.ImptoLiq = self.ImpIVA = str(f.get('imp_iva'))
666
+ self.ImpOpEx = str(f.get('imp_op_ex'))
667
+ self.ImpTrib = str(f.get('imp_trib'))
668
+ self.EmisionTipo = f['emision_tipo']
669
+ if self.EmisionTipo == 'CAE':
670
+ self.CAE = f['cae']
671
+ elif self.EmisionTipo == 'CAEA':
672
+ self.CAEA = f['caea']
673
+ # Obs:
674
+ self.Observaciones = []
675
+ for obs in f.get('obs', []):
676
+ self.Observaciones.append("%(code)s: %(msg)s" % (obs))
677
+ self.Obs = '\n'.join(self.Observaciones)
678
+ return True
679
+ except BaseException:
680
+ return False
681
+
682
+ # metodos para CAEA:
683
+
684
+ @inicializar_y_capturar_excepciones
685
+ def CAEASolicitar(self, periodo, orden):
686
+ ret = self.client.FECAEASolicitar(
687
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
688
+ Periodo=periodo,
689
+ Orden=orden,
690
+ )
691
+
692
+ result = ret['FECAEASolicitarResult']
693
+ self.__analizar_errores(result)
694
+
695
+ if 'ResultGet' in result:
696
+ result = result['ResultGet']
697
+ if 'CAEA' in result:
698
+ self.CAEA = result['CAEA']
699
+ self.Periodo = result['Periodo']
700
+ self.Orden = result['Orden']
701
+ self.FchVigDesde = result['FchVigDesde']
702
+ self.FchVigHasta = result['FchVigHasta']
703
+ self.FchTopeInf = result['FchTopeInf']
704
+ self.FchProceso = result['FchProceso']
705
+ # Obs (COMPGv28):
706
+ for obs in result.get('Observaciones', []):
707
+ self.Observaciones.append("%(Code)s: %(Msg)s" % (obs['Obs']))
708
+ self.Obs = '\n'.join(self.Observaciones)
709
+
710
+ return self.CAEA and str(self.CAEA) or ''
711
+
712
+ @inicializar_y_capturar_excepciones
713
+ def CAEAConsultar(self, periodo, orden):
714
+ "MοΏ½todo de consulta de CAEA"
715
+ ret = self.client.FECAEAConsultar(
716
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
717
+ Periodo=periodo,
718
+ Orden=orden,
719
+ )
720
+
721
+ result = ret['FECAEAConsultarResult']
722
+ self.__analizar_errores(result)
723
+
724
+ if 'ResultGet' in result:
725
+ result = result['ResultGet']
726
+ if 'CAEA' in result:
727
+ self.CAEA = result['CAEA']
728
+ self.Periodo = result['Periodo']
729
+ self.Orden = result['Orden']
730
+ self.FchVigDesde = result['FchVigDesde']
731
+ self.FchVigHasta = result['FchVigHasta']
732
+ self.FchTopeInf = result['FchTopeInf']
733
+ self.FchProceso = result['FchProceso']
734
+
735
+ return self.CAEA and str(self.CAEA) or ''
736
+
737
+ @inicializar_y_capturar_excepciones
738
+ def CAEARegInformativo(self):
739
+ "MοΏ½todo para informar comprobantes emitidos con CAEA"
740
+ f = self.factura
741
+ ret = self.client.FECAEARegInformativo(
742
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
743
+ FeCAEARegInfReq={
744
+ 'FeCabReq': {'CantReg': 1,
745
+ 'PtoVta': f['punto_vta'],
746
+ 'CbteTipo': f['tipo_cbte']},
747
+ 'FeDetReq': [{'FECAEADetRequest': {
748
+ 'Concepto': f['concepto'],
749
+ 'DocTipo': f['tipo_doc'],
750
+ 'DocNro': f['nro_doc'],
751
+ 'CbteDesde': f['cbt_desde'],
752
+ 'CbteHasta': f['cbt_hasta'],
753
+ 'CbteFch': f['fecha_cbte'],
754
+ 'ImpTotal': f['imp_total'],
755
+ 'ImpTotConc': f['imp_tot_conc'],
756
+ 'ImpNeto': f['imp_neto'],
757
+ 'ImpOpEx': f['imp_op_ex'],
758
+ 'ImpTrib': f['imp_trib'],
759
+ 'ImpIVA': f['imp_iva'],
760
+ # Fechas solo se informan si Concepto in (2,3)
761
+ 'FchServDesde': f.get('fecha_serv_desde'),
762
+ 'FchServHasta': f.get('fecha_serv_hasta'),
763
+ 'FchVtoPago': f.get('fecha_venc_pago'),
764
+ 'MonId': f['moneda_id'],
765
+ 'MonCotiz': f['moneda_ctz'],
766
+ 'CbtesAsoc': [
767
+ {'CbteAsoc': {
768
+ 'Tipo': cbte_asoc['tipo'],
769
+ 'PtoVta': cbte_asoc['pto_vta'],
770
+ 'Nro': cbte_asoc['nro'],
771
+ 'Cuit': cbte_asoc.get('cuit'),
772
+ 'CbteFch': cbte_asoc.get('fecha'),
773
+ }}
774
+ for cbte_asoc in f['cbtes_asoc']]
775
+ if f['cbtes_asoc'] else None,
776
+ 'Tributos': [
777
+ {'Tributo': {
778
+ 'Id': tributo['tributo_id'],
779
+ 'Desc': tributo['desc'],
780
+ 'BaseImp': tributo['base_imp'],
781
+ 'Alic': tributo['alic'],
782
+ 'Importe': tributo['importe'],
783
+ }}
784
+ for tributo in f['tributos']]
785
+ if f['tributos'] else None,
786
+ 'Iva': [
787
+ {'AlicIva': {
788
+ 'Id': iva['iva_id'],
789
+ 'BaseImp': iva['base_imp'],
790
+ 'Importe': iva['importe'],
791
+ }}
792
+ for iva in f['iva']]
793
+ if f['iva'] else None,
794
+ 'Opcionales': [
795
+ {'Opcional': {
796
+ 'Id': opcional['opcional_id'],
797
+ 'Valor': opcional['valor'],
798
+ }} for opcional in f['opcionales']] or None,
799
+ 'CAEA': f['caea'],
800
+ 'CbteFchHsGen': f.get('fecha_hs_gen'),
801
+ }
802
+ }]
803
+ })
804
+
805
+ result = ret['FECAEARegInformativoResult']
806
+ if 'FeCabResp' in result:
807
+ fecabresp = result['FeCabResp']
808
+ fedetresp = result['FeDetResp'][0]['FECAEADetResponse']
809
+
810
+ # Reprocesar en caso de error (recuperar CAE emitido anteriormente)
811
+ if self.Reprocesar and 'Errors' in result:
812
+ for error in result['Errors']:
813
+ err_code = str(error['Err']['Code'])
814
+ if fedetresp['Resultado'] == 'R' and err_code == '703':
815
+ caea = self.CompConsultar(f['tipo_cbte'], f['punto_vta'], f['cbt_desde'], reproceso=True)
816
+ if caea and self.EmisionTipo == 'CAE':
817
+ self.Reproceso = 'S'
818
+ return caea
819
+ self.Reproceso = 'N'
820
+
821
+ self.Resultado = fecabresp['Resultado']
822
+ # Obs:
823
+ for obs in fedetresp.get('Observaciones', []):
824
+ self.Observaciones.append("%(Code)s: %(Msg)s" % (obs['Obs']))
825
+ self.Obs = '\n'.join(self.Observaciones)
826
+ self.CAEA = fedetresp['CAEA'] and str(fedetresp['CAEA']) or ""
827
+ self.EmisionTipo = 'CAEA'
828
+ self.FechaCbte = fedetresp['CbteFch'] # .strftime("%Y/%m/%d")
829
+ self.CbteNro = fedetresp['CbteHasta'] # 1L
830
+ self.PuntoVenta = fecabresp['PtoVta'] # 4000
831
+ self.CbtDesde = fedetresp['CbteDesde']
832
+ self.CbtHasta = fedetresp['CbteHasta']
833
+ self.__analizar_errores(result)
834
+ return self.CAEA
835
+
836
+ @inicializar_y_capturar_excepciones
837
+ def CAEASinMovimientoInformar(self, punto_vta, caea):
838
+ "MοΏ½todo para informar CAEA sin movimiento"
839
+ ret = self.client.FECAEASinMovimientoInformar(
840
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
841
+ PtoVta=punto_vta,
842
+ CAEA=caea,
843
+ )
844
+
845
+ result = ret['FECAEASinMovimientoInformarResult']
846
+ self.__analizar_errores(result)
847
+
848
+ if 'CAEA' in result:
849
+ self.CAEA = result['CAEA']
850
+ if 'FchProceso' in result:
851
+ self.FchProceso = result['FchProceso']
852
+ if 'Resultado' in result:
853
+ self.Resultado = result['Resultado']
854
+ self.PuntoVenta = result['PtoVta'] # 4000
855
+
856
+ return self.Resultado or ''
857
+
858
+ @inicializar_y_capturar_excepciones
859
+ def ParamGetTiposCbte(self, sep="|"):
860
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de Comprobantes"
861
+ ret = self.client.FEParamGetTiposCbte(
862
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
863
+ )
864
+ res = ret['FEParamGetTiposCbteResult']
865
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['CbteTipo']).replace("\t", sep)
866
+ for p in res['ResultGet']]
867
+
868
+ @inicializar_y_capturar_excepciones
869
+ def ParamGetTiposConcepto(self, sep="|"):
870
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de Conceptos"
871
+ ret = self.client.FEParamGetTiposConcepto(
872
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
873
+ )
874
+ res = ret['FEParamGetTiposConceptoResult']
875
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['ConceptoTipo']).replace("\t", sep)
876
+ for p in res['ResultGet']]
877
+
878
+ @inicializar_y_capturar_excepciones
879
+ def ParamGetTiposDoc(self, sep="|"):
880
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de Documentos"
881
+ ret = self.client.FEParamGetTiposDoc(
882
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
883
+ )
884
+ res = ret['FEParamGetTiposDocResult']
885
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['DocTipo']).replace("\t", sep)
886
+ for p in res['ResultGet']]
887
+
888
+ @inicializar_y_capturar_excepciones
889
+ def ParamGetTiposIva(self, sep="|"):
890
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de AlοΏ½cuotas"
891
+ ret = self.client.FEParamGetTiposIva(
892
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
893
+ )
894
+ res = ret['FEParamGetTiposIvaResult']
895
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['IvaTipo']).replace("\t", sep)
896
+ for p in res['ResultGet']]
897
+
898
+ @inicializar_y_capturar_excepciones
899
+ def ParamGetTiposMonedas(self, sep="|"):
900
+ "Recuperador de valores referenciales de cοΏ½digos de Monedas"
901
+ ret = self.client.FEParamGetTiposMonedas(
902
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
903
+ )
904
+ res = ret['FEParamGetTiposMonedasResult']
905
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['Moneda']).replace("\t", sep)
906
+ for p in res['ResultGet']]
907
+
908
+ @inicializar_y_capturar_excepciones
909
+ def ParamGetTiposOpcional(self, sep="|"):
910
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de datos opcionales"
911
+ ret = self.client.FEParamGetTiposOpcional(
912
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
913
+ )
914
+ res = ret['FEParamGetTiposOpcionalResult']
915
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['OpcionalTipo']).replace("\t", sep)
916
+ for p in res.get('ResultGet', [])]
917
+
918
+ @inicializar_y_capturar_excepciones
919
+ def ParamGetTiposTributos(self, sep="|"):
920
+ "Recuperador de valores referenciales de cοΏ½digos de Tipos de Tributos"
921
+ "Este mοΏ½todo permite consultar los tipos de tributos habilitados en este WS"
922
+ ret = self.client.FEParamGetTiposTributos(
923
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
924
+ )
925
+ res = ret['FEParamGetTiposTributosResult']
926
+ return [("%(Id)s\t%(Desc)s\t%(FchDesde)s\t%(FchHasta)s" % p['TributoTipo']).replace("\t", sep)
927
+ for p in res['ResultGet']]
928
+
929
+ @inicializar_y_capturar_excepciones
930
+ def ParamGetTiposPaises(self, sep="|"):
931
+ "Recuperador de valores referenciales de cοΏ½digos de Paises"
932
+ "Este mοΏ½todo permite consultar los tipos de tributos habilitados en este WS"
933
+ ret = self.client.FEParamGetTiposPaises(
934
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
935
+ )
936
+ res = ret['FEParamGetTiposPaisesResult']
937
+ return [("%(Id)s\t%(Desc)s" % p['PaisTipo']).replace("\t", sep)
938
+ for p in res['ResultGet']]
939
+
940
+ @inicializar_y_capturar_excepciones
941
+ def ParamGetCotizacion(self, moneda_id):
942
+ "Recuperador de cotizaciοΏ½n de moneda"
943
+ ret = self.client.FEParamGetCotizacion(
944
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
945
+ MonId=moneda_id,
946
+ )
947
+ self.__analizar_errores(ret)
948
+ res = ret['FEParamGetCotizacionResult']['ResultGet']
949
+ self.FchCotiz = res.get("FchCotiz")
950
+ return str(res.get('MonCotiz', ""))
951
+
952
+ @inicializar_y_capturar_excepciones
953
+ def ParamGetPtosVenta(self, sep="|"):
954
+ "Recuperador de valores referenciales Puntos de Venta registrados"
955
+ ret = self.client.FEParamGetPtosVenta(
956
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
957
+ )
958
+ res = ret.get('FEParamGetPtosVentaResult', {})
959
+ return [("%(Nro)s\tEmisionTipo:%(EmisionTipo)s\tBloqueado:%(Bloqueado)s\tFchBaja:%(FchBaja)s" % p['PtoVenta']).replace("\t", sep)
960
+ for p in res.get('ResultGet', [])]
961
+
962
+
963
+ def p_assert_eq(a, b):
964
+ print(a, a == b and '==' or '!=', b)
965
+
966
+
967
+ def main():
968
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
969
+ import os
970
+ import time
971
+
972
+ DEBUG = '--debug' in sys.argv
973
+
974
+ if DEBUG:
975
+ from pysimplesoap.client import __version__ as soapver
976
+ print("pysimplesoap.__version__ = ", soapver)
977
+
978
+ wsfev1 = WSFEv1()
979
+ wsfev1.LanzarExcepciones = True
980
+
981
+ cache = None
982
+ if "--prod" in sys.argv:
983
+ wsdl = "https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL"
984
+ else:
985
+ wsdl = WSDL
986
+ proxy = ""
987
+ wrapper = "" # "pycurl"
988
+ cacert = "conf/afip_ca_info.crt"
989
+
990
+ ok = wsfev1.Conectar(cache, wsdl, proxy, wrapper, cacert)
991
+
992
+ if not ok:
993
+ raise RuntimeError(wsfev1.Excepcion)
994
+
995
+ if DEBUG:
996
+ print("LOG: ", wsfev1.DebugLog())
997
+
998
+ if "--dummy" in sys.argv:
999
+ print(wsfev1.client.help("FEDummy"))
1000
+ wsfev1.Dummy()
1001
+ print("AppServerStatus", wsfev1.AppServerStatus)
1002
+ print("DbServerStatus", wsfev1.DbServerStatus)
1003
+ print("AuthServerStatus", wsfev1.AuthServerStatus)
1004
+ sys.exit(0)
1005
+
1006
+ # obteniendo el TA para pruebas
1007
+ from .wsaa import WSAA
1008
+ ta = WSAA().Autenticar("wsfe", "reingart.crt", "reingart.key", debug=True)
1009
+ wsfev1.SetTicketAcceso(ta)
1010
+ wsfev1.Cuit = "20267565393"
1011
+
1012
+ if "--prueba" in sys.argv:
1013
+ print(wsfev1.client.help("FECAESolicitar").encode("latin1"))
1014
+
1015
+ if '--usados' in sys.argv:
1016
+ tipo_cbte = 49
1017
+ concepto = 1
1018
+ elif '--fce' in sys.argv:
1019
+ tipo_cbte = 203
1020
+ concepto = 1
1021
+ else:
1022
+ tipo_cbte = 3
1023
+ concepto = 3 if ('--rg4109' not in sys.argv) else 1
1024
+ punto_vta = 4001
1025
+ cbte_nro = int(wsfev1.CompUltimoAutorizado(tipo_cbte, punto_vta) or 0)
1026
+ fecha = datetime.datetime.now().strftime("%Y%m%d")
1027
+ tipo_doc = 80 if '--usados' not in sys.argv else 30
1028
+ nro_doc = "30500010912"
1029
+ cbt_desde = cbte_nro + 1
1030
+ cbt_hasta = cbte_nro + 1
1031
+ imp_total = "222.00"
1032
+ imp_tot_conc = "0.00"
1033
+ imp_neto = "200.00"
1034
+ imp_iva = "21.00"
1035
+ imp_trib = "1.00"
1036
+ imp_op_ex = "0.00"
1037
+ fecha_cbte = fecha
1038
+ fecha_venc_pago = fecha_serv_desde = fecha_serv_hasta = None
1039
+ # Fechas del perΓ­odo del servicio facturado y vencimiento de pago:
1040
+ if concepto > 1:
1041
+ fecha_venc_pago = fecha
1042
+ fecha_serv_desde = fecha; fecha_serv_hasta = fecha
1043
+ elif '--fce' in sys.argv:
1044
+ # obligatorio en Factura de CrΓ©dito ElectrΓ³nica MiPyMEs (FCE):
1045
+ fecha_venc_pago = fecha
1046
+ moneda_id = 'PES'; moneda_ctz = '1.000'
1047
+
1048
+ # inicializar prueba de multiples comprobantes por solicitud
1049
+ if "--multiple" in sys.argv:
1050
+ wsfev1.IniciarFacturasX()
1051
+ reg_x_req = wsfev1.CompTotXRequest() # cant max. comprobantes
1052
+ else:
1053
+ reg_x_req = 1 # un solo comprobante
1054
+
1055
+ for i in range(reg_x_req):
1056
+
1057
+ wsfev1.CrearFactura(concepto, tipo_doc, nro_doc,
1058
+ tipo_cbte, punto_vta, cbt_desde + i, cbt_hasta + i,
1059
+ imp_total, imp_tot_conc, imp_neto,
1060
+ imp_iva, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
1061
+ fecha_serv_desde, fecha_serv_hasta, # --
1062
+ moneda_id, moneda_ctz)
1063
+
1064
+ if '--caea' in sys.argv:
1065
+ periodo = datetime.datetime.today().strftime("%Y%M")
1066
+ orden = 1 if datetime.datetime.today().day < 15 else 2
1067
+ caea = wsfev1.CAEAConsultar(periodo, orden)
1068
+ wsfev1.EstablecerCampoFactura("caea", caea)
1069
+ wsfev1.EstablecerCampoFactura("fecha_hs_gen", "yyyymmddhhmiss")
1070
+
1071
+ # comprobantes asociados (notas de crΓ©dito / dΓ©bito)
1072
+ if tipo_cbte in (2, 3, 7, 8, 12, 13, 203, 208, 213):
1073
+ tipo = 201 if tipo_cbte in (203, 208, 213) else 3
1074
+ pto_vta = 4001
1075
+ nro = 1
1076
+ cuit = "20267565393"
1077
+ # obligatorio en Factura de CrΓ©dito ElectrΓ³nica MiPyMEs (FCE):
1078
+ fecha_cbte = fecha if tipo_cbte in (203, 208, 213) else None
1079
+ wsfev1.AgregarCmpAsoc(tipo, pto_vta, nro, cuit, fecha_cbte)
1080
+
1081
+ # otros tributos:
1082
+ tributo_id = 99
1083
+ desc = 'Impuesto Municipal Matanza'
1084
+ base_imp = None
1085
+ alic = None
1086
+ importe = 1
1087
+ wsfev1.AgregarTributo(tributo_id, desc, base_imp, alic, importe)
1088
+
1089
+ # subtotales por alicuota de IVA:
1090
+ iva_id = 3 # 0%
1091
+ base_imp = 100
1092
+ importe = 0
1093
+ wsfev1.AgregarIva(iva_id, base_imp, importe)
1094
+
1095
+ # subtotales por alicuota de IVA:
1096
+ iva_id = 5 # 21%
1097
+ base_imp = 100
1098
+ importe = 21
1099
+ wsfev1.AgregarIva(iva_id, base_imp, importe)
1100
+
1101
+ # datos opcionales para proyectos promovidos:
1102
+ if '--proyectos' in sys.argv:
1103
+ wsfev1.AgregarOpcional(2, "1234") # identificador del proyecto
1104
+ # datos opcionales para RG Bienes Usados 3411 (del vendedor):
1105
+ if '--usados' in sys.argv:
1106
+ wsfev1.AgregarOpcional(91, "Juan Perez") # Nombre y Apellido
1107
+ wsfev1.AgregarOpcional(92, "200") # Nacionalidad
1108
+ wsfev1.AgregarOpcional(93, "Balcarce 50") # Domicilio
1109
+ # datos opcionales para RG 3668 Impuesto al Valor Agregado - Art.12:
1110
+ if '--rg3668' in sys.argv:
1111
+ wsfev1.AgregarOpcional(5, "02") # IVA Excepciones
1112
+ wsfev1.AgregarOpcional(61, "80") # Firmante Doc Tipo
1113
+ wsfev1.AgregarOpcional(62, "20267565393") # Firmante Doc Nro
1114
+ wsfev1.AgregarOpcional(7, "01") # CarοΏ½cter del Firmante
1115
+ # datos opcionales para RG 4004-E Alquiler de inmuebles (Ganancias)
1116
+ if '--rg4004' in sys.argv:
1117
+ wsfev1.AgregarOpcional(17, "1") # Intermediario
1118
+ wsfev1.AgregarOpcional(1801, "30500010912") # CUIT Propietario
1119
+ wsfev1.AgregarOpcional(1802, "BNA") # Nombr e Titular
1120
+ # datos de compradores RG 4109-E bienes muebles registrables (%)
1121
+ if '--rg4109' in sys.argv:
1122
+ wsfev1.AgregarComprador(80, "30500010912", 99.99)
1123
+ wsfev1.AgregarComprador(80, "30999032083", 0.01)
1124
+
1125
+ # datos de Factura de CrΓ©dito ElectrΓ³nica MiPyMEs (FCE):
1126
+ if '--fce' in sys.argv:
1127
+ wsfev1.AgregarOpcional(2101, "2850590940090418135201") # CBU
1128
+ wsfev1.AgregarOpcional(2102, "pyafipws") # alias
1129
+ if tipo_cbte in (203, 208, 213):
1130
+ wsfev1.AgregarOpcional(22, "S") # AnulaciΓ³n
1131
+
1132
+ # agregar la factura creada internamente para solicitud mΓΊltiple:
1133
+ if "--multiple" in sys.argv:
1134
+ wsfev1.AgregarFacturaX()
1135
+
1136
+ import time
1137
+ t0 = time.time()
1138
+ if not '--caea' in sys.argv:
1139
+ if not "--multiple" in sys.argv:
1140
+ wsfev1.CAESolicitar()
1141
+ else:
1142
+ cant = wsfev1.CAESolicitarX()
1143
+ print("Cantidad de comprobantes procesados:", cant)
1144
+ else:
1145
+ wsfev1.CAEARegInformativo()
1146
+ t1 = time.time()
1147
+
1148
+ # revisar los resultados:
1149
+ for i in range(reg_x_req):
1150
+ if "--multiple" in sys.argv:
1151
+ print("Analizando respuesta para factura indice: ", i)
1152
+ ok = wsfev1.LeerFacturaX(i)
1153
+ print("Nro. Cbte. desde-hasta", wsfev1.CbtDesde, wsfev1.CbtHasta)
1154
+ print("Resultado", wsfev1.Resultado)
1155
+ print("Reproceso", wsfev1.Reproceso)
1156
+ print("CAE", wsfev1.CAE)
1157
+ print("Vencimiento", wsfev1.Vencimiento)
1158
+ print("Observaciones", wsfev1.Obs)
1159
+
1160
+ if DEBUG:
1161
+ print("t0", t0)
1162
+ print("t1", t1)
1163
+ print("lapso", t1 - t0)
1164
+ open("xmlrequest.xml", "wb").write(wsfev1.XmlRequest)
1165
+ open("xmlresponse.xml", "wb").write(wsfev1.XmlResponse)
1166
+
1167
+ if not "--multiple" in sys.argv:
1168
+ wsfev1.AnalizarXml("XmlResponse")
1169
+ p_assert_eq(wsfev1.ObtenerTagXml('CAE'), str(wsfev1.CAE))
1170
+ p_assert_eq(wsfev1.ObtenerTagXml('Concepto'), '2')
1171
+ p_assert_eq(wsfev1.ObtenerTagXml('Obs', 0, 'Code'), "10017")
1172
+ print(wsfev1.ObtenerTagXml('Obs', 0, 'Msg'))
1173
+
1174
+ if "--reprocesar" in sys.argv:
1175
+ print("reprocesando....")
1176
+ wsfev1.Reproceso = True
1177
+ cae = wsfev1.CAE
1178
+ wsfev1.CAESolicitar()
1179
+ assert cae == wsfev1.CAE
1180
+ assert wsfev1.Reproceso == "S"
1181
+
1182
+ if "--consultar" in sys.argv:
1183
+ cae = wsfev1.CAE
1184
+ cae2 = wsfev1.CompConsultar(tipo_cbte, punto_vta, cbt_desde)
1185
+ p_assert_eq(cae, cae2)
1186
+ # comparar datos del encabezado
1187
+ p_assert_eq(wsfev1.ObtenerCampoFactura('cae'), str(wsfev1.CAE))
1188
+ p_assert_eq(wsfev1.ObtenerCampoFactura('nro_doc'), int(nro_doc))
1189
+ p_assert_eq(wsfev1.ObtenerCampoFactura('imp_total'), float(imp_total))
1190
+ # comparar primer alicuota de IVA
1191
+ p_assert_eq(wsfev1.ObtenerCampoFactura('iva', 0, 'importe'), 21)
1192
+ # comparar primer tributo
1193
+ p_assert_eq(wsfev1.ObtenerCampoFactura('tributos', 0, 'importe'), 1)
1194
+ # comparar primer opcional
1195
+ if '--rg3668' in sys.argv:
1196
+ p_assert_eq(wsfev1.ObtenerCampoFactura('opcionales', 0, 'valor'), "02")
1197
+ # comparar primer observacion de AFIP
1198
+ p_assert_eq(wsfev1.ObtenerCampoFactura('obs', 0, 'code'), 10017)
1199
+ # pruebo la segunda observacion inexistente
1200
+ p_assert_eq(wsfev1.ObtenerCampoFactura('obs', 1, 'code'), None)
1201
+ p_assert_eq(wsfev1.Excepcion, "El campo 1 solicitado no existe")
1202
+
1203
+ if "--get" in sys.argv:
1204
+ tipo_cbte = 2
1205
+ punto_vta = 4001
1206
+ cbte_nro = wsfev1.CompUltimoAutorizado(tipo_cbte, punto_vta)
1207
+
1208
+ wsfev1.CompConsultar(tipo_cbte, punto_vta, cbte_nro)
1209
+
1210
+ print("FechaCbte = ", wsfev1.FechaCbte)
1211
+ print("CbteNro = ", wsfev1.CbteNro)
1212
+ print("PuntoVenta = ", wsfev1.PuntoVenta)
1213
+ print("ImpTotal =", wsfev1.ImpTotal)
1214
+ print("CAE = ", wsfev1.CAE)
1215
+ print("Vencimiento = ", wsfev1.Vencimiento)
1216
+ print("EmisionTipo = ", wsfev1.EmisionTipo)
1217
+
1218
+ wsfev1.AnalizarXml("XmlResponse")
1219
+ p_assert_eq(wsfev1.ObtenerTagXml('CodAutorizacion'), str(wsfev1.CAE))
1220
+ p_assert_eq(wsfev1.ObtenerTagXml('CbteFch'), wsfev1.FechaCbte)
1221
+ p_assert_eq(wsfev1.ObtenerTagXml('MonId'), "PES")
1222
+ p_assert_eq(wsfev1.ObtenerTagXml('MonCotiz'), "1")
1223
+ p_assert_eq(wsfev1.ObtenerTagXml('DocTipo'), "80")
1224
+ p_assert_eq(wsfev1.ObtenerTagXml('DocNro'), "30500010912")
1225
+
1226
+ if "--parametros" in sys.argv:
1227
+ import codecs
1228
+ import locale
1229
+ import traceback
1230
+ if sys.stdout.encoding is None:
1231
+ sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout, "replace")
1232
+ sys.stderr = codecs.getwriter(locale.getpreferredencoding())(sys.stderr, "replace")
1233
+
1234
+ print('\n'.join(wsfev1.ParamGetTiposDoc()))
1235
+ print("=== Tipos de Comprobante ===")
1236
+ print('\n'.join(wsfev1.ParamGetTiposCbte()))
1237
+ print("=== Tipos de Concepto ===")
1238
+ print('\n'.join(wsfev1.ParamGetTiposConcepto()))
1239
+ print("=== Tipos de Documento ===")
1240
+ print('\n'.join(wsfev1.ParamGetTiposDoc()))
1241
+ print("=== Alicuotas de IVA ===")
1242
+ print('\n'.join(wsfev1.ParamGetTiposIva()))
1243
+ print("=== Monedas ===")
1244
+ print('\n'.join(wsfev1.ParamGetTiposMonedas()))
1245
+ print("=== Tipos de datos opcionales ===")
1246
+ print('\n'.join(wsfev1.ParamGetTiposOpcional()))
1247
+ print("=== Tipos de Tributo ===")
1248
+ print('\n'.join(wsfev1.ParamGetTiposTributos()))
1249
+ print("=== Tipos de Paises ===")
1250
+ print('\n'.join(wsfev1.ParamGetTiposPaises()))
1251
+ print("=== Puntos de Venta ===")
1252
+ print('\n'.join(wsfev1.ParamGetPtosVenta()))
1253
+
1254
+ if "--cotizacion" in sys.argv:
1255
+ print(wsfev1.ParamGetCotizacion('DOL'))
1256
+
1257
+ if "--comptox" in sys.argv:
1258
+ print(wsfev1.CompTotXRequest())
1259
+
1260
+ if "--ptosventa" in sys.argv:
1261
+ print(wsfev1.ParamGetPtosVenta())
1262
+
1263
+ if "--solicitar-caea" in sys.argv:
1264
+ periodo = sys.argv[sys.argv.index("--solicitar-caea") + 1]
1265
+ orden = sys.argv[sys.argv.index("--solicitar-caea") + 2]
1266
+
1267
+ if DEBUG:
1268
+ print("Solicitando CAEA para periodo %s orden %s" % (periodo, orden))
1269
+
1270
+ caea = wsfev1.CAEASolicitar(periodo, orden)
1271
+ print("CAEA:", caea)
1272
+
1273
+ if wsfev1.Observaciones:
1274
+ print("Observaciones:")
1275
+ for obs in wsfev1.Observaciones:
1276
+ print(obs)
1277
+
1278
+ if wsfev1.Errores:
1279
+ print("Errores:")
1280
+ for error in wsfev1.Errores:
1281
+ print(error)
1282
+
1283
+ if DEBUG:
1284
+ print("periodo:", wsfev1.Periodo)
1285
+ print("orden:", wsfev1.Orden)
1286
+ print("fch_vig_desde:", wsfev1.FchVigDesde)
1287
+ print("fch_vig_hasta:", wsfev1.FchVigHasta)
1288
+ print("fch_tope_inf:", wsfev1.FchTopeInf)
1289
+ print("fch_proceso:", wsfev1.FchProceso)
1290
+
1291
+ if not caea:
1292
+ print('Consultando CAEA')
1293
+ caea = wsfev1.CAEAConsultar(periodo, orden)
1294
+ print("CAEA:", caea)
1295
+ if wsfev1.Errores:
1296
+ print("Errores:")
1297
+ for error in wsfev1.Errores:
1298
+ print(error)
1299
+
1300
+ if "--sinmovimiento-caea" in sys.argv:
1301
+ punto_vta = sys.argv[sys.argv.index("--sinmovimiento-caea") + 1]
1302
+ caea = sys.argv[sys.argv.index("--sinmovimiento-caea") + 2]
1303
+
1304
+ if DEBUG:
1305
+ print("Informando Punto Venta %s CAEA %s SIN MOVIMIENTO" % (punto_vta, caea))
1306
+
1307
+ resultado = wsfev1.CAEASinMovimientoInformar(punto_vta, caea)
1308
+ print("Resultado:", resultado)
1309
+ print("fch_proceso:", wsfev1.FchProceso)
1310
+
1311
+ if wsfev1.Errores:
1312
+ print("Errores:")
1313
+ for error in wsfev1.Errores:
1314
+ print(error)
1315
+
1316
+
1317
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
1318
+ INSTALL_DIR = WSFEv1.InstallDir = get_install_dir()
1319
+
1320
+
1321
+ if __name__ == '__main__':
1322
+
1323
+ if "--register" in sys.argv or "--unregister" in sys.argv:
1324
+ import pythoncom
1325
+ if TYPELIB:
1326
+ if '--register' in sys.argv:
1327
+ tlb = os.path.abspath(os.path.join(INSTALL_DIR, "typelib", "wsfev1.tlb"))
1328
+ print("Registering %s" % (tlb,))
1329
+ tli = pythoncom.LoadTypeLib(tlb)
1330
+ pythoncom.RegisterTypeLib(tli, tlb)
1331
+ elif '--unregister' in sys.argv:
1332
+ k = WSFEv1
1333
+ pythoncom.UnRegisterTypeLib(k._typelib_guid_,
1334
+ k._typelib_version_[0],
1335
+ k._typelib_version_[1],
1336
+ 0,
1337
+ pythoncom.SYS_WIN32)
1338
+ print("Unregistered typelib")
1339
+ import win32com.server.register
1340
+ # print "_reg_class_spec_", WSFEv1._reg_class_spec_
1341
+ win32com.server.register.UseCommandLine(WSFEv1)
1342
+ elif "/Automate" in sys.argv:
1343
+ # MS seems to like /automate to run the class factories.
1344
+ import win32com.server.localserver
1345
+ # win32com.server.localserver.main()
1346
+ # start the server.
1347
+ win32com.server.localserver.serve([WSFEv1._reg_clsid_])
1348
+ else:
1349
+ main()
app/pyafipws/wsfexv1.py ADDED
@@ -0,0 +1,799 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para obtener CAE, cοΏ½digo de autorizaciοΏ½n de impresiοΏ½n o electrοΏ½nico,
14
+ del web service WSFEXv1 de AFIP (Factura ElectrοΏ½nica ExportaciοΏ½n VersiοΏ½n 1)
15
+ segοΏ½n RG2758/2010 (Registros Especiales Aduaneros) y RG3689/14 (servicios)
16
+ http://www.sistemasagiles.com.ar/trac/wiki/FacturaElectronicaExportacion
17
+ """
18
+
19
+ __author__ = "Mariano Reingart (reingart@gmail.com)"
20
+ __copyright__ = "Copyright (C) 2011-2015 Mariano Reingart"
21
+ __license__ = "GPL 3.0"
22
+ __version__ = "1.08f"
23
+
24
+ import datetime
25
+ import decimal
26
+ import os
27
+ import sys
28
+ from .utils import inicializar_y_capturar_excepciones, BaseWS, get_install_dir
29
+
30
+ HOMO = False
31
+ WSDL = "https://wswhomo.afip.gov.ar/wsfexv1/service.asmx?WSDL"
32
+
33
+
34
+ class WSFEXv1(BaseWS):
35
+ "Interfaz para el WebService de Factura ElectrοΏ½nica ExportaciοΏ½n VersiοΏ½n 1"
36
+ _public_methods_ = ['CrearFactura', 'AgregarItem', 'Authorize', 'GetCMP',
37
+ 'AgregarPermiso', 'AgregarCmpAsoc',
38
+ 'GetParamMon', 'GetParamTipoCbte', 'GetParamTipoExpo',
39
+ 'GetParamIdiomas', 'GetParamUMed', 'GetParamIncoterms',
40
+ 'GetParamDstPais', 'GetParamDstCUIT', 'GetParamIdiomas',
41
+ 'GetParamIncoterms', 'GetParamDstCUIT',
42
+ 'GetParamMonConCotizacion',
43
+ 'GetParamPtosVenta', 'GetParamCtz', 'LoadTestXML',
44
+ 'AnalizarXml', 'ObtenerTagXml', 'DebugLog',
45
+ 'SetParametros', 'SetTicketAcceso', 'GetParametro',
46
+ 'GetLastCMP', 'GetLastID',
47
+ 'Dummy', 'Conectar', 'SetTicketAcceso']
48
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
49
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
50
+ 'XmlRequest', 'XmlResponse', 'Version',
51
+ 'Resultado', 'Obs', 'Reproceso',
52
+ 'CAE', 'Vencimiento', 'Eventos', 'ErrCode', 'ErrMsg', 'FchVencCAE',
53
+ 'Excepcion', 'LanzarExcepciones', 'Traceback', "InstallDir",
54
+ 'PuntoVenta', 'CbteNro', 'FechaCbte', 'ImpTotal', 'FchCotiz']
55
+
56
+ _reg_progid_ = "WSFEXv1"
57
+ _reg_clsid_ = "{8106F039-D132-4F87-8AFE-ADE47B5503D4}"
58
+
59
+ # Variables globales para BaseWS:
60
+ HOMO = HOMO
61
+ WSDL = WSDL
62
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
63
+ factura = None
64
+
65
+ def inicializar(self):
66
+ BaseWS.inicializar(self)
67
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
68
+ self.Resultado = self.Motivo = self.Reproceso = ''
69
+ self.LastID = self.LastCMP = self.CAE = self.Vencimiento = ''
70
+ self.CbteNro = self.FechaCbte = self.PuntoVenta = self.ImpTotal = None
71
+ self.InstallDir = INSTALL_DIR
72
+ self.FchVencCAE = "" # retrocompatibilidad
73
+ self.FchCotiz = None
74
+
75
+ def __analizar_errores(self, ret):
76
+ "Comprueba y extrae errores si existen en la respuesta XML"
77
+ if 'FEXErr' in ret:
78
+ errores = [ret['FEXErr']]
79
+ for error in errores:
80
+ self.Errores.append("%s: %s" % (
81
+ error['ErrCode'],
82
+ error['ErrMsg'],
83
+ ))
84
+ self.ErrCode = ' '.join([str(error['ErrCode']) for error in errores])
85
+ self.ErrMsg = '\n'.join(self.Errores)
86
+ if 'FEXEvents' in ret:
87
+ events = [ret['FEXEvents']]
88
+ self.Eventos = ['%s: %s' % (evt['EventCode'], evt.get('EventMsg', "")) for evt in events]
89
+
90
+ def CrearFactura(self, tipo_cbte=19, punto_vta=1, cbte_nro=0, fecha_cbte=None,
91
+ imp_total=0.0, tipo_expo=1, permiso_existente="N", pais_dst_cmp=None,
92
+ nombre_cliente="", cuit_pais_cliente="", domicilio_cliente="",
93
+ id_impositivo="", moneda_id="PES", moneda_ctz=1.0,
94
+ obs_comerciales="", obs_generales="", forma_pago="", incoterms="",
95
+ idioma_cbte=7, incoterms_ds=None, fecha_pago=None, **kwargs):
96
+ "Creo un objeto factura (interna)"
97
+ # Creo una factura electronica de exportaciοΏ½n
98
+
99
+ fact = {'tipo_cbte': tipo_cbte, 'punto_vta': punto_vta,
100
+ 'cbte_nro': cbte_nro, 'fecha_cbte': fecha_cbte,
101
+ 'tipo_doc': 80, 'nro_doc': cuit_pais_cliente,
102
+ 'imp_total': imp_total,
103
+ 'permiso_existente': permiso_existente,
104
+ 'pais_dst_cmp': pais_dst_cmp,
105
+ 'nombre_cliente': nombre_cliente,
106
+ 'domicilio_cliente': domicilio_cliente,
107
+ 'id_impositivo': id_impositivo,
108
+ 'moneda_id': moneda_id, 'moneda_ctz': moneda_ctz,
109
+ 'obs_comerciales': obs_comerciales,
110
+ 'obs_generales': obs_generales,
111
+ 'forma_pago': forma_pago,
112
+ 'incoterms': incoterms,
113
+ 'incoterms_ds': incoterms_ds,
114
+ 'tipo_expo': tipo_expo,
115
+ 'idioma_cbte': idioma_cbte,
116
+ 'cbtes_asoc': [],
117
+ 'permisos': [],
118
+ 'detalles': [],
119
+ 'fecha_pago': fecha_pago,
120
+ }
121
+ self.factura = fact
122
+
123
+ return True
124
+
125
+ def AgregarItem(self, codigo, ds, qty, umed, precio, importe, bonif=None, **kwargs):
126
+ "Agrego un item a una factura (interna)"
127
+ # Nota: no se calcula total (debe venir calculado!)
128
+ self.factura['detalles'].append({
129
+ 'codigo': codigo,
130
+ 'ds': ds,
131
+ 'qty': qty,
132
+ 'umed': umed,
133
+ 'precio': precio,
134
+ 'bonif': bonif,
135
+ 'importe': importe,
136
+ })
137
+ return True
138
+
139
+ def AgregarPermiso(self, id_permiso, dst_merc, **kwargs):
140
+ "Agrego un permiso a una factura (interna)"
141
+ self.factura['permisos'].append({
142
+ 'id_permiso': id_permiso,
143
+ 'dst_merc': dst_merc,
144
+ })
145
+ return True
146
+
147
+ def AgregarCmpAsoc(self, cbte_tipo=19, cbte_punto_vta=0, cbte_nro=0, cbte_cuit=None, **kwargs):
148
+ "Agrego un comprobante asociado a una factura (interna)"
149
+ self.factura['cbtes_asoc'].append({
150
+ 'cbte_tipo': cbte_tipo, 'cbte_punto_vta': cbte_punto_vta,
151
+ 'cbte_nro': cbte_nro, 'cbte_cuit': cbte_cuit})
152
+ return True
153
+
154
+ @inicializar_y_capturar_excepciones
155
+ def Authorize(self, id):
156
+ "Autoriza la factura cargada en memoria"
157
+ f = self.factura
158
+ ret = self.client.FEXAuthorize(
159
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
160
+ Cmp={
161
+ 'Id': id,
162
+ 'Fecha_cbte': f['fecha_cbte'],
163
+ 'Cbte_Tipo': f['tipo_cbte'],
164
+ 'Punto_vta': f['punto_vta'],
165
+ 'Cbte_nro': f['cbte_nro'],
166
+ 'Tipo_expo': f['tipo_expo'],
167
+ 'Permiso_existente': f['permiso_existente'],
168
+ 'Permisos': f['permisos'] and [
169
+ {'Permiso': {
170
+ 'Id_permiso': p['id_permiso'],
171
+ 'Dst_merc': p['dst_merc'],
172
+ }} for p in f['permisos']] or None,
173
+ 'Dst_cmp': f['pais_dst_cmp'],
174
+ 'Cliente': f['nombre_cliente'],
175
+ 'Cuit_pais_cliente': f['nro_doc'],
176
+ 'Domicilio_cliente': f['domicilio_cliente'],
177
+ 'Id_impositivo': f['id_impositivo'],
178
+ 'Moneda_Id': f['moneda_id'],
179
+ 'Moneda_ctz': f['moneda_ctz'],
180
+ 'Obs_comerciales': f['obs_comerciales'],
181
+ 'Imp_total': f['imp_total'],
182
+ 'Obs': f['obs_generales'],
183
+ 'Cmps_asoc': f['cbtes_asoc'] and [
184
+ {'Cmp_asoc': {
185
+ 'Cbte_tipo': c['cbte_tipo'],
186
+ 'Cbte_punto_vta': c['cbte_punto_vta'],
187
+ 'Cbte_nro': c['cbte_nro'],
188
+ 'Cbte_cuit': c['cbte_cuit'],
189
+ }} for c in f['cbtes_asoc']] or None,
190
+ 'Forma_pago': f['forma_pago'],
191
+ 'Incoterms': f['incoterms'],
192
+ 'Incoterms_Ds': f['incoterms_ds'],
193
+ 'Idioma_cbte': f['idioma_cbte'],
194
+ 'Items': [
195
+ {'Item': {
196
+ 'Pro_codigo': d['codigo'],
197
+ 'Pro_ds': d['ds'],
198
+ 'Pro_qty': d['qty'],
199
+ 'Pro_umed': d['umed'],
200
+ 'Pro_precio_uni': d['precio'],
201
+ 'Pro_bonificacion': d['bonif'],
202
+ 'Pro_total_item': d['importe'],
203
+ }} for d in f['detalles']],
204
+ 'Fecha_pago': f['fecha_pago'],
205
+ })
206
+
207
+ result = ret['FEXAuthorizeResult']
208
+ self.__analizar_errores(result)
209
+ if 'FEXResultAuth' in result:
210
+ auth = result['FEXResultAuth']
211
+ # Resultado: A: Aceptado, R: Rechazado
212
+ self.Resultado = auth.get('Resultado', "")
213
+ # Obs:
214
+ self.Obs = auth.get('Motivos_Obs', "")
215
+ self.Reproceso = auth.get('Reproceso', "")
216
+ self.CAE = auth.get('Cae', "")
217
+ self.CbteNro = auth.get('Cbte_nro', "")
218
+ vto = str(auth.get('Fch_venc_Cae', ""))
219
+ self.FchVencCAE = vto
220
+ self.Vencimiento = "%s/%s/%s" % (vto[6:8], vto[4:6], vto[0:4])
221
+ return self.CAE
222
+
223
+ @inicializar_y_capturar_excepciones
224
+ def Dummy(self):
225
+ "Obtener el estado de los servidores de la AFIP"
226
+ result = self.client.FEXDummy()['FEXDummyResult']
227
+ self.__analizar_errores(result)
228
+ self.AppServerStatus = str(result.get('AppServer', ''))
229
+ self.DbServerStatus = str(result.get('DbServer', ''))
230
+ self.AuthServerStatus = str(result.get('AuthServer', ''))
231
+ return True
232
+
233
+ @inicializar_y_capturar_excepciones
234
+ def GetCMP(self, tipo_cbte, punto_vta, cbte_nro):
235
+ "Recuperar los datos completos de un comprobante ya autorizado"
236
+ ret = self.client.FEXGetCMP(
237
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
238
+ Cmp={
239
+ 'Cbte_tipo': tipo_cbte,
240
+ 'Punto_vta': punto_vta,
241
+ 'Cbte_nro': cbte_nro,
242
+ })
243
+ result = ret['FEXGetCMPResult']
244
+ self.__analizar_errores(result)
245
+ if 'FEXResultGet' in result:
246
+ resultget = result['FEXResultGet']
247
+ # Obs, cae y fecha cae
248
+ self.Obs = resultget.get('Obs') and resultget['Obs'].strip(" ") or ''
249
+ self.CAE = resultget.get('Cae', '')
250
+ vto = str(resultget.get('Fch_venc_Cae', ''))
251
+ self.Vencimiento = "%s/%s/%s" % (vto[6:8], vto[4:6], vto[0:4])
252
+ self.FechaCbte = resultget.get('Fecha_cbte', '') # .strftime("%Y/%m/%d")
253
+ self.PuntoVenta = resultget['Punto_vta'] # 4000
254
+ self.Resultado = resultget.get('Resultado', '')
255
+ self.CbteNro = resultget['Cbte_nro']
256
+ self.ImpTotal = str(resultget['Imp_total'])
257
+ return self.CAE
258
+ else:
259
+ return 0
260
+
261
+ @inicializar_y_capturar_excepciones
262
+ def GetLastCMP(self, tipo_cbte, punto_vta):
263
+ "Recuperar οΏ½ltimo nοΏ½mero de comprobante emitido"
264
+ ret = self.client.FEXGetLast_CMP(
265
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit,
266
+ 'Cbte_Tipo': tipo_cbte,
267
+ 'Pto_venta': punto_vta,
268
+ })
269
+ result = ret['FEXGetLast_CMPResult']
270
+ self.__analizar_errores(result)
271
+ if 'FEXResult_LastCMP' in result:
272
+ resultget = result['FEXResult_LastCMP']
273
+ self.CbteNro = resultget.get('Cbte_nro')
274
+ self.FechaCbte = resultget.get('Cbte_fecha') # .strftime("%Y/%m/%d")
275
+ return self.CbteNro
276
+
277
+ @inicializar_y_capturar_excepciones
278
+ def GetLastID(self):
279
+ "Recuperar οΏ½ltimo nοΏ½mero de transacciοΏ½n (ID)"
280
+ ret = self.client.FEXGetLast_ID(
281
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
282
+ result = ret['FEXGetLast_IDResult']
283
+ self.__analizar_errores(result)
284
+ if 'FEXResultGet' in result:
285
+ resultget = result['FEXResultGet']
286
+ return resultget.get('Id')
287
+
288
+ @inicializar_y_capturar_excepciones
289
+ def GetParamUMed(self, sep="|"):
290
+ ret = self.client.FEXGetPARAM_UMed(
291
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
292
+ result = ret['FEXGetPARAM_UMedResult']
293
+ self.__analizar_errores(result)
294
+
295
+ umeds = [] # unidades de medida
296
+ for u in result['FEXResultGet']:
297
+ u = u['ClsFEXResponse_UMed']
298
+ try:
299
+ umed = {'id': u.get('Umed_Id'), 'ds': u.get('Umed_Ds'),
300
+ 'vig_desde': u.get('Umed_vig_desde'),
301
+ 'vig_hasta': u.get('Umed_vig_hasta')}
302
+ except Exception as e:
303
+ print(e)
304
+ if u is None:
305
+ # <ClsFEXResponse_UMed xsi:nil="true"/> WTF!
306
+ umed = {'id': '', 'ds': '', 'vig_desde': '', 'vig_hasta': ''}
307
+ #import pdb; pdb.set_trace()
308
+ # print u
309
+
310
+ umeds.append(umed)
311
+ if sep:
312
+ return [("\t%(id)s\t%(ds)s\t%(vig_desde)s\t%(vig_hasta)s\t"
313
+ % it).replace("\t", sep) for it in umeds]
314
+ else:
315
+ return umeds
316
+
317
+ @inicializar_y_capturar_excepciones
318
+ def GetParamMon(self, sep="|"):
319
+ ret = self.client.FEXGetPARAM_MON(
320
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
321
+ result = ret['FEXGetPARAM_MONResult']
322
+ self.__analizar_errores(result)
323
+
324
+ mons = [] # monedas
325
+ for u in result['FEXResultGet']:
326
+ u = u['ClsFEXResponse_Mon']
327
+ try:
328
+ mon = {'id': u.get('Mon_Id'), 'ds': u.get('Mon_Ds'),
329
+ 'vig_desde': u.get('Mon_vig_desde'),
330
+ 'vig_hasta': u.get('Mon_vig_hasta')}
331
+ except Exception as e:
332
+ print(e)
333
+ if u is None:
334
+ # <ClsFEXResponse_UMed xsi:nil="true"/> WTF!
335
+ mon = {'id': '', 'ds': '', 'vig_desde': '', 'vig_hasta': ''}
336
+ #import pdb; pdb.set_trace()
337
+ # print u
338
+
339
+ mons.append(mon)
340
+ if sep:
341
+ return [("\t%(id)s\t%(ds)s\t%(vig_desde)s\t%(vig_hasta)s\t"
342
+ % it).replace("\t", sep) for it in mons]
343
+ else:
344
+ return mons
345
+
346
+ @inicializar_y_capturar_excepciones
347
+ def GetParamDstPais(self, sep="|"):
348
+ "Recuperador de valores referenciales de cοΏ½digos de PaοΏ½ses"
349
+ ret = self.client.FEXGetPARAM_DST_pais(
350
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
351
+ result = ret['FEXGetPARAM_DST_paisResult']
352
+ self.__analizar_errores(result)
353
+
354
+ ret = []
355
+ for u in result['FEXResultGet']:
356
+ u = u['ClsFEXResponse_DST_pais']
357
+ try:
358
+ r = {'codigo': u.get('DST_Codigo'), 'ds': u.get('DST_Ds'), }
359
+ except Exception as e:
360
+ print(e)
361
+
362
+ ret.append(r)
363
+ if sep:
364
+ return [("\t%(codigo)s\t%(ds)s\t"
365
+ % it).replace("\t", sep) for it in ret]
366
+ else:
367
+ return ret
368
+
369
+ @inicializar_y_capturar_excepciones
370
+ def GetParamDstCUIT(self, sep="|"):
371
+ "Recuperar lista de valores referenciales de CUIT de PaοΏ½ses"
372
+ ret = self.client.FEXGetPARAM_DST_CUIT(
373
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
374
+ result = ret['FEXGetPARAM_DST_CUITResult']
375
+ self.__analizar_errores(result)
376
+
377
+ ret = []
378
+ for u in result['FEXResultGet']:
379
+ u = u['ClsFEXResponse_DST_cuit']
380
+ try:
381
+ r = {'codigo': u.get('DST_CUIT'), 'ds': u.get('DST_Ds'), }
382
+ except Exception as e:
383
+ print(e)
384
+
385
+ ret.append(r)
386
+ if sep:
387
+ return [("\t%(codigo)s\t%(ds)s\t"
388
+ % it).replace("\t", sep) for it in ret]
389
+ else:
390
+ return ret
391
+
392
+ @inicializar_y_capturar_excepciones
393
+ def GetParamTipoCbte(self, sep="|"):
394
+ "Recuperador de valores referenciales de cοΏ½digos de Tipo de comprobantes"
395
+ ret = self.client.FEXGetPARAM_Cbte_Tipo(
396
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
397
+ result = ret['FEXGetPARAM_Cbte_TipoResult']
398
+ self.__analizar_errores(result)
399
+
400
+ ret = []
401
+ for u in result['FEXResultGet']:
402
+ u = u['ClsFEXResponse_Cbte_Tipo']
403
+ try:
404
+ r = {'codigo': u.get('Cbte_Id'),
405
+ 'ds': u.get('Cbte_Ds').replace('\n', '').replace('\r', ''),
406
+ 'vig_desde': u.get('Cbte_vig_desde'),
407
+ 'vig_hasta': u.get('Cbte_vig_hasta')}
408
+ except Exception as e:
409
+ print(e)
410
+
411
+ ret.append(r)
412
+ if sep:
413
+ return [("\t%(codigo)s\t%(ds)s\t%(vig_desde)s\t%(vig_hasta)s\t"
414
+ % it).replace("\t", sep) for it in ret]
415
+ else:
416
+ return ret
417
+
418
+ @inicializar_y_capturar_excepciones
419
+ def GetParamTipoExpo(self, sep="|"):
420
+ "Recuperador de valores referenciales de cοΏ½digos de Tipo de exportaciοΏ½n"
421
+ ret = self.client.FEXGetPARAM_Tipo_Expo(
422
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
423
+ result = ret['FEXGetPARAM_Tipo_ExpoResult']
424
+ self.__analizar_errores(result)
425
+
426
+ ret = []
427
+ for u in result['FEXResultGet']:
428
+ u = u['ClsFEXResponse_Tex']
429
+ try:
430
+ r = {'codigo': u.get('Tex_Id'), 'ds': u.get('Tex_Ds'),
431
+ 'vig_desde': u.get('Tex_vig_desde'),
432
+ 'vig_hasta': u.get('Tex_vig_hasta')}
433
+ except Exception as e:
434
+ print(e)
435
+
436
+ ret.append(r)
437
+ if sep:
438
+ return [("\t%(codigo)s\t%(ds)s\t%(vig_desde)s\t%(vig_hasta)s\t"
439
+ % it).replace("\t", sep) for it in ret]
440
+ else:
441
+ return ret
442
+
443
+ @inicializar_y_capturar_excepciones
444
+ def GetParamIdiomas(self, sep="|"):
445
+ "Recuperar lista de valores referenciales de cοΏ½digos de Idiomas"
446
+ ret = self.client.FEXGetPARAM_Idiomas(
447
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
448
+ result = ret['FEXGetPARAM_IdiomasResult']
449
+ self.__analizar_errores(result)
450
+
451
+ ret = []
452
+ for u in result['FEXResultGet']:
453
+ u = u['ClsFEXResponse_Idi']
454
+ try:
455
+ r = {'codigo': u.get('Idi_Id'), 'ds': u.get('Idi_Ds'),
456
+ 'vig_desde': u.get('Idi_vig_hasta'),
457
+ 'vig_hasta': u.get('Idi_vig_desde')}
458
+ except Exception as e:
459
+ print(e)
460
+
461
+ ret.append(r)
462
+ if sep:
463
+ return [("\t%(codigo)s\t%(ds)s\t%(vig_desde)s\t%(vig_hasta)s\t"
464
+ % it).replace("\t", sep) for it in ret]
465
+ else:
466
+ return ret
467
+
468
+ def GetParamIncoterms(self, sep="|"):
469
+ "Recuperar lista de valores referenciales de Incoterms"
470
+ ret = self.client.FEXGetPARAM_Incoterms(
471
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit, })
472
+ result = ret['FEXGetPARAM_IncotermsResult']
473
+ self.__analizar_errores(result)
474
+
475
+ ret = []
476
+ for u in result['FEXResultGet']:
477
+ u = u['ClsFEXResponse_Inc']
478
+ try:
479
+ r = {'codigo': u.get('Inc_Id'), 'ds': u.get('Inc_Ds'),
480
+ 'vig_desde': u.get('Inc_vig_hasta'),
481
+ 'vig_hasta': u.get('Inc_vig_desde')}
482
+ except Exception as e:
483
+ print(e)
484
+
485
+ ret.append(r)
486
+ if sep:
487
+ return [("\t%(codigo)s\t%(ds)s\t%(vig_desde)s\t%(vig_hasta)s\t"
488
+ % it).replace("\t", sep) for it in ret]
489
+ else:
490
+ return ret
491
+
492
+ @inicializar_y_capturar_excepciones
493
+ def GetParamCtz(self, moneda_id):
494
+ "Recuperador de cotizaciοΏ½n de moneda"
495
+ ret = self.client.FEXGetPARAM_Ctz(
496
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
497
+ Mon_id=moneda_id,
498
+ )
499
+ self.__analizar_errores(ret['FEXGetPARAM_CtzResult'])
500
+ res = ret['FEXGetPARAM_CtzResult'].get('FEXResultGet')
501
+ if res:
502
+ ctz = str(res.get('Mon_ctz', ""))
503
+ self.FchCotiz = res.get("Mon_fecha")
504
+ else:
505
+ ctz = ''
506
+ return ctz
507
+
508
+ @inicializar_y_capturar_excepciones
509
+ def GetParamMonConCotizacion(self, fecha=None, sep="|"):
510
+ "Recupera el listado de monedas que tengan cotizacion de ADUANA"
511
+ if not fecha:
512
+ fecha = datetime.date.today().strftime("%Y%m%d")
513
+
514
+ ret = self.client.FEXGetPARAM_MON_CON_COTIZACION(
515
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
516
+ Fecha_CTZ=fecha)
517
+ result = ret['FEXGetPARAM_MON_CON_COTIZACIONResult']
518
+ self.__analizar_errores(result)
519
+
520
+ mons = [] # monedas
521
+ for u in result['FEXResultGet']:
522
+ u = u['ClsFEXResponse_Mon_CON_Cotizacion']
523
+ try:
524
+ mon = {'id': u.get('Mon_Id'), 'ctz': u.get('Mon_ctz'),
525
+ 'fecha': u.get('Fecha_ctz')}
526
+ except Exception as e:
527
+ print(e)
528
+ if u is None:
529
+ # <ClsFEXResponse_UMed xsi:nil="true"/> WTF!
530
+ mon = {'id':'', 'ctz':'','fecha':''}
531
+ mons.append(mon)
532
+ if sep:
533
+ return [("\t%(id)s\t%(ctz)s\t%(fecha)s\t"
534
+ % it).replace("\t", sep) for it in mons]
535
+ else:
536
+ return mons
537
+
538
+ @inicializar_y_capturar_excepciones
539
+ def GetParamPtosVenta(self, sep="|"):
540
+ "Recupera el listado de los puntos de venta para exportacion y estado"
541
+ ret = self.client.FEXGetPARAM_PtoVenta(
542
+ Auth={'Token': self.Token, 'Sign': self.Sign, 'Cuit': self.Cuit},
543
+ )
544
+ self.__analizar_errores(ret['FEXGetPARAM_PtoVentaResult'])
545
+ res = ret['FEXGetPARAM_PtoVentaResult'].get('FEXResultGet')
546
+ ret = []
547
+ for pu in res:
548
+ p = pu['ClsFEXResponse_PtoVenta']
549
+ try:
550
+ r = {'nro': p.get('Pve_Nro'), 'baja': p.get('Pve_FchBaj'),
551
+ 'bloqueado': p.get('Pve_Bloqueado'), }
552
+ except Exception as e:
553
+ print(e)
554
+ ret.append(r)
555
+ return [("%(nro)s\tBloqueado:%(bloqueado)s\tFchBaja:%(baja)s" % r).replace("\t", sep)
556
+ for r in ret]
557
+
558
+
559
+ class WSFEX(WSFEXv1):
560
+ "Wrapper para retrocompatibilidad con WSFEX"
561
+
562
+ _reg_progid_ = "WSFEX"
563
+ _reg_clsid_ = "{B3C8D3D3-D5DA-44C9-B003-11845803B2BD}"
564
+
565
+ def __init__(self):
566
+ WSFEXv1.__init__(self)
567
+ self.Version = "%s %s WSFEXv1" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
568
+
569
+ def Conectar(self, url="", proxy=""):
570
+ # Ajustar URL de V0 a V1:
571
+ if url in ("https://wswhomo.afip.gov.ar/wsfex/service.asmx",
572
+ "http://wswhomo.afip.gov.ar/WSFEX/service.asmx"):
573
+ url = "https://wswhomo.afip.gov.ar/wsfexv1/service.asmx"
574
+ elif url in ("https://servicios1.afip.gov.ar/wsfex/service.asmx",
575
+ "http://servicios1.afip.gov.ar/WSFEX/service.asmx"):
576
+ url = "https://servicios1.afip.gov.ar/wsfexv1/service.asmx"
577
+ return WSFEXv1.Conectar(self, cache=None, wsdl=url, proxy=proxy)
578
+
579
+
580
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
581
+ INSTALL_DIR = WSFEXv1.InstallDir = get_install_dir()
582
+
583
+
584
+ def p_assert_eq(a, b):
585
+ print(a, a == b and '==' or '!=', b)
586
+
587
+
588
+ if __name__ == "__main__":
589
+
590
+ if "--register" in sys.argv or "--unregister" in sys.argv:
591
+ import win32com.server.register
592
+ win32com.server.register.UseCommandLine(WSFEXv1)
593
+ if '--wsfex' in sys.argv:
594
+ win32com.server.register.UseCommandLine(WSFEX)
595
+ # elif "/Automate" in sys.argv:
596
+ # # MS seems to like /automate to run the class factories.
597
+ # import win32com.server.localserver
598
+ # #win32com.server.localserver.main()
599
+ # # start the server.
600
+ # win32com.server.localserver.serve([WSFEXv1._reg_clsid_])
601
+ else:
602
+
603
+ # Crear objeto interface Web Service de Factura ElectrοΏ½nica de ExportaciοΏ½n
604
+ wsfexv1 = WSFEXv1()
605
+ # Setear token y sing de autorizaciοΏ½n (pasos previos)
606
+
607
+ # obteniendo el TA para pruebas
608
+ from .wsaa import WSAA
609
+ ta = WSAA().Autenticar("wsfex", "reingart.crt", "reingart.key")
610
+ wsfexv1.SetTicketAcceso(ta)
611
+
612
+ # CUIT del emisor (debe estar registrado en la AFIP)
613
+ wsfexv1.Cuit = "20267565393"
614
+
615
+ # Conectar al Servicio Web de FacturaciοΏ½n (producciοΏ½n u homologaciοΏ½n)
616
+ if "--prod" in sys.argv:
617
+ wsdl = "https://servicios1.afip.gov.ar/wsfexv1/service.asmx?WSDL"
618
+ else:
619
+ wsdl = "https://wswhomo.afip.gov.ar/wsfexv1/service.asmx?WSDL"
620
+ cache = proxy = ""
621
+ wrapper = "httplib2"
622
+ cacert = open("conf/afip_ca_info.crt").read()
623
+ ok = wsfexv1.Conectar(cache, wsdl, proxy, wrapper, cacert)
624
+
625
+ if '--dummy' in sys.argv:
626
+ #wsfexv1.LanzarExcepciones = False
627
+ print(wsfexv1.Dummy())
628
+ print("AppServerStatus", wsfexv1.AppServerStatus)
629
+ print("DbServerStatus", wsfexv1.DbServerStatus)
630
+ print("AuthServerStatus", wsfexv1.AuthServerStatus)
631
+
632
+ if "--prueba" in sys.argv:
633
+ try:
634
+ # Establezco los valores de la factura a autorizar:
635
+ tipo_cbte = '--nc' in sys.argv and 21 or 19 # FC/NC Expo (ver tabla de parοΏ½metros)
636
+ punto_vta = 7
637
+ # Obtengo el οΏ½ltimo nοΏ½mero de comprobante y le agrego 1
638
+ cbte_nro = int(wsfexv1.GetLastCMP(tipo_cbte, punto_vta)) + 1
639
+ fecha_cbte = datetime.datetime.now().strftime("%Y%m%d")
640
+ tipo_expo = 1 # tipo de exportaciοΏ½n (ver tabla de parοΏ½metros)
641
+ permiso_existente = (tipo_cbte not in (20, 21) or tipo_expo != 1) and "S" or ""
642
+ print("permiso_existente", permiso_existente)
643
+ dst_cmp = 203 # paοΏ½s destino
644
+ cliente = "Joao Da Silva"
645
+ cuit_pais_cliente = "50000000016"
646
+ domicilio_cliente = "RοΏ½a Ρ°76 km 34.5 Alagoas"
647
+ id_impositivo = "PJ54482221-l"
648
+ moneda_id = "DOL" # para reales, "DOL" o "PES" (ver tabla de parοΏ½metros)
649
+ moneda_ctz = "8.00" # wsfexv1.GetParamCtz('DOL') <- no funciona
650
+ obs_comerciales = "Observaciones comerciales"
651
+ obs = "Sin observaciones"
652
+ forma_pago = "30 dias"
653
+ incoterms = "FOB" # (ver tabla de parοΏ½metros)
654
+ incoterms_ds = "Flete a Bordo"
655
+ idioma_cbte = 1 # (ver tabla de parοΏ½metros)
656
+ imp_total = "250.00"
657
+
658
+ # Creo una factura (internamente, no se llama al WebService):
659
+ ok = wsfexv1.CrearFactura(tipo_cbte, punto_vta, cbte_nro, fecha_cbte,
660
+ imp_total, tipo_expo, permiso_existente, dst_cmp,
661
+ cliente, cuit_pais_cliente, domicilio_cliente,
662
+ id_impositivo, moneda_id, moneda_ctz,
663
+ obs_comerciales, obs, forma_pago, incoterms,
664
+ idioma_cbte, incoterms_ds)
665
+
666
+ # Agrego un item:
667
+ codigo = "PRO1"
668
+ ds = "Producto Tipo 1 Exportacion MERCOSUR ISO 9001"
669
+ qty = 2
670
+ precio = "150.00"
671
+ umed = 1 # Ver tabla de parοΏ½metros (unidades de medida)
672
+ bonif = "50.00"
673
+ imp_total = "250.00" # importe total final del artοΏ½culo
674
+ # lo agrego a la factura (internamente, no se llama al WebService):
675
+ ok = wsfexv1.AgregarItem(codigo, ds, qty, umed, precio, imp_total, bonif)
676
+ ok = wsfexv1.AgregarItem(codigo, ds, qty, umed, precio, imp_total, bonif)
677
+ ok = wsfexv1.AgregarItem(codigo, ds, 0, 99, 0, -float(imp_total), 0)
678
+
679
+ # Agrego un permiso (ver manual para el desarrollador)
680
+ if permiso_existente:
681
+ id = "99999AAXX999999A"
682
+ dst = 225 # paοΏ½s destino de la mercaderia
683
+ ok = wsfexv1.AgregarPermiso(id, dst)
684
+
685
+ # Agrego un comprobante asociado (solo para N/C o N/D)
686
+ if tipo_cbte in (20, 21):
687
+ cbteasoc_tipo = 19
688
+ cbteasoc_pto_vta = 2
689
+ cbteasoc_nro = 1234
690
+ cbteasoc_cuit = 20111111111
691
+ wsfexv1.AgregarCmpAsoc(cbteasoc_tipo, cbteasoc_pto_vta, cbteasoc_nro, cbteasoc_cuit)
692
+
693
+ # id = "99000000000100" # nοΏ½mero propio de transacciοΏ½n
694
+ # obtengo el οΏ½ltimo ID y le adiciono 1
695
+ # (advertencia: evitar overflow y almacenar!)
696
+ id = int(wsfexv1.GetLastID()) + 1
697
+
698
+ # Llamo al WebService de AutorizaciοΏ½n para obtener el CAE
699
+ cae = wsfexv1.Authorize(id)
700
+
701
+ print("Comprobante", tipo_cbte, wsfexv1.CbteNro)
702
+ print("Resultado", wsfexv1.Resultado)
703
+ print("CAE", wsfexv1.CAE)
704
+ print("Vencimiento", wsfexv1.Vencimiento)
705
+
706
+ if wsfexv1.Resultado and False:
707
+ print(wsfexv1.client.help("FEXGetCMP").encode("latin1"))
708
+ wsfexv1.GetCMP(tipo_cbte, punto_vta, cbte_nro)
709
+ print("CAE consulta", wsfexv1.CAE, wsfexv1.CAE == cae)
710
+ print("NRO consulta", wsfexv1.CbteNro, wsfexv1.CbteNro == cbte_nro)
711
+ print("TOTAL consulta", wsfexv1.ImpTotal, wsfexv1.ImpTotal == imp_total)
712
+
713
+ except Exception as e:
714
+ print(wsfexv1.XmlRequest)
715
+ print(wsfexv1.XmlResponse)
716
+ print(wsfexv1.ErrCode)
717
+ print(wsfexv1.ErrMsg)
718
+ print(wsfexv1.Excepcion)
719
+ print(wsfexv1.Traceback)
720
+ raise
721
+
722
+ if "--get" in sys.argv:
723
+ wsfexv1.client.help("FEXGetCMP")
724
+ tipo_cbte = 19
725
+ punto_vta = 7
726
+ cbte_nro = wsfexv1.GetLastCMP(tipo_cbte, punto_vta)
727
+
728
+ wsfexv1.GetCMP(tipo_cbte, punto_vta, cbte_nro)
729
+
730
+ print("FechaCbte = ", wsfexv1.FechaCbte)
731
+ print("CbteNro = ", wsfexv1.CbteNro)
732
+ print("PuntoVenta = ", wsfexv1.PuntoVenta)
733
+ print("ImpTotal =", wsfexv1.ImpTotal)
734
+ print("CAE = ", wsfexv1.CAE)
735
+ print("Vencimiento = ", wsfexv1.Vencimiento)
736
+
737
+ wsfexv1.AnalizarXml("XmlResponse")
738
+ p_assert_eq(wsfexv1.ObtenerTagXml('Cae'), str(wsfexv1.CAE))
739
+ p_assert_eq(wsfexv1.ObtenerTagXml('Fecha_cbte'), wsfexv1.FechaCbte)
740
+ p_assert_eq(wsfexv1.ObtenerTagXml('Moneda_Id'), "DOL")
741
+ p_assert_eq(wsfexv1.ObtenerTagXml('Moneda_ctz'), "8")
742
+ p_assert_eq(wsfexv1.ObtenerTagXml('Id_impositivo'), "PJ54482221-l")
743
+
744
+ if "--params" in sys.argv:
745
+ import codecs
746
+ import locale
747
+ sys.stdout = codecs.getwriter('latin1')(sys.stdout)
748
+
749
+ print("=== Incoterms ===")
750
+ idiomas = wsfexv1.GetParamIncoterms(sep="||")
751
+ for idioma in idiomas:
752
+ print(idioma)
753
+
754
+ print("=== Idiomas ===")
755
+ idiomas = wsfexv1.GetParamIdiomas(sep="||")
756
+ for idioma in idiomas:
757
+ print(idioma)
758
+
759
+ print("=== Tipos Comprobantes ===")
760
+ tipos = wsfexv1.GetParamTipoCbte(sep=False)
761
+ for t in tipos:
762
+ print("||%(codigo)s||%(ds)s||" % t)
763
+
764
+ print("=== Tipos Expo ===")
765
+ tipos = wsfexv1.GetParamTipoExpo(sep=False)
766
+ for t in tipos:
767
+ print("||%(codigo)s||%(ds)s||%(vig_desde)s||%(vig_hasta)s||" % t)
768
+ #umeds = dict([(u.get('id', ""),u.get('ds', "")) for u in umedidas])
769
+
770
+ print("=== Monedas ===")
771
+ mons = wsfexv1.GetParamMon(sep=False)
772
+ for m in mons:
773
+ print("||%(id)s||%(ds)s||%(vig_desde)s||%(vig_hasta)s||" % m)
774
+ #umeds = dict([(u.get('id', ""),u.get('ds', "")) for u in umedidas])
775
+
776
+ print("=== Unidades de medida ===")
777
+ umedidas = wsfexv1.GetParamUMed(sep=False)
778
+ for u in umedidas:
779
+ print("||%(id)s||%(ds)s||%(vig_desde)s||%(vig_hasta)s||" % u)
780
+ umeds = dict([(u.get('id', ""), u.get('ds', "")) for u in umedidas])
781
+
782
+ print("=== CοΏ½digo Pais Destino ===")
783
+ ret = wsfexv1.GetParamDstPais(sep=False)
784
+ for r in ret:
785
+ print("||%(codigo)s||%(ds)s||" % r)
786
+
787
+ print("=== CUIT Pais Destino ===")
788
+ ret = wsfexv1.GetParamDstCUIT(sep=False)
789
+ for r in ret:
790
+ print("||%(codigo)s||%(ds)s||" % r)
791
+
792
+ if "--ctz" in sys.argv:
793
+ print(wsfexv1.GetParamCtz('DOL'))
794
+
795
+ if "--monctz" in sys.argv:
796
+ print(wsfexv1.GetParamMonConCotizacion())
797
+
798
+ if "--ptosventa" in sys.argv:
799
+ print(wsfexv1.GetParamPtosVenta())
app/pyafipws/wslpg.py ADDED
The diff for this file is too large to render. See raw diff
 
app/pyafipws/wslpg_datos.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ from decimal import Decimal
4
+
5
+ TIPOS_OP = {1: 'Compraventa de granos', 2: 'ConsignaciΓ³n de granos'}
6
+ GRANOS = {
7
+ 1: 'LINO', 2: 'GIRASOL', 3: 'MANI EN CAJA',
8
+ 4: 'GIRASOL DESCASCARADO', 5: 'MANI PARA INDUSTRIA DE SELECCION',
9
+ 6: 'MANI PARA INDUSTRIA ACEITERA', 7: 'MANI TIPO CONFITERIA',
10
+ 8: 'COLZA', 9: 'COLZA 00 CANOLA', 10: 'TRIGO FORRAJERO',
11
+ 11: 'CEBADA FORRAJERA', 12: 'CEBADA APTA PARA MALTERIA',
12
+ 14: 'TRIGO CANDEAL', 15: 'TRIGO PAN',
13
+ 16: 'AVENA', 17: 'CEBADA CERVECERA', 18: 'CENTENO',
14
+ 19: 'MAIZ', 20: 'MIJO', 21: 'ARROZ CASCARA',
15
+ 22: 'SORGO GRANIFERO', 23: 'SOJA', 25: 'TRIGO PLATA',
16
+ 26: 'MAIZ FLYNT O PLATA', 27: 'MAIZ PISINGALLO',
17
+ 28: 'TRITICALE', 30: 'ALPISTE', 31: 'ALGODON', 32: 'CARTAMO',
18
+ 33: 'POROTO BLANCO NATURAL OVAL Y ALUBIA',
19
+ 34: 'POROTO DISTINTO DEL BLANCO OVAL Y ALUBIA',
20
+ 35: 'ARROZ', 46: 'LENTEJA', 47: 'ARVEJA',
21
+ 48: 'POROTO BLANCO SELECCIONADO OVAL Y ALUBIA',
22
+ 49: 'OTRAS LEGUMBRES', 50: 'OTROS GRANOS', 59: 'GARBANZO', }
23
+
24
+ PUERTOS = {1: "SAN LORENZO/SAN MARTIN", 2: "ROSARIO",
25
+ 3: "BAHIA BLANCA", 4: "NECOCHEA", 5: "RAMALLO", 6: "LIMA",
26
+ 7: "DIAMANTE", 8: "BUENOS AIRES", 9: "SAN PEDRO",
27
+ 10: "SAN NICOLAS", 11: "TERMINAL DEL GUAZU", 12: "ZARATE",
28
+ 13: "VILLA CONSTITUCION"}
29
+
30
+ PROVINCIAS = {1: 'BUENOS AIRES', 0: 'CAPITAL FEDERAL',
31
+ 2: 'CATAMARCA', 16: 'CHACO', 17: 'CHUBUT',
32
+ 4: 'CORRIENTES', 3: 'CΓ“RDOBA', 5: 'ENTRE RIOS',
33
+ 18: 'FORMOSA', 6: 'JUJUY', 21: 'LA PAMPA',
34
+ 8: 'LA RIOJA', 7: 'MENDOZA', 19: 'MISIONES',
35
+ 20: 'NEUQUÉN', 22: 'RIO NEGRO', 9: 'SALTA',
36
+ 10: 'SAN JUAN', 11: 'SAN LUIS', 23: 'SANTA CRUZ',
37
+ 12: 'SANTA FE', 13: 'SANTIAGO DEL ESTERO',
38
+ 24: 'TIERRA DEL FUEGO', 14: 'TUCUMÁN'}
39
+
40
+ TIPO_CERT_DEP = {1: "F1116/RT", 5: "F1116/A", 332: "Cert.Elec."}
41
+
42
+ CAMPANIAS = {1213: "2012/2013", 1112: "2011/2012", 1011: "2010/2011",
43
+ 910: "2009/2010", 809: "2008/2009", 708: "2007/2008",
44
+ 607: "2006/2007", 506: "2005/2006", 405: "2004/2005",
45
+ 304: "2003/2004", 1314: "2013/2014", 1415: "2014/2015"}
46
+
47
+ ACTIVIDADES = {41: "FRACCIONADOR DE GRANOS", 29: "ACOPIADOR - CONSIGNATARIO",
48
+ 33: "CANJEADOR DE BIENES Y/O SERVICIOS POR GRANO",
49
+ 40: "EXPORTADOR", 31: "ACOPIADOR DE MANÍ",
50
+ 30: "ACOPIADOR DE LEGUMBRES",
51
+ 35: "COMPRADOR DE GRANO PARA CONSUMO PROPIO",
52
+ 44: "INDUSTRIAL ACEITERO", 47: "INDUSTRIAL BIOCOMBUSTIBLE",
53
+ 46: "INDUSTRIAL BALANCEADOR", 48: "INDUSTRIAL CERVECERO",
54
+ 49: "INDUSTRIAL DESTILERIA",
55
+ 51: "INDUSTRIAL MOLINO DE HARINA DE TRIGO",
56
+ 50: "INDUSTRIAL MOLINERO", 45: "INDUSTRIAL ARROCERO",
57
+ 59: "USUARIO DE MOLIENDA DE TRIGO(incluye MAQUILA)",
58
+ 57: "USUARIO DE INDUSTRIA (Otros granos MENOS trigo)",
59
+ 52: "INDUSTRIAL SELECCIONADOR", 34: "COMPLEJO INDUSTRIAL",
60
+ 28: "ACONDICIONADOR", 36: "CORREDOR",
61
+ 55: "MERCADO DE FUTUROS Y OPCIONES O MERCADO A TERMINO",
62
+ 39: "EXPLOTADOR DE DEPOSITO Y/O ELEVADOR DE GRANOS",
63
+ 37: "DESMOTADOR DE ALGODON",
64
+ }
65
+
66
+ # Grados
67
+ GRADOS_REF = {'G3': 'Grado 3', 'G2': 'Grado 2', 'G1': 'Grado 1'}
68
+
69
+ # Datos de grado entregado por tipo de granos:
70
+ GRADO_ENT_VALOR = {
71
+ 49: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
72
+ 25: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
73
+ 26: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
74
+ 27: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
75
+ 20: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
76
+ 21: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
77
+ 22: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
78
+ 23: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
79
+ 46: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
80
+ 47: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
81
+ 48: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
82
+ 28: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
83
+ 1: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
84
+ 3: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
85
+ 2: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
86
+ 5: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
87
+ 4: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
88
+ 7: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
89
+ 6: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
90
+ 9: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
91
+ 8: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
92
+ 59: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
93
+ 11: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
94
+ 10: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
95
+ 12: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
96
+ 15: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.99'), 'G2': Decimal('1.00'), 'G1': Decimal('1.015'), 'FG': Decimal('0')},
97
+ 14: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
98
+ 17: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
99
+ 16: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
100
+ 19: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
101
+ 18: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
102
+ 31: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
103
+ 30: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
104
+ 50: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
105
+ 35: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
106
+ 34: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
107
+ 33: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
108
+ 32: {'F1': Decimal('0'), 'F2': Decimal('0'), 'F3': Decimal('0'), 'G3': Decimal('0.985'), 'G2': Decimal('1.00'), 'G1': Decimal('1.01'), 'FG': Decimal('0')},
109
+ }
110
+
111
+ # Diccionario de localidades por provincia
112
+ # (wslpg.py lo reemplaza con un shelve si es posible)
113
+ LOCALIDADES = {}
app/pyafipws/wslsp.py ADDED
@@ -0,0 +1,1210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+
14
+ import shelve
15
+ import sys
16
+ import datetime
17
+ import decimal
18
+ import os
19
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, BaseWS, inicializar_y_capturar_excepciones, get_install_dir
20
+ from . import utils
21
+ from fpdf import Template
22
+ from pysimplesoap.client import SoapFault
23
+ import pprint
24
+ import traceback
25
+ """MΓ³dulo para obtener cΓ³digo de autorizaciΓ³n electrΓ³nica (CAE) para
26
+ LiquidaciΓ³n Sector Pecuario (hacienda/carne) del web service WSLSP de AFIP
27
+ """
28
+
29
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
30
+ __copyright__ = "Copyright (C) 2016 Mariano Reingart"
31
+ __license__ = "GPL 3.0"
32
+ __version__ = "1.07a"
33
+
34
+ LICENCIA = """
35
+ wslsp.py: Interfaz para generar CΓ³digo de AutorizaciΓ³n ElectrΓ³nica (CAE) para
36
+ LiquidaciΓ³n Sector Pecuario (LspService)
37
+ Copyright (C) 2016 Mariano Reingart reingart@gmail.com
38
+ http://www.sistemasagiles.com.ar/trac/wiki/LiquidacionSectorPecuario
39
+
40
+ Este progarma es software libre, se entrega ABSOLUTAMENTE SIN GARANTIA
41
+ y es bienvenido a redistribuirlo respetando la licencia GPLv3.
42
+
43
+ Para informaciΓ³n adicional sobre garantΓ­a, soporte tΓ©cnico comercial
44
+ e incorporaciΓ³n/distribuciΓ³n en programas propietarios ver PyAfipWs:
45
+ http://www.sistemasagiles.com.ar/trac/wiki/PyAfipWs
46
+ """
47
+
48
+ AYUDA = """
49
+ Opciones:
50
+ --ayuda: este mensaje
51
+
52
+ --debug: modo depuraciΓ³n (detalla y confirma las operaciones)
53
+ --formato: muestra el formato de los archivos de entrada/salida
54
+ --prueba: genera y autoriza una liquidaciΓ³n de prueba (no usar en producciΓ³n!)
55
+ --xml: almacena los requerimientos y respuestas XML (depuraciΓ³n)
56
+ --json: utilizar formato json para el archivo de intercambio
57
+ --dummy: consulta estado de servidores
58
+
59
+ --autorizar: Autorizar Liquidación Única (generarLiquidacion)
60
+ --ajustar: Ajuste FΓ­sico/Monetario/Financiero, Credito/Debito (generarAjuste)
61
+ --ult: Consulta el ΓΊltimo nΓΊmero de orden registrado en AFIP
62
+ (consultarUltimoComprobanteXPuntoVenta)
63
+ --consultar: Consulta una liquidaciΓ³n registrada en AFIP
64
+ (consultarLiquidacionXNroComprobante / consultarLiquidacionXCAE)
65
+
66
+ --provincias: obtiene el listado de provincias (cΓ³digo/descripciΓ³n)
67
+ --localidades: obtiene el listado de localidades para una provincia
68
+ --tributos: obtiene el listado de los tipos de tributos
69
+ --gastos: obtiene el listado de los tipos de gastos
70
+ --puntosventa: obtiene el listado de puntos de venta habilitados
71
+
72
+ Ver wslsp.ini para parΓ‘metros de configuraciΓ³n (URL, certificados, etc.)"
73
+ """
74
+
75
+
76
+ # importo funciones compartidas:
77
+
78
+
79
+ WSDL = "https://fwshomo.afip.gov.ar/wslsp/LspService?wsdl"
80
+ #WSDL = "https://serviciosjava.afip.gov.ar/wslsp/LspService?wsdl"
81
+
82
+ DEBUG = False
83
+ XML = False
84
+ CONFIG_FILE = "wslsp.ini"
85
+ HOMO = False
86
+
87
+
88
+ class WSLSP(BaseWS):
89
+ "Interfaz para el WebService de Liquidación Única Mensual (lechería)"
90
+ _public_methods_ = ['Conectar', 'Dummy', 'SetTicketAcceso', 'DebugLog',
91
+ 'AutorizarLiquidacion',
92
+ 'CrearLiquidacion', 'AgregarFrigorifico',
93
+ 'AgregarEmisor', 'AgregarReceptor', 'AgregarOperador',
94
+ 'AgregarDTE',
95
+ 'AgregarItemDetalle', 'AgregarCompraAsociada',
96
+ 'AgregarGasto', 'AgregarTributo', 'AgregarGuia',
97
+ 'ConsultarLiquidacion', 'ConsultarUltimoComprobante',
98
+ 'CrearAjuste', 'AgregarComprobanteAAjustar',
99
+ 'AgregarItemDetalleAjuste',
100
+ 'AgregarAjusteMonetario', 'AgregarAjusteFisico',
101
+ 'AgregarAjusteFinanciero',
102
+ 'AjustarLiquidacion',
103
+ 'LeerDatosLiquidacion',
104
+ 'ConsultarOperaciones',
105
+ 'ConsultarTiposComprobante',
106
+ 'ConsultarTiposLiquidacion',
107
+ 'ConsultarCategorias', 'ConsultarMotivos',
108
+ 'ConsultarRazas', 'ConsultarCortes',
109
+ 'ConsultarCaracteresParticipante',
110
+ 'ConsultarGastos', 'ConsultarTributos',
111
+ 'ConsultarPuntosVentas',
112
+ 'ConsultarProvincias', 'ConsultarLocalidades',
113
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
114
+ 'SetParametros', 'SetParametro', 'GetParametro',
115
+ ]
116
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
117
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
118
+ 'Excepcion', 'ErrCode', 'ErrMsg', 'LanzarExcepciones', 'Errores',
119
+ 'XmlRequest', 'XmlResponse', 'Version', 'Traceback', 'InstallDir',
120
+ 'CAE', 'NroComprobante', 'FechaComprobante',
121
+ 'NroCodigoBarras', 'FechaVencimientoCae', 'FechaProcesoAFIP',
122
+ 'ImporteBruto', 'ImporteIVASobreBruto', '',
123
+ 'ImporteTotalGastos', 'ImporteIVASobreGastos',
124
+ 'ImporteTotalTributos', 'ImporteTotalNeto',
125
+ ]
126
+ _reg_progid_ = "WSLSP"
127
+ _reg_clsid_ = "{9750BBD4-FBC3-4FE7-8DE5-E193667D6813}"
128
+
129
+ # Variables globales para BaseWS:
130
+ HOMO = HOMO
131
+ WSDL = WSDL
132
+ LanzarExcepciones = False
133
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
134
+
135
+ def inicializar(self):
136
+ BaseWS.inicializar(self)
137
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
138
+ self.errores = []
139
+ self.CAE = ""
140
+ self.NroComprobante = self.FechaComprobante = self.FechaProcesoAFIP = ''
141
+ self.NroCodigoBarras = self.FechaVencimientoCae = None
142
+ self.ImporteBruto = self.ImporteIVASobreBruto = None
143
+ self.ImporteTotalGastos = self.ImporteIVASobreGastos = None
144
+ self.ImporteTotalTributos = self.ImporteTotalNeto = None
145
+ self.datos = {}
146
+
147
+ @inicializar_y_capturar_excepciones
148
+ def Conectar(self, cache=None, url="", proxy="", wrapper="", cacert=None, timeout=60):
149
+ "Establecer la conexiΓ³n a los servidores de la AFIP"
150
+ # llamo al constructor heredado:
151
+ ok = BaseWS.Conectar(self, cache, url, proxy, wrapper, cacert, timeout)
152
+ if False and ok:
153
+ # corrijo ubicaciΓ³n del servidor (puerto htttp 80 en el WSDL)
154
+ location = self.client.services['LspService']['ports']['LumEndPoint']['location']
155
+ if location.startswith("http://"):
156
+ print("Corrigiendo WSDL ...", location, end=' ')
157
+ location = location.replace("http://", "https://").replace(":80", ":443")
158
+ self.client.services['LspService']['ports']['LspEndPoint']['location'] = location
159
+ print(location)
160
+ return ok
161
+
162
+ def __analizar_errores(self, ret):
163
+ "Comprueba y extrae errores si existen en la respuesta XML"
164
+ errores = []
165
+ if 'errores' in ret:
166
+ errores.extend(ret['errores'])
167
+ if errores:
168
+ self.Errores = ["%(codigo)s: %(descripcion)s" % err['error'][0]
169
+ for err in errores]
170
+ self.errores = [
171
+ {'codigo': err['error'][0]['codigo'],
172
+ 'descripcion': err['error'][0]['descripcion'].replace("\n", "")
173
+ .replace("\r", "")}
174
+ for err in errores]
175
+ self.ErrCode = ' '.join(self.Errores)
176
+ self.ErrMsg = '\n'.join(self.Errores)
177
+
178
+ @inicializar_y_capturar_excepciones
179
+ def Dummy(self):
180
+ "Obtener el estado de los servidores de la AFIP"
181
+ results = self.client.dummy()['respuesta']
182
+ self.AppServerStatus = str(results['appserver'])
183
+ self.DbServerStatus = str(results['dbserver'])
184
+ self.AuthServerStatus = str(results['authserver'])
185
+ return True
186
+
187
+ @inicializar_y_capturar_excepciones
188
+ def CrearLiquidacion(self, cod_operacion, fecha_cbte, fecha_op, cod_motivo,
189
+ cod_localidad_procedencia, cod_provincia_procedencia,
190
+ cod_localidad_destino, cod_provincia_destino,
191
+ lugar_realizacion=None,
192
+ fecha_recepcion=None, fecha_faena=None,
193
+ datos_adicionales=None, **kwargs):
194
+ "Inicializa internamente los datos de una liquidaciΓ³n para autorizar"
195
+ # creo el diccionario con los campos generales de la liquidaciΓ³n:
196
+ liq = {'fechaComprobante': fecha_cbte, 'fechaOperacion': fecha_op,
197
+ 'lugarRealizacion': lugar_realizacion,
198
+ 'codMotivo': cod_motivo,
199
+ 'codLocalidadProcedencia': cod_localidad_procedencia,
200
+ 'codProvinciaProcedencia': cod_provincia_procedencia,
201
+ 'codLocalidadDestino': cod_localidad_destino,
202
+ 'codProvinciaDestino': cod_provincia_destino,
203
+ 'fechaRecepcion': fecha_recepcion,
204
+ 'fechaFaena': fecha_faena,
205
+ }
206
+ self.solicitud = dict(codOperacion=cod_operacion,
207
+ emisor={}, receptor={},
208
+ datosLiquidacion=liq,
209
+ itemDetalleLiquidacion=[],
210
+ guia=[], dte=[],
211
+ tributo=[], gasto=[],
212
+ datosAdicionales=datos_adicionales,
213
+ )
214
+ return True
215
+
216
+ @inicializar_y_capturar_excepciones
217
+ def AgregarFrigorifico(self, cuit, nro_planta):
218
+ "Agrego el frigorifico a la liquidacΓ­on (opcional)."
219
+ frig = {'cuit': cuit, 'nroPlanta': nro_planta}
220
+ self.solicitud['datosLiquidacion']['frigorifico'] = frig
221
+ return True
222
+
223
+ @inicializar_y_capturar_excepciones
224
+ def AgregarEmisor(self, tipo_cbte, pto_vta, nro_cbte, cod_caracter=None,
225
+ fecha_inicio_act=None, iibb=None, nro_ruca=None,
226
+ nro_renspa=None, cuit_autorizado=None, **kwargs):
227
+ "Agrego los datos del emisor a la liq."
228
+ # cod_caracter y fecha_inicio_act no es requerido para ajustes
229
+ d = {'tipoComprobante': tipo_cbte, 'puntoVenta': pto_vta,
230
+ 'nroComprobante': nro_cbte,
231
+ 'codCaracter': cod_caracter,
232
+ 'fechaInicioActividades': fecha_inicio_act,
233
+ 'iibb': iibb,
234
+ 'nroRUCA': nro_ruca,
235
+ 'nroRenspa': nro_renspa,
236
+ 'cuitAutorizado': cuit_autorizado}
237
+ self.solicitud['emisor'].update(d)
238
+ return True
239
+
240
+ @inicializar_y_capturar_excepciones
241
+ def AgregarReceptor(self, cod_caracter, **kwargs):
242
+ "Agrego los datos del receptor a la liq."
243
+ d = {'codCaracter': cod_caracter}
244
+ self.solicitud['receptor'].update(d)
245
+ return True
246
+
247
+ @inicializar_y_capturar_excepciones
248
+ def AgregarOperador(self, cuit, iibb=None, nro_ruca=None, nro_renspa=None,
249
+ cuit_autorizado=None, **kwargs):
250
+ "Agrego los datos del operador a la liq."
251
+ d = {'cuit': cuit,
252
+ 'iibb': iibb,
253
+ 'nroRUCA': nro_ruca,
254
+ 'nroRenspa': nro_renspa,
255
+ 'cuitAutorizado': cuit_autorizado}
256
+ self.solicitud['receptor']['operador'] = d
257
+ return True
258
+
259
+ @inicializar_y_capturar_excepciones
260
+ def AgregarItemDetalle(self, cuit_cliente, cod_categoria, tipo_liquidacion,
261
+ cantidad, precio_unitario, alicuota_iva, cod_raza,
262
+ cantidad_cabezas=None, nro_tropa=None,
263
+ cod_corte=None, cantidad_kg_vivo=None,
264
+ precio_recupero=None, detalle_raza=None,
265
+ nro_item=None,
266
+ **kwargs):
267
+ "Agrega el detalle de item de la liquidaciΓ³n"
268
+ d = {'cuitCliente': cuit_cliente,
269
+ 'codCategoria': cod_categoria,
270
+ 'tipoLiquidacion': tipo_liquidacion,
271
+ 'cantidad': cantidad,
272
+ 'precioUnitario': precio_unitario,
273
+ 'alicuotaIVA': alicuota_iva,
274
+ 'cantidadCabezas': cantidad_cabezas,
275
+ 'raza': {'codRaza': cod_raza, 'detalle': detalle_raza},
276
+ 'nroTropa': nro_tropa,
277
+ 'codCorte': cod_corte,
278
+ 'cantidadKgVivo': cantidad_kg_vivo,
279
+ 'precioRecupero': precio_recupero,
280
+ 'liquidacionCompraAsociada': [],
281
+ 'nroItem': nro_item,
282
+ }
283
+ self.solicitud['itemDetalleLiquidacion'].append(d)
284
+ return True
285
+
286
+ @inicializar_y_capturar_excepciones
287
+ def AgregarCompraAsociada(self, tipo_cbte, pto_vta, nro_cbte, cant_asoc, nro_item):
288
+ "Agrega la informaciΓ³n referente a la liquidaciΓ³n compra asociada"
289
+ d = {'tipoComprobante': tipo_cbte,
290
+ 'puntoVenta': pto_vta,
291
+ 'nroComprobante': nro_cbte,
292
+ 'nroItem': nro_item,
293
+ 'cantidadAsociada': cant_asoc}
294
+ if 'itemDetalleLiquidacion' in self.solicitud:
295
+ item_liq = self.solicitud['itemDetalleLiquidacion'][-1]
296
+ item_liq['liquidacionCompraAsociada'].append(d)
297
+ else:
298
+ item_liq = self.solicitud['itemDetalleAjusteLiquidacion'][-1]
299
+ item_liq['ajusteCompraAsociada'].append(d)
300
+ return True
301
+
302
+ @inicializar_y_capturar_excepciones
303
+ def AgregarGasto(self, cod_gasto, descripcion=None, base_imponible=None,
304
+ alicuota=None, importe=None, alicuota_iva=None,
305
+ tipo_iva_nulo=None):
306
+ "Agrega la informaciΓ³n referente a los gastos de la liquidaciΓ³n"
307
+ # WSLSPv1.4.1: tipo_iva_nulo debe ser NG, NA, EX: Exento.
308
+ if alicuota_iva == 0:
309
+ alicuota_iva = None # sΓ³lo acepta [10.5, 21.0]
310
+ elif alicuota_iva:
311
+ tipo_iva_nulo = None
312
+ gasto = {'codGasto': cod_gasto, 'descripcion': descripcion,
313
+ 'baseImponible': base_imponible, 'alicuota': alicuota,
314
+ 'importe': importe, 'alicuotaIVA': alicuota_iva,
315
+ 'tipoIVANulo': tipo_iva_nulo,
316
+ }
317
+ if 'ajusteFinanciero' in self.solicitud:
318
+ self.solicitud['ajusteFinanciero']['gasto'].append(gasto)
319
+ else:
320
+ self.solicitud['gasto'].append(gasto)
321
+ return True
322
+
323
+ @inicializar_y_capturar_excepciones
324
+ def AgregarTributo(self, cod_tributo, descripcion=None,
325
+ base_imponible=None, alicuota=None, importe=None):
326
+ "Agrega la informaciΓ³n referente a los tributos de la liquidaciΓ³n"
327
+ trib = {'codTributo': cod_tributo, 'descripcion': descripcion,
328
+ 'baseImponible': base_imponible, 'alicuota': alicuota,
329
+ 'importe': importe}
330
+ if 'ajusteFinanciero' in self.solicitud:
331
+ self.solicitud['ajusteFinanciero']['tributo'].append(trib)
332
+ else:
333
+ self.solicitud['tributo'].append(trib)
334
+ return True
335
+
336
+ @inicializar_y_capturar_excepciones
337
+ def AgregarDTE(self, nro_dte, nro_renspa=None):
338
+ "Agrega la informaciΓ³n referente a DTE (multiples)"
339
+ d = {"nroDTE": nro_dte, "nroRenspa": nro_renspa}
340
+ self.solicitud['dte'].append(d)
341
+ return True
342
+
343
+ @inicializar_y_capturar_excepciones
344
+ def AgregarGuia(self, nro_guia):
345
+ "Agrega la informaciΓ³n referente a las guΓ­as (multiples)"
346
+ self.solicitud['guia'].append({"nroGuia": nro_guia})
347
+ return True
348
+
349
+ @inicializar_y_capturar_excepciones
350
+ def AutorizarLiquidacion(self):
351
+ "Generar o ajustar una liquidaciΓ³n ΓΊnica y obtener del CAE"
352
+ # limpio los elementos que no correspondan por estar vacios:
353
+ for campo in ["guia", "dte", "gasto", "tributo"]:
354
+ if campo in self.solicitud and not self.solicitud[campo]:
355
+ del self.solicitud[campo]
356
+ for item in self.solicitud['itemDetalleLiquidacion']:
357
+ if not item.get("liquidacionCompraAsociada", True):
358
+ del item["liquidacionCompraAsociada"]
359
+ # llamo al webservice:
360
+ ret = self.client.generarLiquidacion(
361
+ auth={
362
+ 'token': self.Token, 'sign': self.Sign,
363
+ 'cuit': self.Cuit, },
364
+ solicitud=self.solicitud,
365
+ )
366
+ # analizo la respusta
367
+ ret = ret['respuesta']
368
+ self.__analizar_errores(ret)
369
+ self.AnalizarLiquidacion(ret)
370
+ return True
371
+
372
+ def AnalizarLiquidacion(self, liq):
373
+ "MΓ©todo interno para analizar la respuesta de AFIP"
374
+ # proceso los datos bΓ‘sicos de la liquidaciΓ³n (devuelto por consultar):
375
+ cab = liq.get('cabecera')
376
+ if cab:
377
+ self.CAE = str(cab.get('cae', ''))
378
+ self.FechaVencimientoCae = str(cab.get('fechaVencimientoCae', ''))
379
+ self.FechaProcesoAFIP = str(cab.get('fechaProcesoAFIP', ''))
380
+ self.NroCodigoBarras = cab.get('nroCodigoBarras')
381
+ datos = liq.get('datosLiquidacion', {})
382
+ self.FechaComprobante = str(datos.get('fechaComprobante', ''))
383
+ emisor = liq.get('emisor', {})
384
+ self.NroComprobante = emisor.get('nroComprobante')
385
+ tot = liq.get('resumenTotales', {})
386
+ self.ImporteBruto = tot.get('importeBruto')
387
+ self.ImporteTotalGastos = tot.get('importeTotalGastos')
388
+ self.ImporteTotalTributos = tot.get('importeTotalTributos')
389
+ self.ImporteTotalNeto = tot.get('importeTotalNeto')
390
+ self.ImporteIVASobreBruto = tot.get('importeIVASobreBruto')
391
+ self.ImporteIVASobreGastos = tot.get('importeIVASobreGastos')
392
+ receptor = liq.get('receptor', {})
393
+
394
+ # parΓ‘metros de salida:
395
+ self.params_out = dict(
396
+ tipo_cbte=emisor.get('tipoComprobante'),
397
+ pto_vta=emisor.get('puntoVenta'),
398
+ nro_cbte=emisor.get('nroComprobante'),
399
+ fecha=datos.get('fechaComprobante'),
400
+ cae=cab.get('cae'),
401
+ emisor=dict(
402
+ razon_social=emisor.get('razonSocial'),
403
+ domicilio_punto_venta=emisor.get('domicilioPuntoVenta'),
404
+ ),
405
+ receptor=dict(
406
+ nombre=receptor.get('nombre'),
407
+ domicilio=receptor.get('domicilio'),
408
+ ),
409
+ bruto=tot.get('importeBruto'),
410
+ iva_bruto=tot.get('importeIVASobreBruto'),
411
+ iva_gastos=tot.get('importeIVASobreGastos'),
412
+ total_neto=tot.get('importeTotalNeto'),
413
+ total_tributos=tot.get('importeTotalTributos'),
414
+ total_gastos=tot.get('importeTotalGastos'),
415
+ gasto=[],
416
+ guia=[],
417
+ tributo=[],
418
+ pdf=liq.get('pdf'),
419
+ )
420
+ for ret in liq.get('gasto', []):
421
+ self.params_out['gasto'].append(dict(
422
+ cod_gasto=ret['codGasto'],
423
+ importe=ret['importe'],
424
+ ))
425
+ for trib in liq.get('tributo', []):
426
+ self.params_out['tributo'].append(dict(
427
+ descripcion=trib.get('descripcion', ""),
428
+ base_imponible=trib.get('baseImponible'),
429
+ alicuota=trib.get('alicuota'),
430
+ codigo=trib['codTributo'],
431
+ importe=trib['importe'],
432
+ ))
433
+ # analizar datos del ajuste generado:
434
+ ajuste = liq.get('ajuste', {})
435
+ if ajuste:
436
+ self.params_out.update(dict(
437
+ tipo_ajuste=ajuste['tipoAjuste'],
438
+ modo_ajuste=ajuste['modoAjuste'],
439
+ ))
440
+ ajustado = ajuste.get('comprobanteAjustado')
441
+ if ajustado:
442
+ self.params_out.update(dict(
443
+ cbte_ajuste=dict(
444
+ tipo_cbte=ajustado['tipoComprobante'],
445
+ pto_vta=ajustado['puntoVenta'],
446
+ nro_cbte=ajustado['nroComprobante'],
447
+ )
448
+ ))
449
+ if DEBUG:
450
+ import pprint
451
+ pprint.pprint(self.params_out)
452
+ self.params_out['errores'] = self.errores
453
+
454
+ @inicializar_y_capturar_excepciones
455
+ def ConsultarLiquidacion(self, tipo_cbte=None, pto_vta=None, nro_cbte=None,
456
+ cae=None, cuit_comprador=None, pdf="liq.pdf"):
457
+ "Consulta una liquidaciΓ³n por No de Comprobante o CAE"
458
+ if cae:
459
+ ret = self.client.consultarLiquidacionPorCae(
460
+ auth={
461
+ 'token': self.Token, 'sign': self.Sign,
462
+ 'cuit': self.Cuit, },
463
+ solicitud={
464
+ 'cae': cae,
465
+ 'pdf': pdf and True or False,
466
+ },
467
+ )
468
+ else:
469
+ ret = self.client.consultarLiquidacionPorNroComprobante(
470
+ auth={
471
+ 'token': self.Token, 'sign': self.Sign,
472
+ 'cuit': self.Cuit, },
473
+ solicitud={
474
+ 'cuitComprador': cuit_comprador,
475
+ 'puntoVenta': pto_vta,
476
+ 'nroComprobante': nro_cbte,
477
+ 'tipoComprobante': tipo_cbte,
478
+ 'pdf': pdf and True or False,
479
+ },
480
+ )
481
+ ret = ret['respuesta']
482
+ self.__analizar_errores(ret)
483
+ self.AnalizarLiquidacion(ret)
484
+ # guardo el PDF si se indico archivo y vino en la respuesta:
485
+ if pdf and 'pdf' in ret:
486
+ open(pdf, "wb").write(ret['pdf'])
487
+ return True
488
+
489
+ @inicializar_y_capturar_excepciones
490
+ def ConsultarUltimoComprobante(self, tipo_cbte=151, pto_vta=1):
491
+ "Consulta el ΓΊltimo No de Comprobante registrado"
492
+ ret = self.client.consultarUltimoNroComprobantePorPtoVta(
493
+ auth={
494
+ 'token': self.Token, 'sign': self.Sign,
495
+ 'cuit': self.Cuit, },
496
+ solicitud={
497
+ 'puntoVenta': pto_vta,
498
+ 'tipoComprobante': tipo_cbte},
499
+ )
500
+ ret = ret['respuesta']
501
+ self.__analizar_errores(ret)
502
+ self.NroComprobante = ret['nroComprobante']
503
+ return True
504
+
505
+ @inicializar_y_capturar_excepciones
506
+ def CrearAjuste(self, tipo_ajuste, fecha_cbte, datos_adicionales=None, **kwargs):
507
+ "Inicializa internamente los datos de una liquidaciΓ³n para ajustar"
508
+ # creo el diccionario con los campos generales de la liquidaciΓ³n:
509
+ self.solicitud = dict(tipoAjuste=tipo_ajuste,
510
+ fechaComprobante=fecha_cbte,
511
+ emisor={},
512
+ itemDetalleAjusteLiquidacion=[],
513
+ ajusteFinanciero={},
514
+ datosAdicionales=datos_adicionales,
515
+ )
516
+ return True
517
+
518
+ @inicializar_y_capturar_excepciones
519
+ def AgregarComprobanteAAjustar(self, tipo_cbte, pto_vta, nro_cbte):
520
+ "Agrega comprobante a ajustar"
521
+ cbte = dict(tipoComprobante=tipo_cbte, puntoVenta=pto_vta, nroComprobante=nro_cbte)
522
+ self.solicitud['emisor']["comprobanteAAjustar"] = cbte
523
+ return True
524
+
525
+ @inicializar_y_capturar_excepciones
526
+ def AgregarItemDetalleAjuste(self, nro_item_ajustar, **kwargs):
527
+ "Agrega el detalle de item a un ajuste de liquidaciΓ³n"
528
+ d = {'nroItemAjustar': nro_item_ajustar, 'ajusteCompraAsociada': []}
529
+ self.solicitud['itemDetalleAjusteLiquidacion'].append(d)
530
+ return True
531
+
532
+ @inicializar_y_capturar_excepciones
533
+ def AgregarAjusteFisico(self, cantidad, cantidad_cabezas=None,
534
+ cantidad_kg_vivo=None, **kwargs):
535
+ "Agrega campos al detalle de item por un ajuste fisico"
536
+ d = {'cantidad': cantidad,
537
+ 'cantidadCabezas': cantidad_cabezas,
538
+ 'cantidadKgVivo': cantidad_kg_vivo,
539
+ }
540
+ item_liq = self.solicitud['itemDetalleAjusteLiquidacion'][-1]
541
+ item_liq['ajusteFisico'] = d
542
+ return True
543
+
544
+ @inicializar_y_capturar_excepciones
545
+ def AgregarAjusteMonetario(self, precio_unitario, precio_recupero=None,
546
+ **kwargs):
547
+ "Agrega campos al detalle de item por un ajuste monetario"
548
+ d = {'precioUnitario': precio_unitario,
549
+ 'precioRecupero': precio_recupero,
550
+ }
551
+ item_liq = self.solicitud['itemDetalleAjusteLiquidacion'][-1]
552
+ item_liq['ajusteMonetario'] = d
553
+ return True
554
+
555
+ @inicializar_y_capturar_excepciones
556
+ def AgregarAjusteFinanciero(self, **kwargs):
557
+ "Prepara el detalle de item por un ajuste financiero (gastos/tributos)"
558
+ self.solicitud['ajusteFinanciero'] = {'gasto': [], 'tributo': []}
559
+ return True
560
+
561
+ @inicializar_y_capturar_excepciones
562
+ def AjustarLiquidacion(self):
563
+ "Generar y ajustar una liquidaciΓ³n para obtener del CAE"
564
+ # limpio los elementos que no correspondan por estar vacios:
565
+ for item_liq in self.solicitud['itemDetalleAjusteLiquidacion']:
566
+ campo = 'ajusteCompraAsociada'
567
+ if campo in item_liq and not item_liq[campo]:
568
+ del item_liq[campo]
569
+ for campo in self.solicitud.get('ajusteFinanciero', {}).copy():
570
+ if not self.solicitud['ajusteFinanciero'][campo]:
571
+ del self.solicitud['ajusteFinanciero'][campo]
572
+ # llamo al webservice:
573
+ ret = self.client.generarAjuste(
574
+ auth={
575
+ 'token': self.Token, 'sign': self.Sign,
576
+ 'cuit': self.Cuit, },
577
+ solicitud=self.solicitud,
578
+ )
579
+ # analizo la respusta
580
+ ret = ret['respuesta']
581
+ self.__analizar_errores(ret)
582
+ self.AnalizarLiquidacion(ret)
583
+ return True
584
+
585
+ @inicializar_y_capturar_excepciones
586
+ def ConsultarProvincias(self, sep="||"):
587
+ "Consulta las provincias habilitadas"
588
+ ret = self.client.consultarProvincias(
589
+ auth={
590
+ 'token': self.Token, 'sign': self.Sign,
591
+ 'cuit': self.Cuit, },
592
+ )['respuesta']
593
+ self.__analizar_errores(ret)
594
+ array = ret.get('provincia', [])
595
+ if sep is None:
596
+ return dict([(it['codigo'], it['descripcion']) for it in array])
597
+ else:
598
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
599
+ (it['codigo'], it['descripcion']) for it in array]
600
+
601
+ @inicializar_y_capturar_excepciones
602
+ def ConsultarLocalidades(self, cod_provincia, sep="||"):
603
+ "Consulta las localidades habilitadas"
604
+ ret = self.client.consultarLocalidadesPorProvincia(
605
+ auth={
606
+ 'token': self.Token, 'sign': self.Sign,
607
+ 'cuit': self.Cuit, },
608
+ solicitud={'codProvincia': cod_provincia},
609
+ )['respuesta']
610
+ self.__analizar_errores(ret)
611
+ array = ret.get('localidad', [])
612
+ if sep is None:
613
+ return dict([(it['codigo'], it['descripcion']) for it in array])
614
+ else:
615
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
616
+ (it['codigo'], it['descripcion']) for it in array]
617
+
618
+ @inicializar_y_capturar_excepciones
619
+ def ConsultarOperaciones(self, sep="||"):
620
+ "Retorna un listado de cΓ³digo y descripciΓ³n de operaciones permitidas"
621
+ ret = self.client.consultarOperaciones(
622
+ auth={
623
+ 'token': self.Token, 'sign': self.Sign,
624
+ 'cuit': self.Cuit, },
625
+ )['respuesta']
626
+ self.__analizar_errores(ret)
627
+ array = ret.get('operacion', []) + ret.get('operacionPorcina', [])
628
+ if sep is None:
629
+ return dict([(it['codigo'], it['descripcion']) for it in array])
630
+ else:
631
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
632
+ (it['codigo'], it['descripcion']) for it in array]
633
+
634
+ @inicializar_y_capturar_excepciones
635
+ def ConsultarTributos(self, sep="||"):
636
+ "Retorna un listado de tributos con cΓ³digo, descripciΓ³n y signo."
637
+ ret = self.client.consultarTributos(
638
+ auth={
639
+ 'token': self.Token, 'sign': self.Sign,
640
+ 'cuit': self.Cuit, },
641
+ )['respuesta']
642
+ self.__analizar_errores(ret)
643
+ array = ret.get('tributo', []) + ret.get('tributoPorcino', [])
644
+ if sep is None:
645
+ return dict([(it['codigo'], it['descripcion']) for it in array])
646
+ else:
647
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
648
+ (it['codigo'], it['descripcion']) for it in array]
649
+
650
+ @inicializar_y_capturar_excepciones
651
+ def ConsultarGastos(self, sep="||"):
652
+ "Retorna un listado de gastos con cΓ³digo y descripciΓ³n"
653
+ ret = self.client.consultarGastos(
654
+ auth={
655
+ 'token': self.Token, 'sign': self.Sign,
656
+ 'cuit': self.Cuit, },
657
+ )['respuesta']
658
+ self.__analizar_errores(ret)
659
+ array = ret.get('gasto', [])
660
+ if sep is None:
661
+ return dict([(it['codigo'], it['descripcion']) for it in array])
662
+ else:
663
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
664
+ (it['codigo'], it['descripcion']) for it in array]
665
+
666
+ @inicializar_y_capturar_excepciones
667
+ def ConsultarTiposComprobante(self, sep="||"):
668
+ "Retorna un listado de tipos de comprobantes con cΓ³digo y descripciΓ³n"
669
+ ret = self.client.consultarTiposComprobante(
670
+ auth={
671
+ 'token': self.Token, 'sign': self.Sign,
672
+ 'cuit': self.Cuit, },
673
+ )['respuesta']
674
+ self.__analizar_errores(ret)
675
+ array = ret.get('tipoComprobante', [])
676
+ if sep is None:
677
+ return dict([(it['codigo'], it['descripcion']) for it in array])
678
+ else:
679
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
680
+ (it['codigo'], it['descripcion']) for it in array]
681
+
682
+ @inicializar_y_capturar_excepciones
683
+ def ConsultarTiposLiquidacion(self, sep="||"):
684
+ "Retorna un listado de tipos de liquidaciΓ³n con cΓ³digo y descripciΓ³n"
685
+ ret = self.client.consultarTiposLiquidacion(
686
+ auth={
687
+ 'token': self.Token, 'sign': self.Sign,
688
+ 'cuit': self.Cuit, },
689
+ )['respuesta']
690
+ self.__analizar_errores(ret)
691
+ array = ret.get('tipoLiquidacion', [])
692
+ if sep is None:
693
+ return dict([(it['codigo'], it['descripcion']) for it in array])
694
+ else:
695
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
696
+ (it['codigo'], it['descripcion']) for it in array]
697
+
698
+ @inicializar_y_capturar_excepciones
699
+ def ConsultarCaracteres(self, sep="||"):
700
+ "Retorna listado de caracteres emisor/receptor (cΓ³digo, descripciΓ³n)"
701
+ ret = self.client.consultarCaracteresParticipante(
702
+ auth={
703
+ 'token': self.Token, 'sign': self.Sign,
704
+ 'cuit': self.Cuit, },
705
+ )['respuesta']
706
+ self.__analizar_errores(ret)
707
+ array = ret.get('caracter', []) + ret.get('caracterPorcino', [])
708
+ if sep is None:
709
+ return dict([(it['codigo'], it['descripcion']) for it in array])
710
+ else:
711
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
712
+ (it['codigo'], it['descripcion']) for it in array]
713
+
714
+ @inicializar_y_capturar_excepciones
715
+ def ConsultarCategorias(self, sep="||"):
716
+ "Retorna listado de categorΓ­as existentes (cΓ³digo, descripciΓ³n)"
717
+ ret = self.client.consultarCategorias(
718
+ auth={
719
+ 'token': self.Token, 'sign': self.Sign,
720
+ 'cuit': self.Cuit, },
721
+ )['respuesta']
722
+ self.__analizar_errores(ret)
723
+ array = ret.get('categoria', []) + ret.get('categoriaPorcina', [])
724
+ if sep is None:
725
+ return dict([(it['codigo'], it['descripcion']) for it in array])
726
+ else:
727
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
728
+ (it['codigo'], it['descripcion']) for it in array]
729
+
730
+ @inicializar_y_capturar_excepciones
731
+ def ConsultarMotivos(self, sep="||"):
732
+ "Retorna listado de motivos existentes (cΓ³digo, descripciΓ³n)"
733
+ ret = self.client.consultarMotivos(
734
+ auth={
735
+ 'token': self.Token, 'sign': self.Sign,
736
+ 'cuit': self.Cuit, },
737
+ )['respuesta']
738
+ self.__analizar_errores(ret)
739
+ array = ret.get('motivo', [])
740
+ if sep is None:
741
+ return dict([(it['codigo'], it['descripcion']) for it in array])
742
+ else:
743
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
744
+ (it['codigo'], it['descripcion']) for it in array]
745
+
746
+ @inicializar_y_capturar_excepciones
747
+ def ConsultarRazas(self, sep="||"):
748
+ "Retorna listado de razas -vacunas y porcinos- (cΓ³digo, descripciΓ³n)"
749
+ ret = self.client.consultarRazas(
750
+ auth={
751
+ 'token': self.Token, 'sign': self.Sign,
752
+ 'cuit': self.Cuit, },
753
+ )['respuesta']
754
+ self.__analizar_errores(ret)
755
+ array = ret.get('raza', []) + ret.get('razaPorcina', [])
756
+ if sep is None:
757
+ return dict([(it['codigo'], it['descripcion']) for it in array])
758
+ else:
759
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
760
+ (it['codigo'], it['descripcion']) for it in array]
761
+
762
+ @inicializar_y_capturar_excepciones
763
+ def ConsultarCortes(self, sep="||"):
764
+ "Retorna listado de cortes -carnes- (cΓ³digo, descripciΓ³n)"
765
+ ret = self.client.consultarCortes(
766
+ auth={
767
+ 'token': self.Token, 'sign': self.Sign,
768
+ 'cuit': self.Cuit, },
769
+ )['respuesta']
770
+ self.__analizar_errores(ret)
771
+ array = ret.get('corte', []) + ret.get('cortePorcino', [])
772
+ if sep is None:
773
+ return dict([(it['codigo'], it['descripcion']) for it in array])
774
+ else:
775
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
776
+ (it['codigo'], it['descripcion']) for it in array]
777
+
778
+ @inicializar_y_capturar_excepciones
779
+ def ConsultarPuntosVentas(self, sep="||"):
780
+ "Retorna los puntos de ventas autorizados para la utilizacion de WS"
781
+ ret = self.client.consultarPuntosVenta(
782
+ auth={
783
+ 'token': self.Token, 'sign': self.Sign,
784
+ 'cuit': self.Cuit, },
785
+ )['respuesta']
786
+ self.__analizar_errores(ret)
787
+ array = ret.get('puntoVenta', [])
788
+ if sep is None:
789
+ return dict([(it['codigo'], it.get('descripcion', '')) for it in array])
790
+ else:
791
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
792
+ (it['codigo'], it.get('descripcion', '')) for it in array]
793
+
794
+ def MostrarPDF(self, archivo, imprimir=False):
795
+ try:
796
+ if sys.platform == "linux2":
797
+ import subprocess
798
+ subprocess.run(["evince", archivo], check=False)
799
+ else:
800
+ operation = imprimir and "print" or ""
801
+ os.startfile(archivo, operation)
802
+ return True
803
+ except Exception as e:
804
+ self.Excepcion = str(e)
805
+ return False
806
+
807
+
808
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
809
+ INSTALL_DIR = WSLSP.InstallDir = get_install_dir()
810
+
811
+
812
+ if __name__ == '__main__':
813
+ if '--ayuda' in sys.argv:
814
+ print(LICENCIA)
815
+ print(AYUDA)
816
+ sys.exit(0)
817
+ if '--formato' in sys.argv:
818
+ print("Formato:")
819
+ for msg, formato in []:
820
+ comienzo = 1
821
+ print("=== %s ===" % msg)
822
+ for fmt in formato:
823
+ clave, longitud, tipo = fmt[0:3]
824
+ dec = len(fmt) > 3 and fmt[3] or (tipo == 'I' and '2' or '')
825
+ print(" * Campo: %-20s PosiciΓ³n: %3d Longitud: %4d Tipo: %s Decimales: %s" % (
826
+ clave, comienzo, longitud, tipo, dec))
827
+ comienzo += longitud
828
+ sys.exit(0)
829
+
830
+ if "--register" in sys.argv or "--unregister" in sys.argv:
831
+ import win32com.server.register
832
+ win32com.server.register.UseCommandLine(WSLSP)
833
+ sys.exit(0)
834
+
835
+ import csv
836
+ from configparser import SafeConfigParser
837
+
838
+ from .wsaa import WSAA
839
+
840
+ try:
841
+
842
+ if "--version" in sys.argv:
843
+ print("VersiΓ³n: ", __version__)
844
+
845
+ if len(sys.argv) > 1 and sys.argv[1].endswith(".ini"):
846
+ CONFIG_FILE = sys.argv[1]
847
+ print("Usando configuracion:", CONFIG_FILE)
848
+
849
+ config = SafeConfigParser()
850
+ config.read(CONFIG_FILE)
851
+ CERT = config.get('WSAA', 'CERT')
852
+ PRIVATEKEY = config.get('WSAA', 'PRIVATEKEY')
853
+ CUIT = config.get('WSLSP', 'CUIT')
854
+ ENTRADA = config.get('WSLSP', 'ENTRADA')
855
+ SALIDA = config.get('WSLSP', 'SALIDA')
856
+ PDF = config.has_option('WSLSP', 'PDF') and config.get('WSLSP', 'PDF') or "liq.pdf"
857
+
858
+ if config.has_option('WSAA', 'URL') and not HOMO:
859
+ WSAA_URL = config.get('WSAA', 'URL')
860
+ else:
861
+ WSAA_URL = None # wsaa.WSAAURL
862
+ if config.has_option('WSLSP', 'URL') and not HOMO:
863
+ WSLSP_URL = config.get('WSLSP', 'URL')
864
+ else:
865
+ WSLSP_URL = WSDL
866
+
867
+ PROXY = config.has_option('WSAA', 'PROXY') and config.get('WSAA', 'PROXY') or None
868
+ CACERT = config.has_option('WSAA', 'CACERT') and config.get('WSAA', 'CACERT') or None
869
+ WRAPPER = config.has_option('WSAA', 'WRAPPER') and config.get('WSAA', 'WRAPPER') or None
870
+
871
+ if config.has_section('DBF'):
872
+ conf_dbf = dict(config.items('DBF'))
873
+ if DEBUG:
874
+ print("conf_dbf", conf_dbf)
875
+ else:
876
+ conf_dbf = {}
877
+
878
+ DEBUG = '--debug' in sys.argv
879
+ XML = '--xml' in sys.argv
880
+
881
+ if DEBUG:
882
+ print("Usando ConfiguraciΓ³n:")
883
+ print("WSAA_URL:", WSAA_URL)
884
+ print("WSLSP_URL:", WSLSP_URL)
885
+ print("CACERT", CACERT)
886
+ print("WRAPPER", WRAPPER)
887
+ # obteniendo el TA
888
+ from .wsaa import WSAA
889
+ wsaa = WSAA()
890
+ ta = wsaa.Autenticar("wslsp", CERT, PRIVATEKEY, wsdl=WSAA_URL,
891
+ proxy=PROXY, wrapper=WRAPPER, cacert=CACERT)
892
+ if not ta:
893
+ pass # sys.exit("Imposible autenticar con WSAA: %s" % wsaa.Excepcion)
894
+
895
+ # cliente soap del web service
896
+ wslsp = WSLSP()
897
+ wslsp.LanzarExcepciones = True
898
+ wslsp.Conectar(url=WSLSP_URL, proxy=PROXY, wrapper=WRAPPER, cacert=CACERT)
899
+ wslsp.SetTicketAcceso(ta)
900
+ wslsp.Cuit = CUIT
901
+
902
+ if '--dummy' in sys.argv:
903
+ ret = wslsp.Dummy()
904
+ print("AppServerStatus", wslsp.AppServerStatus)
905
+ print("DbServerStatus", wslsp.DbServerStatus)
906
+ print("AuthServerStatus", wslsp.AuthServerStatus)
907
+ # sys.exit(0)
908
+
909
+ if '--autorizar' in sys.argv:
910
+
911
+ if '--prueba' in sys.argv:
912
+ print(wslsp.client.help("generarLiquidacion"))
913
+
914
+ # Solicitud 1: Cuenta de Venta y LΓ­quido Producto - Hacienda
915
+ wslsp.CrearLiquidacion(
916
+ cod_operacion=1,
917
+ fecha_cbte='2017-04-23',
918
+ fecha_op='2017-04-23',
919
+ cod_motivo=6,
920
+ cod_localidad_procedencia=8274,
921
+ cod_provincia_procedencia=1,
922
+ cod_localidad_destino=8274,
923
+ cod_provincia_destino=1,
924
+ lugar_realizacion='CORONEL SUAREZ',
925
+ fecha_recepcion=None, fecha_faena=None,
926
+ datos_adicionales=None)
927
+ if False:
928
+ wslsp.AgregarFrigorifico(cuit=20160000156, nro_planta=1)
929
+ wslsp.AgregarEmisor(
930
+ tipo_cbte=180, pto_vta=3000, nro_cbte=64,
931
+ cod_caracter=5, fecha_inicio_act='2016-01-01',
932
+ iibb='123456789', nro_ruca=305, nro_renspa=None)
933
+ wslsp.AgregarReceptor(cod_caracter=3)
934
+ wslsp.AgregarOperador(cuit=30160000011, iibb=3456,
935
+ # nro_ruca=1011, # Validacion AFIP 1003
936
+ # cuit_autorizado=20160000261, # 1001
937
+ nro_renspa='22.123.1.12345/A4')
938
+ wslsp.AgregarItemDetalle(
939
+ cuit_cliente="20160000199", # 2403
940
+ cod_categoria=51020102,
941
+ tipo_liquidacion=1,
942
+ cantidad=2,
943
+ precio_unitario=10.0,
944
+ alicuota_iva=10.5,
945
+ cod_raza=1,
946
+ cantidad_cabezas=None, # Validacion AFIP 2408
947
+ nro_tropa=None,
948
+ cod_corte=None,
949
+ cantidad_kg_vivo=None,
950
+ precio_recupero=None,
951
+ detalle_raza=None,
952
+ nro_item=1,
953
+ )
954
+ wslsp.AgregarCompraAsociada(tipo_cbte=185, pto_vta=3000,
955
+ nro_cbte=33, cant_asoc=2,
956
+ nro_item=1)
957
+ wslsp.AgregarGuia(nro_guia=1)
958
+ # wslsp.AgregarGuia(nro_guia=2)
959
+ if True:
960
+ wslsp.AgregarDTE(nro_dte="418-1",
961
+ nro_renspa='22.123.1.12345/A5')
962
+ wslsp.AgregarDTE(nro_dte="418-2",
963
+ nro_renspa='22.123.1.12346/A5')
964
+ else:
965
+ wslsp.AgregarDTE(nro_dte="418-3", nro_renspa=None)
966
+ wslsp.AgregarDTE(nro_dte="418-4", nro_renspa=None)
967
+ wslsp.AgregarGasto(cod_gasto=16, base_imponible=230520.60,
968
+ alicuota=3, alicuota_iva=10.5)
969
+ wslsp.AgregarGasto(cod_gasto=99, base_imponible=None,
970
+ alicuota=1, alicuota_iva=0,
971
+ descripcion="Exento WSLSPv1.4.1",
972
+ tipo_iva_nulo="EX")
973
+ wslsp.AgregarTributo(cod_tributo=5, base_imponible=230520.60,
974
+ alicuota=2.5)
975
+ wslsp.AgregarTributo(cod_tributo=3, importe=397)
976
+ else:
977
+ # cargar un archivo de texto:
978
+ with open(ENTRADA, "r") as f:
979
+ wslsp.solicitud = json.load(f, encoding="utf-8")
980
+
981
+ if '--testing' in sys.argv:
982
+ # mensaje de prueba (no realiza llamada remota),
983
+ # usar solo si no estΓ‘ operativo, cargo respuesta:
984
+ wslsp.LoadTestXML("tests/xml/wslsp_liq_ok_response.xml")
985
+ import json
986
+ with open(ENTRADA, "w") as f:
987
+ json.dump(wslsp.solicitud, f, sort_keys=True, indent=4, encoding="utf-8",)
988
+
989
+ print("Liquidacion: pto_vta=%s nro_cbte=%s tipo_cbte=%s" % (
990
+ wslsp.solicitud['emisor']['puntoVenta'],
991
+ wslsp.solicitud['emisor']['nroComprobante'],
992
+ wslsp.solicitud['emisor']['tipoComprobante'],
993
+ ))
994
+
995
+ if not '--dummy' in sys.argv:
996
+ print("Autorizando...")
997
+ ret = wslsp.AutorizarLiquidacion()
998
+
999
+ if wslsp.Excepcion:
1000
+ print("EXCEPCION:", wslsp.Excepcion, file=sys.stderr)
1001
+ if DEBUG:
1002
+ print(wslsp.Traceback, file=sys.stderr)
1003
+ print("Errores:", wslsp.Errores)
1004
+ print("CAE", wslsp.CAE)
1005
+ print("NroCodigoBarras", wslsp.NroCodigoBarras)
1006
+ print("FechaProcesoAFIP", wslsp.FechaProcesoAFIP)
1007
+ print("FechaComprobante", wslsp.FechaComprobante)
1008
+ print("NroComprobante", wslsp.NroComprobante)
1009
+ print("ImporteBruto", wslsp.ImporteBruto)
1010
+ print("ImporteTotalNeto", wslsp.ImporteTotalNeto)
1011
+ print("ImporteIVA Sobre Bruto", wslsp.ImporteIVASobreBruto)
1012
+ print("ImporteIVA Sobre Gastos", wslsp.ImporteIVASobreGastos)
1013
+ print("ImporteTotalNeto", wslsp.ImporteTotalNeto)
1014
+
1015
+ pdf = wslsp.GetParametro("pdf")
1016
+ if pdf:
1017
+ open(PDF, "wb").write(pdf)
1018
+
1019
+ if '--testing' in sys.argv:
1020
+ assert wslsp.CAE == "97083467167835"
1021
+
1022
+ if DEBUG:
1023
+ pprint.pprint(wslsp.params_out)
1024
+
1025
+ if '--ajustar' in sys.argv:
1026
+ if '--prueba' in sys.argv:
1027
+ # ejemplo documentaciΓ³n AFIP:
1028
+ if '--testing' in sys.argv:
1029
+ wslsp.LoadTestXML("tests/xml/wslsp_ajuste_test.xml")
1030
+ wslsp.CrearAjuste(tipo_ajuste='C', fecha_cbte='2017-01-06',
1031
+ datos_adicionales='Ajuste sobre liquidacion de compra directa'
1032
+ )
1033
+ wslsp.AgregarEmisor(tipo_cbte=186, pto_vta=3000, nro_cbte=1)
1034
+ wslsp.AgregarComprobanteAAjustar(tipo_cbte=186, pto_vta=2000, nro_cbte=4)
1035
+ wslsp.AgregarItemDetalleAjuste(nro_item_ajustar=1)
1036
+ wslsp.AgregarCompraAsociada(tipo_cbte=185, pto_vta=3000,
1037
+ nro_cbte=33, cant_asoc=2,
1038
+ nro_item=1)
1039
+ # ValidaciΓ³n de AFIP 3002:
1040
+ # No se pueden realizar ajustes fisicos y monetario en un mismo comprobante.
1041
+ wslsp.AgregarAjusteFisico(
1042
+ cantidad=1,
1043
+ cantidad_cabezas=None,
1044
+ cantidad_kg_vivo=None,
1045
+ )
1046
+ wslsp.AgregarAjusteMonetario(
1047
+ precio_unitario=15.995,
1048
+ precio_recupero=None,
1049
+ )
1050
+ wslsp.AgregarAjusteFinanciero()
1051
+ wslsp.AgregarGasto(cod_gasto=16, base_imponible=230520.60,
1052
+ alicuota=3, alicuota_iva=10.5)
1053
+ wslsp.AgregarTributo(cod_tributo=5, base_imponible=230520.60,
1054
+ alicuota=2.5)
1055
+ wslsp.AgregarTributo(cod_tributo=3, importe=397)
1056
+ import json
1057
+ with open(ENTRADA, "w") as f:
1058
+ json.dump(wslsp.solicitud, f, sort_keys=True, indent=4, encoding="utf-8",)
1059
+ else:
1060
+ # cargar un archivo de texto:
1061
+ with open(ENTRADA, "r") as f:
1062
+ wslsp.solicitud = json.load(f, encoding="utf-8")
1063
+
1064
+ wslsp.AjustarLiquidacion()
1065
+ print("CAE:", wslsp.CAE)
1066
+ print("Tipo Ajuste:", wslsp.GetParametro("tipo_ajuste"))
1067
+ print("Modo Ajuste:", wslsp.GetParametro("modo_ajuste"))
1068
+ print("Errores:", wslsp.Errores)
1069
+ if '--testing' in sys.argv:
1070
+ assert wslsp.GetParametro("cae") == "97029023118043"
1071
+ assert wslsp.GetParametro("cbte_ajuste", "tipo_cbte") == '186'
1072
+ assert wslsp.GetParametro("cbte_ajuste", "pto_vta") == '2000'
1073
+ assert wslsp.GetParametro("cbte_ajuste", "nro_cbte") == '3'
1074
+
1075
+ pdf = wslsp.GetParametro("pdf")
1076
+ if pdf:
1077
+ open(PDF, "wb").write(pdf)
1078
+
1079
+ if '--consultar' in sys.argv:
1080
+ tipo_cbte = 180
1081
+ pto_vta = 3000
1082
+ nro_cbte = 1
1083
+ cuit = None
1084
+ try:
1085
+ tipo_cbte = sys.argv[sys.argv.index("--consultar") + 1]
1086
+ pto_vta = sys.argv[sys.argv.index("--consultar") + 2]
1087
+ nro_cbte = sys.argv[sys.argv.index("--consultar") + 3]
1088
+ cuit = sys.argv[sys.argv.index("--consultar") + 4]
1089
+ except IndexError:
1090
+ pass
1091
+ if '--testing' in sys.argv:
1092
+ # mensaje de prueba (no realiza llamada remota),
1093
+ # usar solo si no estΓ‘ operativo, cargo prueba:
1094
+ wslsp.LoadTestXML("tests/xml/wslsp_cons_test.xml")
1095
+ print("Consultando: tipo_cbte=%s pto_vta=%s nro_cbte=%s" % (tipo_cbte, pto_vta, nro_cbte))
1096
+ ret = wslsp.ConsultarLiquidacion(tipo_cbte, pto_vta, nro_cbte,
1097
+ cuit_comprador=cuit)
1098
+ print("CAE", wslsp.CAE)
1099
+ print("Errores:", wslsp.Errores)
1100
+
1101
+ if DEBUG:
1102
+ pprint.pprint(wslsp.params_out)
1103
+
1104
+ if '--mostrar' in sys.argv and pdf:
1105
+ wslsp.MostrarPDF(archivo=pdf,
1106
+ imprimir='--imprimir' in sys.argv)
1107
+
1108
+ if '--ult' in sys.argv:
1109
+ tipo_cbte = 27
1110
+ pto_vta = 1
1111
+ try:
1112
+ tipo_cbte = sys.argv[sys.argv.index("--ult") + 1]
1113
+ pto_vta = sys.argv[sys.argv.index("--ult") + 2]
1114
+ except IndexError:
1115
+ pass
1116
+
1117
+ print("Consultando ultimo nro_cbte para pto_vta=%s" % pto_vta, end=' ')
1118
+ ret = wslsp.ConsultarUltimoComprobante(tipo_cbte, pto_vta)
1119
+ if wslsp.Excepcion:
1120
+ print("EXCEPCION:", wslsp.Excepcion, file=sys.stderr)
1121
+ if DEBUG:
1122
+ print(wslsp.Traceback, file=sys.stderr)
1123
+ print("Ultimo Nro de Comprobante", wslsp.NroComprobante)
1124
+ print("Errores:", wslsp.Errores)
1125
+ sys.exit(0)
1126
+
1127
+ if "--guardar" in sys.argv:
1128
+ # grabar un archivo de texto (intercambio) con el resultado:
1129
+ liq = wslsp.params_out.copy()
1130
+ if "pdf" in liq:
1131
+ del liq["pdf"] # eliminador binario
1132
+ with open(SALIDA, "w") as f:
1133
+ json.dump(liq, f, default=str,
1134
+ indent=2, sort_keys=True, encoding="utf-8")
1135
+
1136
+ # Recuperar parΓ‘metros:
1137
+
1138
+ if '--provincias' in sys.argv:
1139
+ ret = wslsp.ConsultarProvincias()
1140
+ print("\n".join(ret))
1141
+
1142
+ if '--localidades' in sys.argv:
1143
+ try:
1144
+ cod_provincia = sys.argv[sys.argv.index("--localidades") + 1]
1145
+ except BaseException:
1146
+ cod_provincia = input("Codigo Provincia:")
1147
+ ret = wslsp.ConsultarLocalidades(cod_provincia)
1148
+ print("\n".join(ret))
1149
+
1150
+ if '--operaciones' in sys.argv:
1151
+ ret = wslsp.ConsultarOperaciones()
1152
+ print("\n".join(ret))
1153
+
1154
+ if '--tributos' in sys.argv:
1155
+ ret = wslsp.ConsultarTributos()
1156
+ print("\n".join(ret))
1157
+
1158
+ if '--gastos' in sys.argv:
1159
+ ret = wslsp.ConsultarGastos()
1160
+ print("\n".join(ret))
1161
+
1162
+ if '--tipos_cbte' in sys.argv:
1163
+ ret = wslsp.ConsultarTiposComprobante()
1164
+ print("\n".join(ret))
1165
+
1166
+ if '--tipos_liq' in sys.argv:
1167
+ ret = wslsp.ConsultarTiposLiquidacion()
1168
+ print("\n".join(ret))
1169
+
1170
+ if '--caracteres' in sys.argv:
1171
+ ret = wslsp.ConsultarCaracteres()
1172
+ print("\n".join(ret))
1173
+
1174
+ if '--categorias' in sys.argv:
1175
+ ret = wslsp.ConsultarCategorias()
1176
+ print("\n".join(ret))
1177
+
1178
+ if '--motivos' in sys.argv:
1179
+ ret = wslsp.ConsultarMotivos()
1180
+ print("\n".join(ret))
1181
+
1182
+ if '--razas' in sys.argv:
1183
+ ret = wslsp.ConsultarRazas()
1184
+ print("\n".join(ret))
1185
+
1186
+ if '--cortes' in sys.argv:
1187
+ ret = wslsp.ConsultarCortes()
1188
+ print("\n".join(ret))
1189
+
1190
+ if '--puntosventa' in sys.argv:
1191
+ ret = wslsp.ConsultarPuntosVentas()
1192
+ print("\n".join(ret))
1193
+
1194
+ print("hecho.")
1195
+
1196
+ except SoapFault as e:
1197
+ print("Falla SOAP:", e.faultcode, e.faultstring.encode("ascii", "ignore"), file=sys.stderr)
1198
+ sys.exit(3)
1199
+ except Exception as e:
1200
+ try:
1201
+ print(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0], file=sys.stderr)
1202
+ except BaseException:
1203
+ print("ExcepciΓ³n no disponible:", type(e), file=sys.stderr)
1204
+ if DEBUG:
1205
+ raise
1206
+ sys.exit(5)
1207
+ finally:
1208
+ if XML:
1209
+ open("wslsp_request.xml", "w").write(wslsp.client.xml_request)
1210
+ open("wslsp_response.xml", "w").write(wslsp.client.xml_response)
app/pyafipws/wsltv.py ADDED
@@ -0,0 +1,1069 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+
14
+ import shelve
15
+ import sys
16
+ import datetime
17
+ import decimal
18
+ import os
19
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, BaseWS, inicializar_y_capturar_excepciones, get_install_dir
20
+ from . import utils
21
+ from fpdf import Template
22
+ from pysimplesoap.client import SoapFault
23
+ import pprint
24
+ import traceback
25
+ """MΓ³dulo para obtener cΓ³digo de autorizaciΓ³n electrΓ³nica (CAE) para
26
+ LiquidaciΓ³n de Tabaco Verde del web service WSLTV de AFIP
27
+ """
28
+
29
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
30
+ __copyright__ = "Copyright (C) 2016 Mariano Reingart"
31
+ __license__ = "GPL 3.0"
32
+ __version__ = "1.06d"
33
+
34
+ LICENCIA = """
35
+ wsltv.py: Interfaz para generar CΓ³digo de AutorizaciΓ³n ElectrΓ³nica (CAE) para
36
+ LiquidaciΓ³n de Tabaco Verde (LtvService)
37
+ Copyright (C) 2016 Mariano Reingart reingart@gmail.com
38
+ http://www.sistemasagiles.com.ar/trac/wiki/LiquidacionPrimariaGranos
39
+
40
+ Este progarma es software libre, se entrega ABSOLUTAMENTE SIN GARANTIA
41
+ y es bienvenido a redistribuirlo respetando la licencia GPLv3.
42
+
43
+ Para informaciΓ³n adicional sobre garantΓ­a, soporte tΓ©cnico comercial
44
+ e incorporaciΓ³n/distribuciΓ³n en programas propietarios ver PyAfipWs:
45
+ http://www.sistemasagiles.com.ar/trac/wiki/PyAfipWs
46
+ """
47
+
48
+ AYUDA = """
49
+ Opciones:
50
+ --ayuda: este mensaje
51
+
52
+ --debug: modo depuraciΓ³n (detalla y confirma las operaciones)
53
+ --formato: muestra el formato de los archivos de entrada/salida
54
+ --prueba: genera y autoriza una liquidaciΓ³n de prueba (no usar en producciΓ³n!)
55
+ --xml: almacena los requerimientos y respuestas XML (depuraciΓ³n)
56
+ --dbf: utilizar tablas DBF (xBase) para los archivos de intercambio
57
+ --json: utilizar formato json para el archivo de intercambio
58
+ --dummy: consulta estado de servidores
59
+
60
+ --autorizar: Autorizar LiquidaciΓ³n de Tabaco Verde (generarLiquidacion)
61
+ --ajustar: Ajustar (ajustarLiquidacion/generarAjusteFisico)
62
+ --ult: Consulta el ΓΊltimo nΓΊmero de orden registrado en AFIP
63
+ (consultarUltimoComprobanteXPuntoVenta)
64
+ --consultar: Consulta una liquidaciΓ³n registrada en AFIP
65
+ (consultarLiquidacionXNroComprobante / consultarLiquidacionXCAE)
66
+
67
+ --pdf: descarga la liquidaciΓ³n en formato PDF
68
+ --mostrar: muestra el documento PDF generado (usar con --pdf)
69
+ --imprimir: imprime el documento PDF generado (usar con --mostrar y --pdf)
70
+
71
+ --provincias: obtiene el listado de provincias (cΓ³digo/descripciΓ³n)
72
+ --condicionesventa: obtiene el listado de las condiciones de venta
73
+ --tributos: obtiene el listado de los tributos
74
+ --retenciones: obtiene el listado de las retenciones de tabaco
75
+ --variedades: obtiene el listado de las variedades y especies de tabaco
76
+ --depositos: obtiene el listado de los depositos de acopio (para el contrib.)
77
+ --puntosventa: obtiene el listado de puntos de venta habilitados
78
+
79
+ Ver wsltv.ini para parΓ‘metros de configuraciΓ³n (URL, certificados, etc.)"
80
+ """
81
+
82
+
83
+ # importo funciones compartidas:
84
+
85
+
86
+ WSDL = "https://fwshomo.afip.gov.ar/wsltv/LtvService?wsdl"
87
+ #WSDL = "https://serviciosjava.afip.gob.ar/wsltv/LtvService?wsdl"
88
+
89
+ DEBUG = False
90
+ XML = False
91
+ CONFIG_FILE = "wsltv.ini"
92
+ TIMEOUT = 30
93
+ HOMO = False
94
+
95
+
96
+ class WSLTV(BaseWS):
97
+ "Interfaz para el WebService de LiquidaciΓ³n de Tabaco Verde"
98
+ _public_methods_ = ['Conectar', 'Dummy', 'SetTicketAcceso', 'DebugLog',
99
+ 'AutorizarLiquidacion',
100
+ 'CrearLiquidacion',
101
+ 'AgregarCondicionVenta', 'AgregarReceptor',
102
+ 'AgregarRomaneo', 'AgregarFardo', 'AgregarPrecioClase',
103
+ 'AgregarRetencion', 'AgregarTributo',
104
+ 'AgregarFlete', 'AgregarBonificacion',
105
+ 'ConsultarLiquidacion', 'ConsultarUltimoComprobante',
106
+ 'CrearAjuste', 'AgregarComprobanteAAjustar',
107
+ 'AjustarLiquidacion', 'GenerarAjusteFisico',
108
+ 'LeerDatosLiquidacion',
109
+ 'ConsultarVariedadesClasesTabaco',
110
+ 'ConsultarTributos',
111
+ 'ConsultarRetencionesTabacaleras',
112
+ 'ConsultarDepositosAcopio',
113
+ 'ConsultarPuntosVentas',
114
+ 'ConsultarProvincias',
115
+ 'ConsultarCondicionesVenta',
116
+ 'MostrarPDF',
117
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
118
+ 'SetParametros', 'SetParametro', 'GetParametro',
119
+ ]
120
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
121
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
122
+ 'Excepcion', 'ErrCode', 'ErrMsg', 'LanzarExcepciones', 'Errores',
123
+ 'XmlRequest', 'XmlResponse', 'Version', 'Traceback', 'InstallDir',
124
+ 'CAE', 'NroComprobante', 'FechaLiquidacion',
125
+ 'ImporteNeto', 'TotalRetenciones', 'TotalTributos', 'Total',
126
+ 'AlicuotaIVA', 'ImporteIVA', 'Subtotal',
127
+ ]
128
+ _reg_progid_ = "WSLTV"
129
+ _reg_clsid_ = "{C6EEAE8A-7560-4538-B29C-76434A8C2DC3}"
130
+
131
+ # Variables globales para BaseWS:
132
+ HOMO = HOMO
133
+ WSDL = WSDL
134
+ LanzarExcepciones = False
135
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
136
+
137
+ def inicializar(self):
138
+ BaseWS.inicializar(self)
139
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
140
+ self.errores = []
141
+ self.CAE = ""
142
+ self.NroComprobante = self.FechaLiquidacion = ''
143
+ self.ImporteNeto = ""
144
+ self.ImporteIVA = ""
145
+ self.AlicuotaIVA = ""
146
+ self.TotalRetenciones = ""
147
+ self.TotalTributos = ""
148
+ self.Subtotal = self.Total = ""
149
+ self.datos = {}
150
+ self.reintentos = 0
151
+
152
+ @inicializar_y_capturar_excepciones
153
+ def Conectar(self, cache=None, url="", proxy="", wrapper="", cacert=None, timeout=30):
154
+ "Establecer la conexiΓ³n a los servidores de la AFIP"
155
+ # llamo al constructor heredado:
156
+ ok = BaseWS.Conectar(self, cache, url, proxy, wrapper, cacert, timeout)
157
+ if ok:
158
+ # corrijo ubicaciΓ³n del servidor (puerto htttp 80 en el WSDL)
159
+ location = self.client.services['LtvService']['ports']['LtvEndPoint']['location']
160
+ if location.startswith("http://"):
161
+ print("Corrigiendo WSDL ...", location, end=' ')
162
+ location = location.replace("http://", "https://").replace(":80", ":443")
163
+ self.client.services['LtvService']['ports']['LtvEndPoint']['location'] = location
164
+ print(location)
165
+ return ok
166
+
167
+ def __analizar_errores(self, ret):
168
+ "Comprueba y extrae errores si existen en la respuesta XML"
169
+ errores = []
170
+ if 'errores' in ret:
171
+ errores.extend(ret['errores'])
172
+ if errores:
173
+ self.Errores = ["%(codigo)s: %(descripcion)s" % err['error'][0]
174
+ for err in errores]
175
+ self.errores = [
176
+ {'codigo': err['error'][0]['codigo'],
177
+ 'descripcion': err['error'][0]['descripcion'].replace("\n", "")
178
+ .replace("\r", "")}
179
+ for err in errores]
180
+ self.ErrCode = ' '.join(self.Errores)
181
+ self.ErrMsg = '\n'.join(self.Errores)
182
+
183
+ @inicializar_y_capturar_excepciones
184
+ def Dummy(self):
185
+ "Obtener el estado de los servidores de la AFIP"
186
+ results = self.client.dummy()['respuesta']
187
+ self.AppServerStatus = str(results['appserver'])
188
+ self.DbServerStatus = str(results['dbserver'])
189
+ self.AuthServerStatus = str(results['authserver'])
190
+ return True
191
+
192
+ @inicializar_y_capturar_excepciones
193
+ def CrearLiquidacion(self, tipo_cbte, pto_vta, nro_cbte, fecha,
194
+ cod_deposito_acopio, tipo_compra,
195
+ variedad_tabaco, cod_provincia_origen_tabaco,
196
+ puerta=None, nro_tarjeta=None, horas=None, control=None,
197
+ nro_interno=None, iibb_emisor=None, fecha_inicio_actividad=None,
198
+ **kwargs):
199
+ "Inicializa internamente los datos de una liquidaciΓ³n para autorizar"
200
+ # creo el diccionario con los campos generales de la liquidaciΓ³n:
201
+ liq = dict(tipoComprobante=tipo_cbte,
202
+ nroComprobante=nro_cbte,
203
+ puntoVenta=pto_vta,
204
+ iibbEmisor=iibb_emisor,
205
+ codDepositoAcopio=cod_deposito_acopio,
206
+ fechaLiquidacion=fecha,
207
+ tipoCompra=tipo_compra,
208
+ condicionVenta=[],
209
+ variedadTabaco=variedad_tabaco,
210
+ codProvinciaOrigenTabaco=cod_provincia_origen_tabaco,
211
+ puerta=puerta,
212
+ nroTarjeta=nro_tarjeta,
213
+ horas=horas,
214
+ control=control,
215
+ nroInterno=nro_interno,
216
+ fechaInicioActividad=fecha_inicio_actividad,
217
+ )
218
+ self.solicitud = dict(liquidacion=liq,
219
+ receptor={},
220
+ romaneo=[],
221
+ precioClase=[],
222
+ retencion=[],
223
+ tributo=[],
224
+ )
225
+ return True
226
+
227
+ @inicializar_y_capturar_excepciones
228
+ def AgregarCondicionVenta(self, codigo, descripcion, **kwargs):
229
+ "Agrego una o mΓ‘s condicion de venta a la liq."
230
+ cond = {'codigo': codigo, 'descripcion': descripcion}
231
+ self.solicitud['liquidacion']['condicionVenta'].append(cond)
232
+ return True
233
+
234
+ @inicializar_y_capturar_excepciones
235
+ def AgregarReceptor(self, cuit, iibb, nro_socio, nro_fet, **kwargs):
236
+ "Agrego un receptor a la liq."
237
+ rcpt = dict(cuit=cuit, iibb=iibb, nroSocio=nro_socio, nroFET=nro_fet)
238
+ self.solicitud['receptor'] = rcpt
239
+ return True
240
+
241
+ @inicializar_y_capturar_excepciones
242
+ def AgregarRomaneo(self, nro_romaneo, fecha_romaneo, **kwargs):
243
+ "Agrego uno o mΓ‘s romaneos a la liq."
244
+ romaneo = dict(nroRomaneo=nro_romaneo, fechaRomaneo=fecha_romaneo,
245
+ fardo=[])
246
+ self.solicitud['romaneo'].append(romaneo)
247
+ return True
248
+
249
+ @inicializar_y_capturar_excepciones
250
+ def AgregarFardo(self, cod_trazabilidad, clase_tabaco, peso, **kwargs):
251
+ "Agrego un fardo al ΓΊltimo romaneo agregado a la liq."
252
+ fardo = dict(codTrazabilidad=cod_trazabilidad, claseTabaco=clase_tabaco, peso=peso)
253
+ self.solicitud['romaneo'][-1]['fardo'].append(fardo)
254
+ return True
255
+
256
+ @inicializar_y_capturar_excepciones
257
+ def AgregarPrecioClase(self, clase_tabaco, precio, total_kilos=None, total_fardos=None, **kwargs):
258
+ "Agrego un PrecioClase a la liq."
259
+ precioclase = dict(claseTabaco=clase_tabaco, precio=precio,
260
+ totalKilos=total_kilos, totalFardos=total_fardos)
261
+ self.solicitud['precioClase'].append(precioclase)
262
+ return True
263
+
264
+ @inicializar_y_capturar_excepciones
265
+ def AgregarRetencion(self, cod_retencion, descripcion, importe, **kwargs):
266
+ "Agrega la informaciΓ³n referente a las retenciones de la liquidaciΓ³n"
267
+ ret = dict(codRetencion=cod_retencion, descripcion=descripcion, importe=importe)
268
+ self.solicitud['retencion'].append(ret)
269
+ return True
270
+
271
+ @inicializar_y_capturar_excepciones
272
+ def AgregarTributo(self, codigo_tributo, descripcion, base_imponible, alicuota, importe):
273
+ "Agrega la informaciΓ³n referente a las retenciones de la liquidaciΓ³n"
274
+ trib = dict(codigoTributo=codigo_tributo, descripcion=descripcion, baseImponible=base_imponible, alicuota=alicuota, importe=importe)
275
+ self.solicitud['tributo'].append(trib)
276
+ return True
277
+
278
+ @inicializar_y_capturar_excepciones
279
+ def AgregarFlete(self, descripcion, importe):
280
+ "Agrega la informaciΓ³n referente al flete de la liquidaciΓ³n (opcional)"
281
+ flete = dict(descripcion=descripcion, importe=importe)
282
+ self.solicitud['flete'] = flete
283
+ return True
284
+
285
+ @inicializar_y_capturar_excepciones
286
+ def AgregarBonificacion(self, porcentaje, importe):
287
+ "Agrega la informaciΓ³n referente a las bonificaciones de la liquidaciΓ³n (opcional)"
288
+ bonif = dict(porcentaje=porcentaje, importe=importe)
289
+ self.solicitud['bonificacion'] = bonif
290
+ return True
291
+
292
+ @inicializar_y_capturar_excepciones
293
+ def AutorizarLiquidacion(self):
294
+ "Autorizar LiquidaciΓ³n ElectrΓ³nica de Tabaco Verde"
295
+ # limpio los elementos que no correspondan por estar vacios:
296
+ for campo in ["tributo", "retencion"]:
297
+ if not self.solicitud[campo]:
298
+ del self.solicitud[campo]
299
+ # llamo al webservice:
300
+ ret = self.client.generarLiquidacion(
301
+ auth={
302
+ 'token': self.Token, 'sign': self.Sign,
303
+ 'cuit': self.Cuit, },
304
+ solicitud=self.solicitud,
305
+ )
306
+ # analizo la respusta
307
+ ret = ret['respuesta']
308
+ self.__analizar_errores(ret)
309
+ liqs = ret.get('liquidacion', [])
310
+ liq = liqs[0] if liqs else None
311
+ self.AnalizarLiquidacion(liq)
312
+ return True
313
+
314
+ def AnalizarLiquidacion(self, liq):
315
+ "MΓ©todo interno para analizar la respuesta de AFIP"
316
+ # proceso los datos bΓ‘sicos de la liquidaciΓ³n (devuelto por consultar):
317
+ if liq:
318
+ cab = liq['cabecera']
319
+ self.CAE = str(cab['cae'])
320
+ self.FechaLiquidacion = cab['fechaLiquidacion']
321
+ self.NroComprobante = cab['nroComprobante']
322
+ tot = liq['totalesOperacion']
323
+ self.AlicuotaIVA = tot['alicuotaIVA']
324
+ self.ImporteNeto = tot['importeNeto']
325
+ self.ImporteIVA = tot['importeIVA']
326
+ self.Subtotal = tot['subtotal']
327
+ self.TotalRetenciones = tot['totalRetenciones']
328
+ self.TotalTributos = tot['totalTributos']
329
+ self.Total = tot['total']
330
+
331
+ # parΓ‘metros de salida:
332
+ self.params_out = dict(
333
+ tipo_cbte=liq['cabecera']['tipoComprobante'],
334
+ pto_vta=liq['cabecera']['puntoVenta'],
335
+ nro_cbte=liq['cabecera']['nroComprobante'],
336
+ fecha=liq['cabecera']['fechaLiquidacion'],
337
+ cod_deposito_acopio=liq['cabecera']['codDepositoAcopio'],
338
+ cae=str(liq['cabecera']['cae']),
339
+ domicilio_punto_venta=liq['cabecera']['domicilioPuntoVenta'],
340
+ domicilio_deposito_acopio=liq['cabecera']['domicilioDepositoAcopio'],
341
+ emisor=dict(
342
+ cuit=liq['emisor']['cuit'],
343
+ razon_social=liq['emisor']['razonSocial'],
344
+ situacion_iva=liq['emisor']['situacionIVA'],
345
+ domicilio=liq['emisor']['domicilio'],
346
+ fecha_inicio_actividad=liq['emisor']['fechaInicioActividad'],
347
+ ),
348
+ receptor=dict(
349
+ cuit=liq['receptor']['cuit'],
350
+ razon_social=liq['receptor']['razonSocial'],
351
+ nro_fet=liq['receptor'].get('nroFET'),
352
+ nro_socio=liq['receptor'].get('nroSocio'),
353
+ situacion_iva=liq['receptor']['situacionIVA'],
354
+ domicilio=liq['receptor']['domicilio'],
355
+ iibb=liq['receptor'].get('iibb'),
356
+ ),
357
+ control=liq['datosOperacion'].get('control'),
358
+ nro_interno=liq['datosOperacion'].get('nroInterno'),
359
+ condicion_venta=liq['datosOperacion'].get('condicionVenta'),
360
+ variedad_tabaco=liq['datosOperacion']['variedadTabaco'],
361
+ puerta=liq['datosOperacion'].get('puerta'),
362
+ nro_tarjeta=liq['datosOperacion'].get('nroTarjeta'),
363
+ horas=liq['datosOperacion'].get('horas'),
364
+ cod_provincia_origen_tabaco=liq['datosOperacion'].get('codProvinciaOrigenTabaco'),
365
+ tipo_compra=liq['datosOperacion'].get('tipoCompra'),
366
+ peso_total_fardos_kg=liq['detalleOperacion']['pesoTotalFardosKg'],
367
+ cantidad_total_fardos=liq['detalleOperacion']['cantidadTotalFardos'],
368
+ romaneos=[],
369
+ alicuota_iva=liq['totalesOperacion']['alicuotaIVA'],
370
+ importe_iva=liq['totalesOperacion']['importeIVA'],
371
+ importe_neto=liq['totalesOperacion']['importeNeto'],
372
+ subtotal=liq['totalesOperacion']['subtotal'],
373
+ total_retenciones=liq['totalesOperacion']['totalRetenciones'],
374
+ total_tributos=liq['totalesOperacion']['totalTributos'],
375
+ total=liq['totalesOperacion']['total'],
376
+ retenciones=[],
377
+ tributos=[],
378
+ cae_ajustado=liq.get("caeAjustado"),
379
+ pdf=liq.get('pdf'),
380
+ )
381
+ for romaneo in liq['detalleOperacion'].get('romaneo', []):
382
+ self.params_out['romaneos'].append(dict(
383
+ fecha_romaneo=romaneo['fechaRomaneo'],
384
+ nro_romaneo=romaneo['nroRomaneo'],
385
+ detalle_clase=[dict(
386
+ cantidad_fardos=det['cantidadFardos'],
387
+ cod_clase=det['codClase'],
388
+ importe=det['importe'],
389
+ peso_fardos_kg=det['pesoFardosKg'],
390
+ precio_x_kg_fardo=det['precioXKgFardo'],
391
+ ) for det in romaneo['detalleClase']],
392
+ ))
393
+ for ret in liq.get('retencion', []):
394
+ self.params_out['retenciones'].append(dict(
395
+ retencion_codigo=ret['codigo'],
396
+ retencion_importe=ret['importe'],
397
+ ))
398
+ for trib in liq.get('tributo', []):
399
+ self.params_out['tributos'].append(dict(
400
+ tributo_descripcion=trib.get('descripcion', ""),
401
+ tributo_base_imponible=trib['baseImponible'],
402
+ tributo_alicuota=trib['alicuota'],
403
+ tributo_codigo=trib['codigo'],
404
+ tributo_importe=trib['importe'],
405
+ ))
406
+ if DEBUG:
407
+ import pprint
408
+ pprint.pprint(self.params_out)
409
+ self.params_out['errores'] = self.errores
410
+
411
+ @inicializar_y_capturar_excepciones
412
+ def CrearAjuste(self, tipo_cbte, pto_vta, nro_cbte, fecha,
413
+ cod_deposito_acopio=None, tipo_ajuste=None, cuit_receptor=None,
414
+ iibb_emisor=None, iibb_receptor=None, fecha_inicio_actividad=None,
415
+ **kwargs):
416
+ "Inicializa internamente los datos de una liquidaciΓ³n para ajustar"
417
+ # codDepositoAcopio, tipoAjuste, cuitReceptor obligatorio ajustar liq.
418
+ # WSLTVv1.3 fechaLiquidacion/fechaInicioActividad para ajuste fΓ­sico
419
+ # creo el diccionario con los campos generales de la liquidaciΓ³n:
420
+ liq = dict(tipoComprobante=tipo_cbte,
421
+ nroComprobante=nro_cbte,
422
+ puntoVenta=pto_vta,
423
+ fechaAjusteLiquidacion=fecha,
424
+ fechaLiquidacion=fecha,
425
+ fechaInicioActividad=fecha_inicio_actividad,
426
+ codDepositoAcopio=cod_deposito_acopio,
427
+ tipoAjuste=tipo_ajuste,
428
+ cuitReceptor=cuit_receptor,
429
+ iibbReceptor=iibb_receptor,
430
+ iibbEmisor=iibb_emisor,
431
+ comprobanteAAjustar=[],
432
+ )
433
+ self.solicitud = dict(liquidacion=liq,
434
+ precioClase=[],
435
+ retencion=[],
436
+ tributo=[],
437
+ )
438
+ return True
439
+
440
+ @inicializar_y_capturar_excepciones
441
+ def AgregarComprobanteAAjustar(self, tipo_cbte, pto_vta, nro_cbte):
442
+ "Agrega comprobante a ajustar"
443
+ cbte = dict(tipoComprobante=tipo_cbte, puntoVenta=pto_vta, nroComprobante=nro_cbte)
444
+ self.solicitud['liquidacion']["comprobanteAAjustar"].append(cbte)
445
+ return True
446
+
447
+ @inicializar_y_capturar_excepciones
448
+ def AjustarLiquidacion(self):
449
+ "Ajustar LiquidaciΓ³n de Tabaco Verde"
450
+
451
+ # renombrar la clave principal de la estructura
452
+ if 'liquidacion' in self.solicitud:
453
+ liq = self.solicitud.pop('liquidacion')
454
+ self.solicitud["liquidacionAjuste"] = liq
455
+
456
+ # llamar al webservice:
457
+ ret = self.client.ajustarLiquidacion(
458
+ auth={
459
+ 'token': self.Token, 'sign': self.Sign,
460
+ 'cuit': self.Cuit, },
461
+ solicitud=self.solicitud,
462
+ )
463
+ # analizo la respusta
464
+ ret = ret['respuesta']
465
+ self.__analizar_errores(ret)
466
+ liqs = ret.get('liquidacion', [])
467
+ liq = liqs[0] if liqs else None
468
+ self.AnalizarLiquidacion(liq)
469
+ return True
470
+
471
+ @inicializar_y_capturar_excepciones
472
+ def GenerarAjusteFisico(self):
473
+ "Generar Ajuste FΓ­sico de LiquidaciΓ³n de Tabaco Verde (WSLTVv1.3)"
474
+
475
+ # renombrar la clave principal de la estructura
476
+ if 'liquidacion' in self.solicitud:
477
+ liq = self.solicitud.pop('liquidacion')
478
+ self.solicitud = liq
479
+
480
+ # llamar al webservice:
481
+ ret = self.client.generarAjusteFisico(
482
+ auth={
483
+ 'token': self.Token, 'sign': self.Sign,
484
+ 'cuit': self.Cuit, },
485
+ solicitud=self.solicitud,
486
+ )
487
+ # analizar el resultado:
488
+ ret = ret['respuesta']
489
+ self.__analizar_errores(ret)
490
+ liqs = ret.get('liquidacion', [])
491
+ liq = liqs[0] if liqs else None
492
+ self.AnalizarLiquidacion(liq)
493
+ return True
494
+
495
+ @inicializar_y_capturar_excepciones
496
+ def ConsultarLiquidacion(self, tipo_cbte=None, pto_vta=None, nro_cbte=None,
497
+ cae=None, pdf="liq.pdf"):
498
+ "Consulta una liquidaciΓ³n por No de Comprobante o CAE"
499
+ if cae:
500
+ ret = self.client.consultarLiquidacionXCAE(
501
+ auth={
502
+ 'token': self.Token, 'sign': self.Sign,
503
+ 'cuit': self.Cuit, },
504
+ solicitud={
505
+ 'cae': cae,
506
+ 'pdf': pdf and True or False,
507
+ },
508
+ )
509
+ else:
510
+ ret = self.client.consultarLiquidacionXNroComprobante(
511
+ auth={
512
+ 'token': self.Token, 'sign': self.Sign,
513
+ 'cuit': self.Cuit, },
514
+ solicitud={
515
+ 'puntoVenta': pto_vta,
516
+ 'nroComprobante': nro_cbte,
517
+ 'tipoComprobante': tipo_cbte,
518
+ 'pdf': pdf and True or False,
519
+ },
520
+ )
521
+ ret = ret['respuesta']
522
+ self.__analizar_errores(ret)
523
+ if 'liquidacion' in ret:
524
+ liqs = ret.get('liquidacion', [])
525
+ liq = liqs[0] if liqs else None
526
+ self.AnalizarLiquidacion(liq)
527
+ # guardo el PDF si se indico archivo y vino en la respuesta:
528
+ if pdf and 'pdf' in liq:
529
+ open(pdf, "wb").write(liq['pdf'])
530
+ return True
531
+
532
+ @inicializar_y_capturar_excepciones
533
+ def ConsultarUltimoComprobante(self, tipo_cbte=151, pto_vta=1):
534
+ "Consulta el ΓΊltimo No de Comprobante registrado"
535
+ ret = self.client.consultarUltimoComprobanteXPuntoVenta(
536
+ auth={
537
+ 'token': self.Token, 'sign': self.Sign,
538
+ 'cuit': self.Cuit, },
539
+ solicitud={
540
+ 'puntoVenta': pto_vta,
541
+ 'tipoComprobante': tipo_cbte},
542
+ )
543
+ ret = ret['respuesta']
544
+ self.__analizar_errores(ret)
545
+ self.NroComprobante = ret['nroComprobante']
546
+ return True
547
+
548
+ def ConsultarProvincias(self, sep="||"):
549
+ "Consulta las provincias habilitadas"
550
+ ret = self.client.consultarProvincias(
551
+ auth={
552
+ 'token': self.Token, 'sign': self.Sign,
553
+ 'cuit': self.Cuit, },
554
+ )['respuesta']
555
+ self.__analizar_errores(ret)
556
+ array = ret.get('provincia', [])
557
+ if sep is None:
558
+ return dict([(it['codigo'], it['descripcion']) for it in array])
559
+ else:
560
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
561
+ (it['codigo'], it['descripcion']) for it in array]
562
+
563
+ def ConsultarCondicionesVenta(self, sep="||"):
564
+ "Retorna un listado de cΓ³digos y descripciones de las condiciones de ventas"
565
+ ret = self.client.consultarCondicionesVenta(
566
+ auth={
567
+ 'token': self.Token, 'sign': self.Sign,
568
+ 'cuit': self.Cuit, },
569
+ )['respuesta']
570
+ self.__analizar_errores(ret)
571
+ array = ret.get('condicionVenta', [])
572
+ if sep is None:
573
+ return dict([(it['codigo'], it['descripcion']) for it in array])
574
+ else:
575
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
576
+ (it['codigo'], it['descripcion']) for it in array]
577
+
578
+ def ConsultarTributos(self, sep="||"):
579
+ "Retorna un listado de tributos con cΓ³digo, descripciΓ³n y signo."
580
+ ret = self.client.consultarTributos(
581
+ auth={
582
+ 'token': self.Token, 'sign': self.Sign,
583
+ 'cuit': self.Cuit, },
584
+ )['respuesta']
585
+ self.__analizar_errores(ret)
586
+ array = ret.get('tributo', [])
587
+ if sep is None:
588
+ return dict([(it['codigo'], it['descripcion']) for it in array])
589
+ else:
590
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
591
+ (it['codigo'], it['descripcion']) for it in array]
592
+
593
+ def ConsultarVariedadesClasesTabaco(self, sep="||"):
594
+ "Retorna un listado de variedades y clases de tabaco"
595
+ # El listado es una estructura anidada (varias clases por variedad)
596
+ #import dbg; dbg.set_trace()
597
+ ret = self.client.consultarVariedadesClasesTabaco(
598
+ auth={
599
+ 'token': self.Token, 'sign': self.Sign,
600
+ 'cuit': self.Cuit, },
601
+ )['respuesta']
602
+ self.__analizar_errores(ret)
603
+ self.XmlResponse = self.client.xml_response
604
+ array = ret.get('variedad', [])
605
+ if sep is None:
606
+ # sin separador, devuelve un diccionario con clave cod_variadedad
607
+ # y valor: {"descripcion": ds_variedad, "clases": lista_clases}
608
+ # siendo lista_clases = [{'codigo': ..., 'descripcion': ...}]
609
+ return dict([(it['codigo'], {'descripcion': it['descripcion'],
610
+ 'clases': it['clase']})
611
+ for it in array])
612
+ else:
613
+ # con separador, devuelve una lista de strings:
614
+ # || cod.variedad || desc.variedad || desc.clase || cod.clase ||
615
+ ret = []
616
+ for it in array:
617
+ for clase in it['clase']:
618
+ ret.append(
619
+ ("%s %%s %s %%s %s %%s %s %%s %s" %
620
+ (sep, sep, sep, sep, sep)) %
621
+ (it['codigo'], it['descripcion'],
622
+ clase['descripcion'], clase['codigo'])
623
+ )
624
+ return ret
625
+
626
+ def ConsultarRetencionesTabacaleras(self, sep="||"):
627
+ "Retorna un listado de retenciones tabacaleras con cΓ³digo y descripciΓ³n"
628
+ ret = self.client.consultarRetencionesTabacaleras(
629
+ auth={
630
+ 'token': self.Token, 'sign': self.Sign,
631
+ 'cuit': self.Cuit, },
632
+ )['respuesta']
633
+ self.__analizar_errores(ret)
634
+ array = ret.get('retencion', [])
635
+ if sep is None:
636
+ return dict([(it['codigo'], it['descripcion']) for it in array])
637
+ else:
638
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
639
+ (it['codigo'], it['descripcion']) for it in array]
640
+
641
+ def ConsultarDepositosAcopio(self, sep="||"):
642
+ "Retorna los depΓ³sitos de acopio pertenencientes al contribuyente"
643
+ ret = self.client.consultarDepositosAcopio(
644
+ auth={
645
+ 'token': self.Token, 'sign': self.Sign,
646
+ 'cuit': self.Cuit, },
647
+ )['respuesta']
648
+ self.__analizar_errores(ret)
649
+ array = ret.get('acopio', [])
650
+ if sep is None:
651
+ return array
652
+ else:
653
+ return [("%s %%s %s %%s %s %%s %s %%s %s" % (sep, sep, sep, sep, sep)) %
654
+ (it['codigo'], it['direccion'], it['localidad'], it['codigoPostal'])
655
+ for it in array]
656
+
657
+ def ConsultarPuntosVentas(self, sep="||"):
658
+ "Retorna los puntos de ventas autorizados para la utilizacion de WS"
659
+ ret = self.client.consultarPuntosVentas(
660
+ auth={
661
+ 'token': self.Token, 'sign': self.Sign,
662
+ 'cuit': self.Cuit, },
663
+ )['respuesta']
664
+ self.__analizar_errores(ret)
665
+ array = ret.get('puntoVenta', [])
666
+ if sep is None:
667
+ return dict([(it['codigo'], it['descripcion']) for it in array])
668
+ else:
669
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
670
+ (it['codigo'], it['descripcion']) for it in array]
671
+
672
+ def MostrarPDF(self, archivo, imprimir=False):
673
+ try:
674
+ if sys.platform == "linux2":
675
+ import subprocess
676
+ subprocess.run(["evince", archivo], check=False)
677
+ else:
678
+ operation = imprimir and "print" or ""
679
+ os.startfile(archivo, operation)
680
+ return True
681
+ except Exception as e:
682
+ self.Excepcion = str(e)
683
+ return False
684
+
685
+
686
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
687
+ INSTALL_DIR = WSLTV.InstallDir = get_install_dir()
688
+
689
+
690
+ if __name__ == '__main__':
691
+ if '--ayuda' in sys.argv:
692
+ print(LICENCIA)
693
+ print(AYUDA)
694
+ sys.exit(0)
695
+ if '--formato' in sys.argv:
696
+ print("Formato:")
697
+ for msg, formato in []:
698
+ comienzo = 1
699
+ print("=== %s ===" % msg)
700
+ for fmt in formato:
701
+ clave, longitud, tipo = fmt[0:3]
702
+ dec = len(fmt) > 3 and fmt[3] or (tipo == 'I' and '2' or '')
703
+ print(" * Campo: %-20s PosiciΓ³n: %3d Longitud: %4d Tipo: %s Decimales: %s" % (
704
+ clave, comienzo, longitud, tipo, dec))
705
+ comienzo += longitud
706
+ sys.exit(0)
707
+
708
+ if "--register" in sys.argv or "--unregister" in sys.argv:
709
+ import win32com.server.register
710
+ win32com.server.register.UseCommandLine(WSLTV)
711
+ sys.exit(0)
712
+
713
+ import csv
714
+ from configparser import SafeConfigParser
715
+
716
+ from .wsaa import WSAA
717
+
718
+ try:
719
+
720
+ if "--version" in sys.argv:
721
+ print("VersiΓ³n: ", __version__)
722
+
723
+ if len(sys.argv) > 1 and sys.argv[1].endswith(".ini"):
724
+ CONFIG_FILE = sys.argv[1]
725
+ print("Usando configuracion:", CONFIG_FILE)
726
+
727
+ config = SafeConfigParser()
728
+ config.read(CONFIG_FILE)
729
+ CERT = config.get('WSAA', 'CERT')
730
+ PRIVATEKEY = config.get('WSAA', 'PRIVATEKEY')
731
+ CUIT = config.get('WSLTV', 'CUIT')
732
+ ENTRADA = config.get('WSLTV', 'ENTRADA')
733
+ SALIDA = config.get('WSLTV', 'SALIDA')
734
+ PDF = config.has_option('WSLTV', 'PDF') and config.get('WSLTV', 'PDF') or "liq.pdf"
735
+
736
+ if config.has_option('WSAA', 'URL') and not HOMO:
737
+ WSAA_URL = config.get('WSAA', 'URL')
738
+ else:
739
+ WSAA_URL = None # wsaa.WSAAURL
740
+ if config.has_option('WSLTV', 'URL') and not HOMO:
741
+ WSLTV_URL = config.get('WSLTV', 'URL')
742
+ else:
743
+ WSLTV_URL = WSDL
744
+
745
+ PROXY = config.has_option('WSAA', 'PROXY') and config.get('WSAA', 'PROXY') or None
746
+ CACERT = config.has_option('WSAA', 'CACERT') and config.get('WSAA', 'CACERT') or None
747
+ WRAPPER = config.has_option('WSAA', 'WRAPPER') and config.get('WSAA', 'WRAPPER') or None
748
+
749
+ if config.has_option('WSLTV', 'TIMEOUT'):
750
+ TIMEOUT = int(config.get('WSLTV', 'TIMEOUT'))
751
+
752
+ if config.has_section('DBF'):
753
+ conf_dbf = dict(config.items('DBF'))
754
+ if DEBUG:
755
+ print("conf_dbf", conf_dbf)
756
+ else:
757
+ conf_dbf = {}
758
+
759
+ DEBUG = '--debug' in sys.argv
760
+ XML = '--xml' in sys.argv
761
+
762
+ if DEBUG:
763
+ print("Usando ConfiguraciΓ³n:")
764
+ print("WSAA_URL:", WSAA_URL)
765
+ print("WSLTV_URL:", WSLTV_URL)
766
+ print("CACERT", CACERT)
767
+ print("WRAPPER", WRAPPER)
768
+ print("timeout:", TIMEOUT)
769
+ # obteniendo el TA
770
+ from .wsaa import WSAA
771
+ wsaa = WSAA()
772
+ ta = wsaa.Autenticar("wsltv", CERT, PRIVATEKEY, wsdl=WSAA_URL,
773
+ proxy=PROXY, wrapper=WRAPPER, cacert=CACERT)
774
+ if not ta:
775
+ sys.exit("Imposible autenticar con WSAA: %s" % wsaa.Excepcion)
776
+
777
+ # cliente soap del web service
778
+ wsltv = WSLTV()
779
+ wsltv.LanzarExcepciones = True
780
+ wsltv.Conectar(url=WSLTV_URL, proxy=PROXY, wrapper=WRAPPER, cacert=CACERT, timeout=TIMEOUT)
781
+ wsltv.SetTicketAcceso(ta)
782
+ wsltv.Cuit = CUIT
783
+
784
+ if '--dummy' in sys.argv:
785
+ ret = wsltv.Dummy()
786
+ print("AppServerStatus", wsltv.AppServerStatus)
787
+ print("DbServerStatus", wsltv.DbServerStatus)
788
+ print("AuthServerStatus", wsltv.AuthServerStatus)
789
+ # sys.exit(0)
790
+
791
+ if '--json' in sys.argv and os.path.exists("wsltv.json"):
792
+ # cargar un archivo de texto:
793
+ with open(ENTRADA, "r") as f:
794
+ wsltv.solicitud = json.load(f, encoding="utf-8")
795
+
796
+ if '--autorizar' in sys.argv:
797
+
798
+ if '--prueba' in sys.argv:
799
+
800
+ # genero una liquidaciΓ³n de ejemplo:
801
+
802
+ tipo_cbte = 150
803
+ pto_vta = 6
804
+
805
+ if not '--prueba' in sys.argv:
806
+ # consulto el ΓΊltimo nΓΊmero de orden emitido:
807
+ ok = wsltv.ConsultarUltimoComprobante(tipo_cbte, pto_vta)
808
+ if ok:
809
+ nro_cbte = wsltv.NroComprobante + 1
810
+ else:
811
+ nro_cbte = 1
812
+
813
+ # datos de la cabecera:
814
+ fecha = '2016-04-18'
815
+ cod_deposito_acopio = 1000
816
+ tipo_compra = 'CPS'
817
+ variedad_tabaco = 'BR'
818
+ cod_provincia_origen_tabaco = 1
819
+ puerta = 22
820
+ nro_tarjeta = 6569866
821
+ horas = 12
822
+ control = "FFAA"
823
+ nro_interno = "77888"
824
+ fecha_inicio_actividad = "2016-04-01"
825
+
826
+ # cargo la liquidaciΓ³n:
827
+ wsltv.CrearLiquidacion(tipo_cbte, pto_vta, nro_cbte, fecha,
828
+ cod_deposito_acopio, tipo_compra,
829
+ variedad_tabaco, cod_provincia_origen_tabaco,
830
+ puerta, nro_tarjeta, horas, control,
831
+ nro_interno, iibb_emisor=None,
832
+ fecha_inicio_actividad=fecha_inicio_actividad)
833
+
834
+ wsltv.AgregarCondicionVenta(codigo=99, descripcion="otra")
835
+
836
+ # datos del receptor:
837
+ cuit = 20111111112
838
+ iibb = 123456
839
+ nro_socio = 11223
840
+ nro_fet = 22
841
+ wsltv.AgregarReceptor(cuit, iibb, nro_socio, nro_fet)
842
+
843
+ # datos romaneo:
844
+ nro_romaneo = 321
845
+ fecha_romaneo = "2015-12-10"
846
+ wsltv.AgregarRomaneo(nro_romaneo, fecha_romaneo)
847
+ # fardo:
848
+ cod_trazabilidad = 356
849
+ clase_tabaco = 4
850
+ peso = 900
851
+ wsltv.AgregarFardo(cod_trazabilidad, clase_tabaco, peso)
852
+
853
+ # precio clase:
854
+ precio = 190
855
+ wsltv.AgregarPrecioClase(clase_tabaco, precio)
856
+
857
+ # retencion:
858
+ descripcion = "otra retencion"
859
+ cod_retencion = 15
860
+ importe = 12
861
+ wsltv.AgregarRetencion(cod_retencion, descripcion, importe)
862
+
863
+ # tributo:
864
+ codigo_tributo = 99
865
+ descripcion = "Ganancias"
866
+ base_imponible = 15000
867
+ alicuota = 8
868
+ importe = 1200
869
+ wsltv.AgregarTributo(codigo_tributo, descripcion, base_imponible, alicuota, importe)
870
+
871
+ # flete:
872
+ descripcion = "transporte"
873
+ importe = 1000.00
874
+ wsltv.AgregarFlete(descripcion, importe)
875
+
876
+ # bonificacion:
877
+ porcentaje = 10.0
878
+ importe = 100.00
879
+ wsltv.AgregarBonificacion(porcentaje, importe)
880
+
881
+ if '--testing' in sys.argv:
882
+ # mensaje de prueba (no realiza llamada remota),
883
+ # usar solo si no estΓ‘ operativo, cargo respuesta:
884
+ wsltv.LoadTestXML("tests/xml/wsltv_aut_test_pdf.xml")
885
+
886
+ print("Liquidacion: pto_vta=%s nro_cbte=%s tipo_cbte=%s" % (
887
+ wsltv.solicitud['liquidacion']['puntoVenta'],
888
+ wsltv.solicitud['liquidacion']['nroComprobante'],
889
+ wsltv.solicitud['liquidacion']['tipoComprobante'],
890
+ ))
891
+
892
+ if not '--dummy' in sys.argv:
893
+ print("Autorizando...")
894
+ ret = wsltv.AutorizarLiquidacion()
895
+
896
+ if wsltv.Excepcion:
897
+ print("EXCEPCION:", wsltv.Excepcion, file=sys.stderr)
898
+ if DEBUG:
899
+ print(wsltv.Traceback, file=sys.stderr)
900
+ print("Errores:", wsltv.Errores)
901
+ print("CAE", wsltv.CAE)
902
+ print("FechaLiquidacion", wsltv.FechaLiquidacion)
903
+ print("NroComprobante", wsltv.NroComprobante)
904
+ print("ImporteNeto", wsltv.ImporteNeto)
905
+ print("AlicuotaIVA", wsltv.AlicuotaIVA)
906
+ print("ImporteIVA", wsltv.ImporteIVA)
907
+ print("Subtotal", wsltv.Subtotal)
908
+ print("TotalRetenciones", wsltv.TotalRetenciones)
909
+ print("TotalTributos", wsltv.TotalTributos)
910
+ print("Total", wsltv.Total)
911
+
912
+ pdf = wsltv.GetParametro("pdf")
913
+ if pdf:
914
+ open(PDF, "wb").write(pdf)
915
+
916
+ if '--testing' in sys.argv:
917
+ assert wsltv.CAE == "85523002502850"
918
+ assert wsltv.Total == 205685.46
919
+ assert wsltv.GetParametro("fecha") == "2016-01-01"
920
+ assert wsltv.GetParametro("peso_total_fardos_kg") == "900"
921
+ assert wsltv.GetParametro("cantidad_total_fardos") == "1"
922
+ assert wsltv.GetParametro("emisor", "domicilio") == 'Peru 100'
923
+ assert wsltv.GetParametro("emisor", "razon_social") == 'JOCKER'
924
+ assert wsltv.GetParametro("receptor", "domicilio") == 'Calle 1'
925
+ assert wsltv.GetParametro("receptor", "razon_social") == 'CUIT PF de Prueba gen\xe9rica'
926
+ assert wsltv.GetParametro("romaneos", 0, "detalle_clase", 0, "cantidad_fardos") == "1"
927
+ assert wsltv.GetParametro("romaneos", 0, "detalle_clase", 0, "cod_clase") == "4"
928
+ assert wsltv.GetParametro("romaneos", 0, "detalle_clase", 0, "importe") == "171000.0"
929
+ assert wsltv.GetParametro("romaneos", 0, "detalle_clase", 0, "peso_fardos_kg") == "900"
930
+ assert wsltv.GetParametro("romaneos", 0, "detalle_clase", 0, "precio_x_kg_fardo") == "190.0"
931
+ assert wsltv.GetParametro("romaneos", 0, "nro_romaneo") == "321"
932
+ assert wsltv.GetParametro("romaneos", 0, "fecha_romaneo") == "2015-12-10"
933
+
934
+ if DEBUG:
935
+ pprint.pprint(wsltv.params_out)
936
+
937
+ if '--generar-ajuste-fisico' in sys.argv:
938
+ if '--prueba' in sys.argv:
939
+ # ejemplo documentaciΓ³n AFIP:
940
+ if '--testing' in sys.argv:
941
+ wsltv.LoadTestXML("tests/xml/wsltv_ajuste_test_pdf.xml")
942
+ wsltv.CrearAjuste(tipo_cbte=151, pto_vta=2, nro_cbte=1,
943
+ fecha='2016-09-09',
944
+ fecha_inicio_actividad="1900-01-01",
945
+ )
946
+ wsltv.AgregarComprobanteAAjustar(tipo_cbte=151, pto_vta=3697, nro_cbte=2)
947
+ wsltv.GenerarAjusteFisico()
948
+ print("CAE Ajustado:", wsltv.GetParametro("cae_ajustado"))
949
+ if '--testing' in sys.argv:
950
+ assert wsltv.GetParametro("cae_ajustado") == "86029002591067"
951
+
952
+ if '--ajustar' in sys.argv:
953
+ if '--prueba' in sys.argv:
954
+ # ejemplo documentaciΓ³n AFIP:
955
+ if '--testing' in sys.argv:
956
+ wsltv.LoadTestXML("tests/xml/wsltv_ajuste_test.xml")
957
+ wsltv.CrearAjuste(tipo_cbte=151, pto_vta=2958, nro_cbte=13,
958
+ fecha='2015-12-31',
959
+ cod_deposito_acopio=201, tipo_ajuste="C",
960
+ cuit_receptor=222222222,
961
+ iibb_receptor=2,
962
+ fecha_inicio_actividad="2010-01-01"
963
+ )
964
+ wsltv.AgregarComprobanteAAjustar(tipo_cbte=151, pto_vta=4521, nro_cbte=12345678)
965
+ wsltv.AgregarPrecioClase(clase_tabaco=111, precio=25, total_kilos=41, total_fardos=1)
966
+ wsltv.AgregarRetencion(cod_retencion=11, descripcion=None, importe=20)
967
+ wsltv.AgregarTributo(codigo_tributo=99, descripcion="Descripcion otros tributos", base_imponible=2, alicuota=2, importe=10)
968
+ wsltv.AjustarLiquidacion()
969
+ print("CAE:", wsltv.CAE)
970
+ if '--testing' in sys.argv:
971
+ assert wsltv.GetParametro("cae") == "86011002510675"
972
+
973
+ if '--consultar' in sys.argv:
974
+ tipo_cbte = 151
975
+ pto_vta = 1
976
+ nro_cbte = 0
977
+ try:
978
+ tipo_cbte = sys.argv[sys.argv.index("--consultar") + 1]
979
+ pto_vta = sys.argv[sys.argv.index("--consultar") + 2]
980
+ nro_cbte = sys.argv[sys.argv.index("--consultar") + 3]
981
+ except IndexError:
982
+ pass
983
+ if '--testing' in sys.argv:
984
+ # mensaje de prueba (no realiza llamada remota),
985
+ # usar solo si no estΓ‘ operativo, cargo prueba:
986
+ wsltv.LoadTestXML("tests/xml/wsltv_cons_test.xml")
987
+ print("Consultando: tipo_cbte=%s pto_vta=%s nro_cbte=%s" % (tipo_cbte, pto_vta, nro_cbte))
988
+ ret = wsltv.ConsultarLiquidacion(tipo_cbte, pto_vta, nro_cbte)
989
+ print("CAE", wsltv.CAE)
990
+ print("Errores:", wsltv.Errores)
991
+
992
+ if DEBUG:
993
+ pprint.pprint(wsltv.params_out)
994
+
995
+ if '--mostrar' in sys.argv and pdf:
996
+ wsltv.MostrarPDF(archivo=pdf,
997
+ imprimir='--imprimir' in sys.argv)
998
+
999
+ if '--json' in sys.argv:
1000
+ import json
1001
+ with open(SALIDA, "w") as f:
1002
+ json.dump(wsltv.solicitud, f, sort_keys=True, indent=4, encoding="utf-8",)
1003
+
1004
+ if '--ult' in sys.argv:
1005
+ tipo_cbte = 151
1006
+ pto_vta = 1
1007
+ try:
1008
+ tipo_cbte = sys.argv[sys.argv.index("--ult") + 1]
1009
+ pto_vta = sys.argv[sys.argv.index("--ult") + 2]
1010
+ except IndexError:
1011
+ pass
1012
+
1013
+ print("Consultando ultimo nro_cbte para pto_vta=%s" % pto_vta, end=' ')
1014
+ ret = wsltv.ConsultarUltimoComprobante(tipo_cbte, pto_vta)
1015
+ if wsltv.Excepcion:
1016
+ print("EXCEPCION:", wsltv.Excepcion, file=sys.stderr)
1017
+ if DEBUG:
1018
+ print(wsltv.Traceback, file=sys.stderr)
1019
+ print("Ultimo Nro de Comprobante", wsltv.NroComprobante)
1020
+ print("Errores:", wsltv.Errores)
1021
+ sys.exit(0)
1022
+
1023
+ # Recuperar parΓ‘metros:
1024
+
1025
+ if '--provincias' in sys.argv:
1026
+ ret = wsltv.ConsultarProvincias()
1027
+ print("\n".join(ret))
1028
+
1029
+ if '--condicionesventa' in sys.argv:
1030
+ ret = wsltv.ConsultarCondicionesVenta()
1031
+ print("\n".join(ret))
1032
+
1033
+ if '--tributos' in sys.argv:
1034
+ ret = wsltv.ConsultarTributos()
1035
+ print("\n".join(ret))
1036
+
1037
+ if '--retenciones' in sys.argv:
1038
+ ret = wsltv.ConsultarRetencionesTabacaleras()
1039
+ print("\n".join(ret))
1040
+
1041
+ if '--variedades' in sys.argv:
1042
+ ret = wsltv.ConsultarVariedadesClasesTabaco()
1043
+ print("\n".join(ret))
1044
+
1045
+ if '--depositos' in sys.argv:
1046
+ ret = wsltv.ConsultarDepositosAcopio()
1047
+ print("\n".join(ret))
1048
+
1049
+ if '--puntosventa' in sys.argv:
1050
+ ret = wsltv.ConsultarPuntosVentas()
1051
+ print("\n".join(ret))
1052
+
1053
+ print("hecho.")
1054
+
1055
+ except SoapFault as e:
1056
+ print("Falla SOAP:", e.faultcode, e.faultstring.encode("ascii", "ignore"), file=sys.stderr)
1057
+ sys.exit(3)
1058
+ except Exception as e:
1059
+ try:
1060
+ print(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0], file=sys.stderr)
1061
+ except BaseException:
1062
+ print("ExcepciΓ³n no disponible:", type(e), file=sys.stderr)
1063
+ if DEBUG:
1064
+ raise
1065
+ sys.exit(5)
1066
+ finally:
1067
+ if XML:
1068
+ open("wsltv_request.xml", "w").write(wsltv.client.xml_request)
1069
+ open("wsltv_response.xml", "w").write(wsltv.client.xml_response)
app/pyafipws/wslum.py ADDED
@@ -0,0 +1,923 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+
14
+ import shelve
15
+ import sys
16
+ import datetime
17
+ import decimal
18
+ import os
19
+ from .utils import leer, escribir, leer_dbf, guardar_dbf, N, A, I, json, BaseWS, inicializar_y_capturar_excepciones, get_install_dir
20
+ from . import utils
21
+ from fpdf import Template
22
+ from pysimplesoap.client import SoapFault
23
+ import pprint
24
+ import traceback
25
+ """MΓ³dulo para obtener cΓ³digo de autorizaciΓ³n electrΓ³nica (CAE) para
26
+ Liquidación Única Mensual (lechería) del web service WSLUM de AFIP
27
+ """
28
+
29
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
30
+ __copyright__ = "Copyright (C) 2016 Mariano Reingart"
31
+ __license__ = "GPL 3.0"
32
+ __version__ = "1.03b"
33
+
34
+ LICENCIA = """
35
+ wslum.py: Interfaz para generar CΓ³digo de AutorizaciΓ³n ElectrΓ³nica (CAE) para
36
+ Liquidación Única Mensual de lechería (LumService)
37
+ Copyright (C) 2016 Mariano Reingart reingart@gmail.com
38
+ http://www.sistemasagiles.com.ar/trac/wiki/LiquidacionUnicaMensualLecheria
39
+
40
+ Este progarma es software libre, se entrega ABSOLUTAMENTE SIN GARANTIA
41
+ y es bienvenido a redistribuirlo respetando la licencia GPLv3.
42
+
43
+ Para informaciΓ³n adicional sobre garantΓ­a, soporte tΓ©cnico comercial
44
+ e incorporaciΓ³n/distribuciΓ³n en programas propietarios ver PyAfipWs:
45
+ http://www.sistemasagiles.com.ar/trac/wiki/PyAfipWs
46
+ """
47
+
48
+ AYUDA = """
49
+ Opciones:
50
+ --ayuda: este mensaje
51
+
52
+ --debug: modo depuraciΓ³n (detalla y confirma las operaciones)
53
+ --formato: muestra el formato de los archivos de entrada/salida
54
+ --prueba: genera y autoriza una liquidaciΓ³n de prueba (no usar en producciΓ³n!)
55
+ --xml: almacena los requerimientos y respuestas XML (depuraciΓ³n)
56
+ --json: utilizar formato json para el archivo de intercambio
57
+ --dummy: consulta estado de servidores
58
+
59
+ --autorizar: Autorizar Liquidación Única Mensual (lechería) (generarLiquidacion)
60
+ --ult: Consulta el ΓΊltimo nΓΊmero de orden registrado en AFIP
61
+ (consultarUltimoComprobanteXPuntoVenta)
62
+ --consultar: Consulta una liquidaciΓ³n registrada en AFIP
63
+ (consultarLiquidacionXNroComprobante / consultarLiquidacionXCAE)
64
+
65
+ --pdf: descarga la liquidaciΓ³n en formato PDF
66
+ --mostrar: muestra el documento PDF generado (usar con --pdf)
67
+ --imprimir: imprime el documento PDF generado (usar con --mostrar y --pdf)
68
+
69
+ --provincias: obtiene el listado de provincias (cΓ³digo/descripciΓ³n)
70
+ --localidades: obtiene el listado de localidades para una provincia
71
+ --bonificaciones_penalizaciones: obtiene el listado de tributos
72
+ --otros_impuestos: obtiene el listado de las retenciones de tabaco
73
+ --puntosventa: obtiene el listado de puntos de venta habilitados
74
+
75
+ Ver wslum.ini para parΓ‘metros de configuraciΓ³n (URL, certificados, etc.)"
76
+ """
77
+
78
+
79
+ # importo funciones compartidas:
80
+
81
+
82
+ WSDL = "https://fwshomo.afip.gov.ar/wslum/LumService?wsdl"
83
+ #WSDL = "https://serviciosjava.afip.gov.ar/wslum/LumService?wsdl"
84
+
85
+ DEBUG = False
86
+ XML = False
87
+ CONFIG_FILE = "wslum.ini"
88
+ HOMO = True
89
+
90
+
91
+ class WSLUM(BaseWS):
92
+ "Interfaz para el WebService de Liquidación Única Mensual (lechería)"
93
+ _public_methods_ = ['Conectar', 'Dummy', 'SetTicketAcceso', 'DebugLog',
94
+ 'AutorizarLiquidacion',
95
+ 'CrearLiquidacion',
96
+ 'AgregarTambero', 'AgregarCondicionVenta',
97
+ 'AgregarTambo', 'AgregarUbicacionTambo',
98
+ 'AgregarBalanceLitrosPorcentajesSolidos',
99
+ 'AgregarConceptosBasicosMercadoInterno',
100
+ 'AgregarConceptosBasicosMercadoExterno',
101
+ 'AgregarBonificacionPenalizacion',
102
+ 'AgregarOtroImpuesto',
103
+ 'AgregarRemito',
104
+ 'ConsultarLiquidacion', 'ConsultarUltimoComprobante',
105
+ 'AgregarAjuste',
106
+ 'LeerDatosLiquidacion',
107
+ 'ConsultarBonificacionesPenalizaciones',
108
+ 'ConsultarOtrosImpuestos',
109
+ 'ConsultarPuntosVentas',
110
+ 'ConsultarProvincias', 'ConsultarLocalidades',
111
+ 'MostrarPDF',
112
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
113
+ 'SetParametros', 'SetParametro', 'GetParametro',
114
+ ]
115
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
116
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
117
+ 'Excepcion', 'ErrCode', 'ErrMsg', 'LanzarExcepciones', 'Errores',
118
+ 'XmlRequest', 'XmlResponse', 'Version', 'Traceback', 'InstallDir',
119
+ 'CAE', 'NroComprobante', 'FechaComprobante',
120
+ 'AlicuotaIVA', 'TotalNeto', 'ImporteIVA',
121
+ 'TotalBonificacionesCalidad', 'TotalPenalizacionesCalidad',
122
+ 'TotalBonificacionesComerciales', 'TotalDebitosComerciales',
123
+ 'TotalOtrosImpuestos', 'Total',
124
+ ]
125
+ _reg_progid_ = "WSLUM"
126
+ _reg_clsid_ = "{4CBB2DF8-7AAE-434E-916D-9D663BB1CAFC}"
127
+
128
+ # Variables globales para BaseWS:
129
+ HOMO = HOMO
130
+ WSDL = WSDL
131
+ LanzarExcepciones = False
132
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
133
+
134
+ def inicializar(self):
135
+ BaseWS.inicializar(self)
136
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
137
+ self.errores = []
138
+ self.CAE = ""
139
+ self.NroComprobante = self.FechaComprobante = ''
140
+ self.AlicuotaIVA = self.TotalNeto = self.ImporteIVA = None
141
+ self.TotalBonificacionesCalidad = None
142
+ self.TotalPenalizacionesCalidad = None
143
+ self.TotalBonificacionesComerciales = None
144
+ self.TotalDebitosComerciales = None
145
+ self.TotalOtrosImpuestos = None
146
+ self.Total = None
147
+ self.datos = {}
148
+
149
+ @inicializar_y_capturar_excepciones
150
+ def Conectar(self, cache=None, url="", proxy="", wrapper="", cacert=None, timeout=30):
151
+ "Establecer la conexiΓ³n a los servidores de la AFIP"
152
+ # llamo al constructor heredado:
153
+ ok = BaseWS.Conectar(self, cache, url, proxy, wrapper, cacert, timeout)
154
+ if False and ok:
155
+ # corrijo ubicaciΓ³n del servidor (puerto htttp 80 en el WSDL)
156
+ location = self.client.services['LumService']['ports']['LumEndPoint']['location']
157
+ if location.startswith("http://"):
158
+ print("Corrigiendo WSDL ...", location, end=' ')
159
+ location = location.replace("http://", "https://").replace(":80", ":443")
160
+ self.client.services['LumService']['ports']['LumEndPoint']['location'] = location
161
+ print(location)
162
+ return ok
163
+
164
+ def __analizar_errores(self, ret):
165
+ "Comprueba y extrae errores si existen en la respuesta XML"
166
+ errores = []
167
+ if 'errores' in ret:
168
+ errores.extend(ret['errores'])
169
+ if errores:
170
+ self.Errores = ["%(codigo)s: %(descripcion)s" % err['error'][0]
171
+ for err in errores]
172
+ self.errores = [
173
+ {'codigo': err['error'][0]['codigo'],
174
+ 'descripcion': err['error'][0]['descripcion'].replace("\n", "")
175
+ .replace("\r", "")}
176
+ for err in errores]
177
+ self.ErrCode = ' '.join(self.Errores)
178
+ self.ErrMsg = '\n'.join(self.Errores)
179
+
180
+ @inicializar_y_capturar_excepciones
181
+ def Dummy(self):
182
+ "Obtener el estado de los servidores de la AFIP"
183
+ results = self.client.dummy()['respuesta']
184
+ self.AppServerStatus = str(results['appserver'])
185
+ self.DbServerStatus = str(results['dbserver'])
186
+ self.AuthServerStatus = str(results['authserver'])
187
+ return True
188
+
189
+ @inicializar_y_capturar_excepciones
190
+ def CrearLiquidacion(self, tipo_cbte, pto_vta, nro_cbte, fecha, periodo,
191
+ iibb_adquirente=None, domicilio_sede=None,
192
+ inscripcion_registro_publico=None,
193
+ datos_adicionales=None, alicuota_iva=None, **kwargs):
194
+ "Inicializa internamente los datos de una liquidaciΓ³n para autorizar"
195
+ # creo el diccionario con los campos generales de la liquidaciΓ³n:
196
+ liq = {'tipoComprobante': tipo_cbte, 'puntoVenta': pto_vta,
197
+ 'nroComprobante': nro_cbte, 'fechaComprobante': fecha,
198
+ 'periodo': periodo, 'iibbAdquirente': iibb_adquirente,
199
+ 'domicilioSede': domicilio_sede,
200
+ 'inscripcionRegistroPublico': inscripcion_registro_publico,
201
+ 'datosAdicionales': datos_adicionales,
202
+ 'alicuotaIVA': alicuota_iva,
203
+ }
204
+ liq["condicionVenta"] = []
205
+ self.solicitud = dict(liquidacion=liq,
206
+ bonificacionPenalizacion=[],
207
+ otroImpuesto=[],
208
+ remito=[]
209
+ )
210
+ return True
211
+
212
+ @inicializar_y_capturar_excepciones
213
+ def AgregarCondicionVenta(self, codigo, descripcion=None, **kwargs):
214
+ "Agrego una o mΓ‘s condicion de venta a la liq."
215
+ cond = {'codigo': codigo, 'descripcion': descripcion}
216
+ self.solicitud['liquidacion']['condicionVenta'].append(cond)
217
+ return True
218
+
219
+ @inicializar_y_capturar_excepciones
220
+ def AgregarTambero(self, cuit, iibb=None, **kwargs):
221
+ "Agrego los datos del productor a la liq."
222
+ tambero = {'cuit': cuit, 'iibb': iibb}
223
+ self.solicitud['tambero'] = tambero
224
+ return True
225
+
226
+ @inicializar_y_capturar_excepciones
227
+ def AgregarTambo(self, nro_tambo_interno, nro_renspa,
228
+ fecha_venc_cert_tuberculosis, fecha_venc_cert_brucelosis,
229
+ nro_tambo_provincial=None, **kwargs):
230
+ "Agrego los datos del productor a la liq."
231
+ tambo = {'nroTamboInterno': nro_tambo_interno,
232
+ 'nroTamboProvincial': nro_tambo_provincial,
233
+ 'nroRenspa': nro_renspa,
234
+ 'ubicacionTambo': {},
235
+ 'fechaVencCertTuberculosis': fecha_venc_cert_tuberculosis,
236
+ 'fechaVencCertBrucelosis': fecha_venc_cert_brucelosis}
237
+ self.solicitud['tambo'] = tambo
238
+ return True
239
+
240
+ @inicializar_y_capturar_excepciones
241
+ def AgregarUbicacionTambo(self, latitud, longitud, domicilio,
242
+ cod_localidad, cod_provincia, codigo_postal,
243
+ nombre_partido_depto, **kwargs):
244
+ "Agrego los datos del productor a la liq."
245
+ ubic_tambo = {'latitud': latitud,
246
+ 'longitud': longitud,
247
+ 'domicilio': domicilio,
248
+ 'codLocalidad': cod_localidad,
249
+ 'codProvincia': cod_provincia,
250
+ 'nombrePartidoDepto': nombre_partido_depto,
251
+ 'codigoPostal': codigo_postal}
252
+ self.solicitud['tambo']['ubicacionTambo'] = ubic_tambo
253
+ return True
254
+
255
+ @inicializar_y_capturar_excepciones
256
+ def AgregarBalanceLitrosPorcentajesSolidos(self, litros_remitidos, litros_decomisados,
257
+ kg_grasa, kg_proteina, **kwargs):
258
+ "Agrega balance litros y porcentajes sΓ³lidos a la liq. (obligatorio)"
259
+ d = {'litrosRemitidos': litros_remitidos,
260
+ 'litrosDecomisados': litros_decomisados,
261
+ 'kgGrasa': kg_grasa,
262
+ 'kgProteina': kg_proteina}
263
+ self.solicitud['balanceLitrosPorcentajesSolidos'] = d
264
+
265
+ @inicializar_y_capturar_excepciones
266
+ def AgregarConceptosBasicosMercadoInterno(self, kg_produccion_gb, precio_por_kg_produccion_gb,
267
+ kg_produccion_pr, precio_por_kg_produccion_pr,
268
+ kg_crecimiento_gb, precio_por_kg_crecimiento_gb,
269
+ kg_crecimiento_pr, precio_por_kg_crecimiento_pr,
270
+ **kwargs):
271
+ "Agrega balance litros y porcentajes sΓ³lidos (mercado interno)"
272
+ d = {'kgProduccionGB': kg_produccion_gb,
273
+ 'precioPorKgProduccionGB': precio_por_kg_produccion_gb,
274
+ 'kgProduccionPR': kg_produccion_pr,
275
+ 'precioPorKgProduccionPR': precio_por_kg_produccion_pr,
276
+ 'kgCrecimientoGB': kg_crecimiento_gb,
277
+ 'precioPorKgCrecimientoGB': precio_por_kg_crecimiento_gb,
278
+ 'kgCrecimientoPR': kg_crecimiento_pr,
279
+ 'precioPorKgCrecimientoPR': precio_por_kg_crecimiento_pr}
280
+ self.solicitud['conceptosBasicosMercadoInterno'] = d
281
+ return True
282
+
283
+ @inicializar_y_capturar_excepciones
284
+ def AgregarConceptosBasicosMercadoExterno(self, kg_produccion_gb, precio_por_kg_produccion_gb,
285
+ kg_produccion_pr, precio_por_kg_produccion_pr,
286
+ kg_crecimiento_gb, precio_por_kg_crecimiento_gb,
287
+ kg_crecimiento_pr, precio_por_kg_crecimiento_pr,
288
+ **kwargs):
289
+ "Agrega balance litros y porcentajes sΓ³lidos (mercado externo)"
290
+ d = {'kgProduccionGB': kg_produccion_gb,
291
+ 'precioPorKgProduccionGB': precio_por_kg_produccion_gb,
292
+ 'kgProduccionPR': kg_produccion_pr,
293
+ 'precioPorKgProduccionPR': precio_por_kg_produccion_pr,
294
+ 'kgCrecimientoGB': kg_crecimiento_gb,
295
+ 'precioPorKgCrecimientoGB': precio_por_kg_crecimiento_gb,
296
+ 'kgCrecimientoPR': kg_crecimiento_pr,
297
+ 'precioPorKgCrecimientoPR': precio_por_kg_crecimiento_pr}
298
+ self.solicitud['conceptosBasicosMercadoExterno'] = d
299
+
300
+ @inicializar_y_capturar_excepciones
301
+ def AgregarBonificacionPenalizacion(self, codigo, detalle, resultado=None,
302
+ porcentaje=None, importe=None, **kwargs):
303
+ "Agrega la informaciΓ³n referente a las bonificaciones o penalizaciones"
304
+ ret = dict(codBonificacionPenalizacion=codigo, detalle=detalle,
305
+ resultado=resultado, porcentajeAAplicar=porcentaje,
306
+ importe=importe)
307
+ self.solicitud['bonificacionPenalizacion'].append(ret)
308
+ return True
309
+
310
+ @inicializar_y_capturar_excepciones
311
+ def AgregarOtroImpuesto(self, tipo, base_imponible, alicuota, detalle=None):
312
+ "Agrega la informaciΓ³n referente a otros tributos de la liquidaciΓ³n"
313
+ trib = dict(tipo=tipo, baseImponible=base_imponible, alicuota=alicuota,
314
+ detalle=detalle)
315
+ self.solicitud['otroImpuesto'].append(trib)
316
+ return True
317
+
318
+ @inicializar_y_capturar_excepciones
319
+ def AgregarRemito(self, nro_remito):
320
+ "Agrega la informaciΓ³n referente a los remitos (multiples)"
321
+ self.solicitud['remito'].append(nro_remito)
322
+ return True
323
+
324
+ @inicializar_y_capturar_excepciones
325
+ def AutorizarLiquidacion(self):
326
+ "Generar o ajustar una liquidaciΓ³n ΓΊnica y obtener del CAE"
327
+ # limpio los elementos que no correspondan por estar vacios:
328
+ for campo in ["bonificacionPenalizacion", "otroImpuesto"]:
329
+ if campo in self.solicitud and not self.solicitud[campo]:
330
+ del self.solicitud[campo]
331
+ # llamo al webservice:
332
+ ret = self.client.generarLiquidacion(
333
+ auth={
334
+ 'token': self.Token, 'sign': self.Sign,
335
+ 'cuit': self.Cuit, },
336
+ solicitud=self.solicitud,
337
+ )
338
+ # analizo la respusta
339
+ ret = ret['respuesta']
340
+ self.__analizar_errores(ret)
341
+ liqs = ret.get('liquidacion', [])
342
+ liq = liqs[0] if liqs else None
343
+ self.AnalizarLiquidacion(liq)
344
+ return True
345
+
346
+ def AnalizarLiquidacion(self, liq):
347
+ "MΓ©todo interno para analizar la respuesta de AFIP"
348
+ # proceso los datos bΓ‘sicos de la liquidaciΓ³n (devuelto por consultar):
349
+ if liq:
350
+ cab = liq['encabezado']
351
+ self.CAE = str(cab['cae'])
352
+ self.FechaComprobante = str(cab['fechaComprobante'])
353
+ self.NroComprobante = cab['nroComprobante']
354
+ tot = liq['resumenTotales']
355
+ self.AlicuotaIVA = tot['alicuotaIVA']
356
+ self.TotalNeto = tot['totalNetoLiquidacion']
357
+ self.ImporteIVA = tot['importeIVA']
358
+ self.TotalBonificacionesCalidad = tot['totalBonificacionesCalidad']
359
+ self.TotalPenalizacionesCalidad = tot['totalPenalizacionesCalidad']
360
+ self.TotalBonificacionesComerciales = tot['totalBonificacionesComerciales']
361
+ self.TotalDebitosComerciales = tot['totalDebitosComerciales']
362
+ self.TotalOtrosImpuestos = tot['totalOtrosImpuestos']
363
+ self.Total = tot['totalLiquidacion']
364
+
365
+ # parΓ‘metros de salida:
366
+ self.params_out = dict(
367
+ tipo_cbte=liq['encabezado']['tipoComprobante'],
368
+ pto_vta=liq['encabezado']['puntoVenta'],
369
+ nro_cbte=liq['encabezado']['nroComprobante'],
370
+ fecha=str(liq['encabezado']['fechaComprobante']),
371
+ cae=str(liq['encabezado']['cae']),
372
+ domicilio_comprador=liq['encabezado']['domicilioComprador'],
373
+ tambero=dict(
374
+ cuit=liq['tambero']['cuit'],
375
+ iibb=liq['tambero']['iibb'],
376
+ razon_social=liq['tambero']['razonSocial'],
377
+ situacion_iva=liq['tambero']['situacionIVA'],
378
+ domicilio_fiscal=liq['tambero']['domicilioFiscal'],
379
+ provincia=liq['tambero']['provincia'],
380
+ cod_postal=liq['tambero']['codPostal'],
381
+ ),
382
+ resumen_kg_remitidos=liq['resumenKgRemitidos'],
383
+ alicuota_iva=liq['resumenTotales']['alicuotaIVA'],
384
+ importe_iva=liq['resumenTotales']['importeIVA'],
385
+ total_neto=liq['resumenTotales']['totalNetoLiquidacion'],
386
+ total_bonificaciones_calidad=liq['resumenTotales']['totalBonificacionesCalidad'],
387
+ total_penalizaciones_calidad=liq['resumenTotales']['totalPenalizacionesCalidad'],
388
+ total_bonificaciones_comerciales=liq['resumenTotales']['totalBonificacionesComerciales'],
389
+ total_debitos_comerciales=liq['resumenTotales']['totalDebitosComerciales'],
390
+ total_otros_impuestos=liq['resumenTotales']['totalOtrosImpuestos'],
391
+ total=liq['resumenTotales']['totalLiquidacion'],
392
+ bonificacion_penalizacion=[],
393
+ remitos=[],
394
+ otro_impuesto=[],
395
+ pdf=liq.get('pdf'),
396
+ )
397
+ for ret in liq.get('bonificacionPenalizacion', []):
398
+ self.params_out['bonificacion_penalizacion'].append(dict(
399
+ retencion_codigo=ret['codigo'],
400
+ retencion_importe=ret['importe'],
401
+ ))
402
+ for trib in liq.get('otroImpuesto', []):
403
+ self.params_out['otro_impuesto'].append(dict(
404
+ tributo_descripcion=trib.get('descripcion', ""),
405
+ tributo_base_imponible=trib['baseImponible'],
406
+ tributo_alicuota=trib['alicuota'],
407
+ tributo_codigo=trib['codigo'],
408
+ tributo_importe=trib['importe'],
409
+ ))
410
+ if DEBUG:
411
+ import pprint
412
+ pprint.pprint(self.params_out)
413
+ self.params_out['errores'] = self.errores
414
+
415
+ @inicializar_y_capturar_excepciones
416
+ def AgregarAjuste(self, cai, tipo_cbte, pto_vta, nro_cbte, cae_a_ajustar):
417
+ "Agrega comprobante a ajustar"
418
+ ajuste = self.solicitud['liquidacion']['ajuste'] = {}
419
+ if cae_a_ajustar:
420
+ ajuste['caeAAjustar'] = cae_a_ajustar
421
+ if cai:
422
+ cbte = dict(cai=cai, caeAAjustar=cae_a_ajustar,
423
+ tipoComprobante=tipo_cbte, puntoVenta=pto_vta,
424
+ nroComprobante=nro_cbte)
425
+ ajuste['formularioPapel'] = cbte
426
+ return True
427
+
428
+ @inicializar_y_capturar_excepciones
429
+ def ConsultarLiquidacion(self, tipo_cbte=None, pto_vta=None, nro_cbte=None,
430
+ cae=None, cuit_comprador=None, pdf="liq.pdf"):
431
+ "Consulta una liquidaciΓ³n por No de Comprobante o CAE"
432
+ if cae:
433
+ ret = self.client.consultarLiquidacionPorCae(
434
+ auth={
435
+ 'token': self.Token, 'sign': self.Sign,
436
+ 'cuit': self.Cuit, },
437
+ solicitud={
438
+ 'cae': cae,
439
+ 'pdf': pdf and True or False,
440
+ },
441
+ )
442
+ else:
443
+ ret = self.client.consultarLiquidacionPorNroComprobante(
444
+ auth={
445
+ 'token': self.Token, 'sign': self.Sign,
446
+ 'cuit': self.Cuit, },
447
+ solicitud={
448
+ 'cuitComprador': cuit_comprador,
449
+ 'puntoVenta': pto_vta,
450
+ 'nroComprobante': nro_cbte,
451
+ 'tipoComprobante': tipo_cbte,
452
+ 'pdf': pdf and True or False,
453
+ },
454
+ )
455
+ ret = ret['respuesta']
456
+ self.__analizar_errores(ret)
457
+ if 'liquidacion' in ret:
458
+ liqs = ret.get('liquidacion', [])
459
+ liq = liqs[0] if liqs else None
460
+ self.AnalizarLiquidacion(liq)
461
+ # guardo el PDF si se indico archivo y vino en la respuesta:
462
+ if pdf and 'pdf' in liq:
463
+ open(pdf, "wb").write(liq['pdf'])
464
+ return True
465
+
466
+ @inicializar_y_capturar_excepciones
467
+ def ConsultarUltimoComprobante(self, tipo_cbte=151, pto_vta=1):
468
+ "Consulta el ΓΊltimo No de Comprobante registrado"
469
+ ret = self.client.consultarUltimoNroComprobantePorPtoVta(
470
+ auth={
471
+ 'token': self.Token, 'sign': self.Sign,
472
+ 'cuit': self.Cuit, },
473
+ solicitud={
474
+ 'puntoVenta': pto_vta,
475
+ 'tipoComprobante': tipo_cbte},
476
+ )
477
+ ret = ret['respuesta']
478
+ self.__analizar_errores(ret)
479
+ self.NroComprobante = ret['nroComprobante']
480
+ return True
481
+
482
+ def ConsultarProvincias(self, sep="||"):
483
+ "Consulta las provincias habilitadas"
484
+ ret = self.client.consultarProvincias(
485
+ auth={
486
+ 'token': self.Token, 'sign': self.Sign,
487
+ 'cuit': self.Cuit, },
488
+ )['respuesta']
489
+ self.__analizar_errores(ret)
490
+ array = ret.get('provincia', [])
491
+ if sep is None:
492
+ return dict([(it['codigo'], it['descripcion']) for it in array])
493
+ else:
494
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
495
+ (it['codigo'], it['descripcion']) for it in array]
496
+
497
+ def ConsultarLocalidades(self, cod_provincia, sep="||"):
498
+ "Consulta las localidades habilitadas"
499
+ ret = self.client.consultarLocalidadesPorProvincia(
500
+ auth={
501
+ 'token': self.Token, 'sign': self.Sign,
502
+ 'cuit': self.Cuit, },
503
+ solicitud={'codProvincia': cod_provincia},
504
+ )['respuesta']
505
+ self.__analizar_errores(ret)
506
+ array = ret.get('localidad', [])
507
+ if sep is None:
508
+ return dict([(it['codigo'], it['descripcion']) for it in array])
509
+ else:
510
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
511
+ (it['codigo'], it['descripcion']) for it in array]
512
+
513
+ def ConsultarCondicionesVenta(self, sep="||"):
514
+ "Retorna un listado de cΓ³digos y descripciones de las condiciones de ventas"
515
+ ret = self.client.consultarCondicionesVenta(
516
+ auth={
517
+ 'token': self.Token, 'sign': self.Sign,
518
+ 'cuit': self.Cuit, },
519
+ )['respuesta']
520
+ self.__analizar_errores(ret)
521
+ array = ret.get('condicionVenta', [])
522
+ if sep is None:
523
+ return dict([(it['codigo'], it['descripcion']) for it in array])
524
+ else:
525
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
526
+ (it['codigo'], it['descripcion']) for it in array]
527
+
528
+ def ConsultarOtrosImpuestos(self, sep="||"):
529
+ "Retorna un listado de tributos con cΓ³digo, descripciΓ³n y signo."
530
+ ret = self.client.consultarOtrosImpuestos(
531
+ auth={
532
+ 'token': self.Token, 'sign': self.Sign,
533
+ 'cuit': self.Cuit, },
534
+ )['respuesta']
535
+ self.__analizar_errores(ret)
536
+ array = ret.get('otroImpuesto', [])
537
+ if sep is None:
538
+ return dict([(it['codigo'], it['descripcion']) for it in array])
539
+ else:
540
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
541
+ (it['codigo'], it['descripcion']) for it in array]
542
+
543
+ def ConsultarBonificacionesPenalizaciones(self, sep="||"):
544
+ "Retorna un listado de bonificaciones/penalizaciones con cΓ³digo y descripciΓ³n"
545
+ ret = self.client.consultarBonificacionesPenalizaciones(
546
+ auth={
547
+ 'token': self.Token, 'sign': self.Sign,
548
+ 'cuit': self.Cuit, },
549
+ )['respuesta']
550
+ self.__analizar_errores(ret)
551
+ self.XmlResponse = self.client.xml_response
552
+ array = ret.get('tipo', [])
553
+ if sep is None:
554
+ # sin separador, devuelve un diccionario con clave cod_variadedad
555
+ # y valor: {"descripcion": ds_variedad, "clases": lista_clases}
556
+ # siendo lista_clases = [{'codigo': ..., 'descripcion': ...}]
557
+ return dict([(it['codigo'], {'descripcion': it['descripcion'],
558
+ 'subtipo': it['subtipo']})
559
+ for it in array])
560
+ else:
561
+ # con separador, devuelve una lista de strings:
562
+ # || cod.variedad || desc.variedad || desc.clase || cod.clase ||
563
+ ret = []
564
+ for it in array:
565
+ for subtipo in it['subtipo']:
566
+ ret.append(
567
+ ("%s %%s %s %%s %s %%s %s %%s %s %%s %s %%s %s" %
568
+ (sep, sep, sep, sep, sep, sep, sep)) %
569
+ (it['codigo'], it['descripcion'],
570
+ subtipo['descripcion'], subtipo['codigo'],
571
+ subtipo['valor'], subtipo['signo'])
572
+ )
573
+ return ret
574
+
575
+ def ConsultarPuntosVentas(self, sep="||"):
576
+ "Retorna los puntos de ventas autorizados para la utilizacion de WS"
577
+ ret = self.client.consultarPuntosVenta(
578
+ auth={
579
+ 'token': self.Token, 'sign': self.Sign,
580
+ 'cuit': self.Cuit, },
581
+ )['respuesta']
582
+ self.__analizar_errores(ret)
583
+ array = ret.get('puntoVenta', [])
584
+ if sep is None:
585
+ return dict([(it['codigo'], it['descripcion']) for it in array])
586
+ else:
587
+ return [("%s %%s %s %%s %s" % (sep, sep, sep)) %
588
+ (it['codigo'], it['descripcion']) for it in array]
589
+
590
+ def MostrarPDF(self, archivo, imprimir=False):
591
+ try:
592
+ if sys.platform == "linux2":
593
+ import subprocess
594
+ subprocess.run(["evince", archivo], check=False)
595
+ else:
596
+ operation = imprimir and "print" or ""
597
+ os.startfile(archivo, operation)
598
+ return True
599
+ except Exception as e:
600
+ self.Excepcion = str(e)
601
+ return False
602
+
603
+
604
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
605
+ INSTALL_DIR = WSLUM.InstallDir = get_install_dir()
606
+
607
+
608
+ if __name__ == '__main__':
609
+ if '--ayuda' in sys.argv:
610
+ print(LICENCIA)
611
+ print(AYUDA)
612
+ sys.exit(0)
613
+ if '--formato' in sys.argv:
614
+ print("Formato:")
615
+ for msg, formato in []:
616
+ comienzo = 1
617
+ print("=== %s ===" % msg)
618
+ for fmt in formato:
619
+ clave, longitud, tipo = fmt[0:3]
620
+ dec = len(fmt) > 3 and fmt[3] or (tipo == 'I' and '2' or '')
621
+ print(" * Campo: %-20s PosiciΓ³n: %3d Longitud: %4d Tipo: %s Decimales: %s" % (
622
+ clave, comienzo, longitud, tipo, dec))
623
+ comienzo += longitud
624
+ sys.exit(0)
625
+
626
+ if "--register" in sys.argv or "--unregister" in sys.argv:
627
+ import win32com.server.register
628
+ win32com.server.register.UseCommandLine(WSLUM)
629
+ sys.exit(0)
630
+
631
+ import csv
632
+ from configparser import SafeConfigParser
633
+
634
+ from .wsaa import WSAA
635
+
636
+ try:
637
+
638
+ if "--version" in sys.argv:
639
+ print("VersiΓ³n: ", __version__)
640
+
641
+ if len(sys.argv) > 1 and sys.argv[1].endswith(".ini"):
642
+ CONFIG_FILE = sys.argv[1]
643
+ print("Usando configuracion:", CONFIG_FILE)
644
+
645
+ config = SafeConfigParser()
646
+ config.read(CONFIG_FILE)
647
+ CERT = config.get('WSAA', 'CERT')
648
+ PRIVATEKEY = config.get('WSAA', 'PRIVATEKEY')
649
+ CUIT = config.get('WSLUM', 'CUIT')
650
+ ENTRADA = config.get('WSLUM', 'ENTRADA')
651
+ SALIDA = config.get('WSLUM', 'SALIDA')
652
+
653
+ if config.has_option('WSAA', 'URL') and not HOMO:
654
+ WSAA_URL = config.get('WSAA', 'URL')
655
+ else:
656
+ WSAA_URL = None # wsaa.WSAAURL
657
+ if config.has_option('WSLUM', 'URL') and not HOMO:
658
+ WSLUM_URL = config.get('WSLUM', 'URL')
659
+ else:
660
+ WSLUM_URL = WSDL
661
+
662
+ PROXY = config.has_option('WSAA', 'PROXY') and config.get('WSAA', 'PROXY') or None
663
+ CACERT = config.has_option('WSAA', 'CACERT') and config.get('WSAA', 'CACERT') or None
664
+ WRAPPER = config.has_option('WSAA', 'WRAPPER') and config.get('WSAA', 'WRAPPER') or None
665
+
666
+ if config.has_section('DBF'):
667
+ conf_dbf = dict(config.items('DBF'))
668
+ if DEBUG:
669
+ print("conf_dbf", conf_dbf)
670
+ else:
671
+ conf_dbf = {}
672
+
673
+ DEBUG = '--debug' in sys.argv
674
+ XML = '--xml' in sys.argv
675
+
676
+ if DEBUG:
677
+ print("Usando ConfiguraciΓ³n:")
678
+ print("WSAA_URL:", WSAA_URL)
679
+ print("WSLUM_URL:", WSLUM_URL)
680
+ print("CACERT", CACERT)
681
+ print("WRAPPER", WRAPPER)
682
+ # obteniendo el TA
683
+ from .wsaa import WSAA
684
+ wsaa = WSAA()
685
+ ta = wsaa.Autenticar("wslum", CERT, PRIVATEKEY, wsdl=WSAA_URL,
686
+ proxy=PROXY, wrapper=WRAPPER, cacert=CACERT)
687
+ if not ta:
688
+ pass # sys.exit("Imposible autenticar con WSAA: %s" % wsaa.Excepcion)
689
+
690
+ # cliente soap del web service
691
+ wslum = WSLUM()
692
+ wslum.LanzarExcepciones = True
693
+ wslum.Conectar(url=WSLUM_URL, proxy=PROXY, wrapper=WRAPPER, cacert=CACERT)
694
+ wslum.SetTicketAcceso(ta)
695
+ wslum.Cuit = CUIT
696
+
697
+ if '--dummy' in sys.argv:
698
+ ret = wslum.Dummy()
699
+ print("AppServerStatus", wslum.AppServerStatus)
700
+ print("DbServerStatus", wslum.DbServerStatus)
701
+ print("AuthServerStatus", wslum.AuthServerStatus)
702
+ # sys.exit(0)
703
+
704
+ if '--autorizar' in sys.argv:
705
+
706
+ if '--prueba' in sys.argv:
707
+
708
+ # Solicitud 1: Alta de liquidaciΓ³n
709
+ wslum.CrearLiquidacion(tipo_cbte=27, pto_vta=1, nro_cbte=1,
710
+ fecha="2015-12-31", periodo="2015/12",
711
+ iibb_adquirente="123456789012345",
712
+ domicilio_sede="Domicilio Administrativo",
713
+ inscripcion_registro_publico="Nro IGJ",
714
+ datos_adicionales="Datos Adicionales Varios",
715
+ alicuota_iva=21.00)
716
+ wslum.AgregarCondicionVenta(codigo=1, descripcion=None)
717
+ if '--ajuste' in sys.argv:
718
+ wslum.AgregarAjuste(cai="10000000000000",
719
+ tipo_cbte=0, pto_vta=0, nro_cbte=0,
720
+ cae_a_ajustar="75521002437246")
721
+
722
+ wslum.AgregarTambero(cuit=11111111111, iibb="123456789012345")
723
+
724
+ wslum.AgregarTambo(nro_tambo_interno=123456789,
725
+ nro_renspa="12.345.6.78901/12",
726
+ fecha_venc_cert_tuberculosis="2015-01-01",
727
+ fecha_venc_cert_brucelosis="2015-01-01",
728
+ nro_tambo_provincial=100000000)
729
+ wslum.AgregarUbicacionTambo(
730
+ latitud=-34.62987, longitud=-58.65155,
731
+ domicilio="Domicilio Tambo",
732
+ cod_localidad=10109, cod_provincia=1,
733
+ codigo_postal=1234,
734
+ nombre_partido_depto='Partido Tambo')
735
+
736
+ wslum.AgregarBalanceLitrosPorcentajesSolidos(
737
+ litros_remitidos=11000, litros_decomisados=1000,
738
+ kg_grasa=100.00, kg_proteina=100.00)
739
+
740
+ wslum.AgregarConceptosBasicosMercadoInterno(
741
+ kg_produccion_gb=100, precio_por_kg_produccion_gb=5.00,
742
+ kg_produccion_pr=100, precio_por_kg_produccion_pr=5.00,
743
+ kg_crecimiento_gb=0, precio_por_kg_crecimiento_gb=0.00,
744
+ kg_crecimiento_pr=0, precio_por_kg_crecimiento_pr=0.00)
745
+
746
+ wslum.AgregarConceptosBasicosMercadoExterno(
747
+ kg_produccion_gb=0, precio_por_kg_produccion_gb=0.00,
748
+ kg_produccion_pr=0, precio_por_kg_produccion_pr=0.00,
749
+ kg_crecimiento_gb=0, precio_por_kg_crecimiento_gb=0.00,
750
+ kg_crecimiento_pr=0, precio_por_kg_crecimiento_pr=0.00)
751
+
752
+ wslum.AgregarBonificacionPenalizacion(codigo=1,
753
+ detalle="opcional", resultado="400", porcentaje=10.00)
754
+ wslum.AgregarBonificacionPenalizacion(codigo=10,
755
+ detalle="opcional", resultado="2.5", porcentaje=10.00)
756
+ wslum.AgregarBonificacionPenalizacion(codigo=4,
757
+ detalle="opcional", resultado="400", porcentaje=10.00)
758
+ wslum.AgregarBonificacionPenalizacion(codigo=5,
759
+ detalle="opcional", resultado="En Saneamiento",
760
+ porcentaje=10.00)
761
+
762
+ wslum.AgregarOtroImpuesto(tipo=1, base_imponible=100.00,
763
+ alicuota=10.00, detalle="")
764
+ wslum.AgregarOtroImpuesto(tipo=9, base_imponible=100.00,
765
+ alicuota=10.00,
766
+ detalle="Detalle Otras Percepciones")
767
+ wslum.AgregarOtroImpuesto(tipo=8, base_imponible=100.00,
768
+ alicuota=10.00, detalle="")
769
+
770
+ wslum.AgregarRemito(nro_remito="123456789012")
771
+ wslum.AgregarRemito(nro_remito="123456789")
772
+
773
+ else:
774
+ # cargar un archivo de texto:
775
+ with open("wslum.json", "r") as f:
776
+ wslum.solicitud = json.load(f, encoding="utf-8")
777
+
778
+ if '--testing' in sys.argv:
779
+ # mensaje de prueba (no realiza llamada remota),
780
+ # usar solo si no estΓ‘ operativo, cargo respuesta:
781
+ wslum.LoadTestXML("tests/xml/wslum_liq_test_pdf_response.xml")
782
+ import json
783
+ with open("wslum.json", "w") as f:
784
+ json.dump(wslum.solicitud, f, sort_keys=True, indent=4, encoding="utf-8",)
785
+
786
+ print("Liquidacion: pto_vta=%s nro_cbte=%s tipo_cbte=%s" % (
787
+ wslum.solicitud['liquidacion']['puntoVenta'],
788
+ wslum.solicitud['liquidacion']['nroComprobante'],
789
+ wslum.solicitud['liquidacion']['tipoComprobante'],
790
+ ))
791
+
792
+ if not '--dummy' in sys.argv:
793
+ print("Autorizando...")
794
+ ret = wslum.AutorizarLiquidacion()
795
+
796
+ if wslum.Excepcion:
797
+ print("EXCEPCION:", wslum.Excepcion, file=sys.stderr)
798
+ if DEBUG:
799
+ print(wslum.Traceback, file=sys.stderr)
800
+ print("Errores:", wslum.Errores)
801
+ print("CAE", wslum.CAE)
802
+ print("FechaComprobante", wslum.FechaComprobante)
803
+ print("NroComprobante", wslum.NroComprobante)
804
+ print("TotalNeto", wslum.TotalNeto)
805
+ print("AlicuotaIVA", wslum.AlicuotaIVA)
806
+ print("ImporteIVA", wslum.ImporteIVA)
807
+ print("TotalBonificacionesCalidad", wslum.TotalBonificacionesCalidad)
808
+ print("TotalPenalizacionesCalidad", wslum.TotalPenalizacionesCalidad)
809
+ print("TotalBonificacionesComerciales", wslum.TotalBonificacionesComerciales)
810
+ print("TotalDebitosComerciales", wslum.TotalDebitosComerciales)
811
+ print("TotalOtrosImpuestos", wslum.TotalOtrosImpuestos)
812
+ print("Total", wslum.Total)
813
+
814
+ pdf = wslum.GetParametro("pdf")
815
+ if pdf:
816
+ open("liq.pdf", "wb").write(pdf)
817
+
818
+ if '--testing' in sys.argv:
819
+ assert wslum.CAE == "75521002437246"
820
+
821
+ if DEBUG:
822
+ pprint.pprint(wslum.params_out)
823
+
824
+ if "--guardar" in sys.argv:
825
+ # grabar un archivo de texto (intercambio) con el resultado:
826
+ liq = wslum.params_out.copy()
827
+ if "pdf" in liq:
828
+ del liq["pdf"] # eliminador binario
829
+ with open("wslum_salida.json", "w") as f:
830
+ json.dump(liq, f, default=str,
831
+ indent=2, sort_keys=True, encoding="utf-8")
832
+
833
+ if '--consultar' in sys.argv:
834
+ tipo_cbte = 27
835
+ pto_vta = 1
836
+ nro_cbte = 0
837
+ cuit = None
838
+ try:
839
+ tipo_cbte = sys.argv[sys.argv.index("--consultar") + 1]
840
+ pto_vta = sys.argv[sys.argv.index("--consultar") + 2]
841
+ nro_cbte = sys.argv[sys.argv.index("--consultar") + 3]
842
+ cuit = sys.argv[sys.argv.index("--consultar") + 4]
843
+ except IndexError:
844
+ pass
845
+ if '--testing' in sys.argv:
846
+ # mensaje de prueba (no realiza llamada remota),
847
+ # usar solo si no estΓ‘ operativo, cargo prueba:
848
+ wslum.LoadTestXML("tests/xml/wslum_cons_test.xml")
849
+ print("Consultando: tipo_cbte=%s pto_vta=%s nro_cbte=%s" % (tipo_cbte, pto_vta, nro_cbte))
850
+ ret = wslum.ConsultarLiquidacion(tipo_cbte, pto_vta, nro_cbte,
851
+ cuit_comprador=cuit)
852
+ print("CAE", wslum.CAE)
853
+ print("Errores:", wslum.Errores)
854
+
855
+ if DEBUG:
856
+ pprint.pprint(wslum.params_out)
857
+
858
+ if '--mostrar' in sys.argv and pdf:
859
+ wslum.MostrarPDF(archivo=pdf,
860
+ imprimir='--imprimir' in sys.argv)
861
+
862
+ if '--ult' in sys.argv:
863
+ tipo_cbte = 27
864
+ pto_vta = 1
865
+ try:
866
+ tipo_cbte = sys.argv[sys.argv.index("--ult") + 1]
867
+ pto_vta = sys.argv[sys.argv.index("--ult") + 2]
868
+ except IndexError:
869
+ pass
870
+
871
+ print("Consultando ultimo nro_cbte para pto_vta=%s" % pto_vta, end=' ')
872
+ ret = wslum.ConsultarUltimoComprobante(tipo_cbte, pto_vta)
873
+ if wslum.Excepcion:
874
+ print("EXCEPCION:", wslum.Excepcion, file=sys.stderr)
875
+ if DEBUG:
876
+ print(wslum.Traceback, file=sys.stderr)
877
+ print("Ultimo Nro de Comprobante", wslum.NroComprobante)
878
+ print("Errores:", wslum.Errores)
879
+ sys.exit(0)
880
+
881
+ # Recuperar parΓ‘metros:
882
+
883
+ if '--provincias' in sys.argv:
884
+ ret = wslum.ConsultarProvincias()
885
+ print("\n".join(ret))
886
+
887
+ if '--localidades' in sys.argv:
888
+ try:
889
+ cod_provincia = sys.argv[sys.argv.index("--localidades") + 1]
890
+ except BaseException:
891
+ cod_provincia = input("Codigo Provincia:")
892
+ ret = wslum.ConsultarLocalidades(cod_provincia)
893
+ print("\n".join(ret))
894
+
895
+ if '--bonificaciones_penalizaciones' in sys.argv:
896
+ ret = wslum.ConsultarBonificacionesPenalizaciones()
897
+ print("\n".join(ret))
898
+
899
+ if '--otros_impuestos' in sys.argv:
900
+ ret = wslum.ConsultarOtrosImpuestos()
901
+ print("\n".join(ret))
902
+
903
+ if '--puntosventa' in sys.argv:
904
+ ret = wslum.ConsultarPuntosVentas()
905
+ print("\n".join(ret))
906
+
907
+ print("hecho.")
908
+
909
+ except SoapFault as e:
910
+ print("Falla SOAP:", e.faultcode, e.faultstring.encode("ascii", "ignore"), file=sys.stderr)
911
+ sys.exit(3)
912
+ except Exception as e:
913
+ try:
914
+ print(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0], file=sys.stderr)
915
+ except BaseException:
916
+ print("ExcepciΓ³n no disponible:", type(e), file=sys.stderr)
917
+ if DEBUG:
918
+ raise
919
+ sys.exit(5)
920
+ finally:
921
+ if XML:
922
+ open("wslum_request.xml", "w").write(wslum.client.xml_request)
923
+ open("wslum_response.xml", "w").write(wslum.client.xml_response)
app/pyafipws/wsmtx.py ADDED
@@ -0,0 +1,1243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: latin-1 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ """MοΏ½dulo para obtener cοΏ½digo de autorizaciοΏ½n electrοΏ½nico CAE/CAEA webservice
14
+ WSMTX de AFIP (Factura ElectrοΏ½nica Mercado Interno con codificaciοΏ½n de
15
+ productos) segοΏ½n RG2904 (opciοΏ½n A con detalle) y RG2926/10 (CAE anticipado).
16
+ """
17
+
18
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
19
+ __copyright__ = "Copyright (C) 2010-2015 Mariano Reingart"
20
+ __license__ = "GPL 3.0"
21
+ __version__ = "1.14a"
22
+
23
+ import datetime
24
+ import decimal
25
+ import os
26
+ import sys
27
+ from .utils import verifica, inicializar_y_capturar_excepciones, BaseWS, get_install_dir
28
+
29
+ HOMO = False
30
+ LANZAR_EXCEPCIONES = True
31
+ WSDL = "https://fwshomo.afip.gov.ar/wsmtxca/services/MTXCAService?wsdl"
32
+
33
+
34
+ class WSMTXCA(BaseWS):
35
+ "Interfaz para el WebService de Factura ElectrοΏ½nica Mercado Interno WSMTXCA"
36
+ _public_methods_ = ['CrearFactura', 'EstablecerCampoFactura', 'AgregarIva', 'AgregarItem',
37
+ 'AgregarTributo', 'AgregarCmpAsoc', 'EstablecerCampoItem', 'AgregarOpcional',
38
+ 'AutorizarComprobante', 'CAESolicitar', 'AutorizarAjusteIVA',
39
+ 'SolicitarCAEA', 'ConsultarCAEA', 'ConsultarCAEAEntreFechas',
40
+ 'InformarComprobanteCAEA', 'InformarAjusteIVACAEA',
41
+ 'InformarCAEANoUtilizado', 'InformarCAEANoUtilizadoPtoVta',
42
+ 'ConsultarUltimoComprobanteAutorizado', 'CompUltimoAutorizado',
43
+ 'ConsultarPtosVtaCAEANoInformados',
44
+ 'ConsultarComprobante',
45
+ 'ConsultarTiposComprobante',
46
+ 'ConsultarTiposDocumento',
47
+ 'ConsultarAlicuotasIVA',
48
+ 'ConsultarCondicionesIVA',
49
+ 'ConsultarMonedas',
50
+ 'ConsultarUnidadesMedida',
51
+ 'ConsultarTiposTributo', 'ConsultarTiposDatosAdicionales',
52
+ 'ConsultarCotizacionMoneda',
53
+ 'ConsultarPuntosVentaCAE',
54
+ 'ConsultarPuntosVentaCAEA',
55
+ 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
56
+ 'SetParametros', 'SetTicketAcceso', 'GetParametro',
57
+ 'Dummy', 'Conectar', 'DebugLog', 'SetTicketAcceso']
58
+ _public_attrs_ = ['Token', 'Sign', 'Cuit',
59
+ 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
60
+ 'XmlRequest', 'XmlResponse', 'Version', 'InstallDir', 'LanzarExcepciones',
61
+ 'Resultado', 'Obs', 'Observaciones', 'ErrCode', 'ErrMsg',
62
+ 'EmisionTipo', 'Reproceso', 'Reprocesar', 'Evento',
63
+ 'CAE', 'Vencimiento', 'Evento', 'Errores', 'Traceback', 'Excepcion',
64
+ 'CAEA', 'Periodo', 'Orden', 'FchVigDesde', 'FchVigHasta', 'FchTopeInf', 'FchProceso',
65
+ 'CbteNro', 'FechaCbte', 'PuntoVenta', 'ImpTotal']
66
+
67
+ _reg_progid_ = "WSMTXCA"
68
+ _reg_clsid_ = "{8128E6AB-FB22-4952-8EA6-BD41C29B17CA}"
69
+
70
+ # Variables globales para BaseWS:
71
+ HOMO = HOMO
72
+ WSDL = WSDL
73
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciοΏ½n' or '')
74
+ Reprocesar = True # recuperar automaticamente CAE emitidos
75
+ LanzarExcepciones = LANZAR_EXCEPCIONES
76
+ factura = None
77
+
78
+ def inicializar(self):
79
+ BaseWS.inicializar(self)
80
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
81
+ self.Resultado = self.Motivo = self.Reproceso = ''
82
+ self.LastID = self.LastCMP = self.CAE = self.Vencimiento = ''
83
+ self.CAEA = None
84
+ self.Periodo = self.Orden = ""
85
+ self.FchVigDesde = self.FchVigHasta = ""
86
+ self.FchTopeInf = self.FchProceso = ""
87
+ self.CbteNro = self.FechaCbte = ImpTotal = None
88
+ self.EmisionTipo = self.Evento = ''
89
+ self.Reproceso = '' # no implementado
90
+
91
+ def __analizar_errores(self, ret):
92
+ "Comprueba y extrae errores si existen en la respuesta XML"
93
+ if 'arrayErrores' in ret:
94
+ errores = ret['arrayErrores']
95
+ for error in errores:
96
+ self.Errores.append("%s: %s" % (
97
+ error['codigoDescripcion']['codigo'],
98
+ error['codigoDescripcion']['descripcion'],
99
+ ))
100
+ self.ErrMsg = '\n'.join(self.Errores)
101
+
102
+ def Dummy(self):
103
+ "Obtener el estado de los servidores de la AFIP"
104
+ result = self.client.dummy()
105
+ self.AppServerStatus = result['appserver']
106
+ self.DbServerStatus = result['dbserver']
107
+ self.AuthServerStatus = result['authserver']
108
+ return True
109
+
110
+ def CrearFactura(self, concepto=None, tipo_doc=None, nro_doc=None, tipo_cbte=None, punto_vta=None,
111
+ cbt_desde=None, cbt_hasta=None, imp_total=None, imp_tot_conc=None, imp_neto=None,
112
+ imp_subtotal=None, imp_trib=None, imp_op_ex=None, fecha_cbte=None, fecha_venc_pago=None,
113
+ fecha_serv_desde=None, fecha_serv_hasta=None, # --
114
+ moneda_id=None, moneda_ctz=None, observaciones=None, caea=None, fch_venc_cae=None,
115
+ **kwargs
116
+ ):
117
+ "Creo un objeto factura (interna)"
118
+ # Creo una factura electronica de exportaciοΏ½n
119
+ fact = {'tipo_doc': tipo_doc, 'nro_doc': nro_doc,
120
+ 'tipo_cbte': tipo_cbte, 'punto_vta': punto_vta,
121
+ 'cbt_desde': cbt_desde, 'cbt_hasta': cbt_hasta,
122
+ 'imp_total': imp_total, 'imp_tot_conc': imp_tot_conc,
123
+ 'imp_neto': imp_neto,
124
+ 'imp_subtotal': imp_subtotal, # 'imp_iva': imp_iva,
125
+ 'imp_trib': imp_trib, 'imp_op_ex': imp_op_ex,
126
+ 'fecha_cbte': fecha_cbte,
127
+ 'fecha_venc_pago': fecha_venc_pago,
128
+ 'moneda_id': moneda_id, 'moneda_ctz': moneda_ctz,
129
+ 'concepto': concepto,
130
+ 'observaciones': observaciones,
131
+ 'cbtes_asoc': [],
132
+ 'tributos': [],
133
+ 'iva': [],
134
+ 'detalles': [],
135
+ }
136
+ if fecha_serv_desde:
137
+ fact['fecha_serv_desde'] = fecha_serv_desde
138
+ if fecha_serv_hasta:
139
+ fact['fecha_serv_hasta'] = fecha_serv_hasta
140
+ if caea:
141
+ fact['caea'] = caea
142
+ if fch_venc_cae:
143
+ fact['fch_venc_cae'] = fch_venc_cae
144
+
145
+ self.factura = fact
146
+ return True
147
+
148
+ def EstablecerCampoFactura(self, campo, valor):
149
+ if campo in self.factura or campo in ('fecha_serv_desde', 'fecha_serv_hasta', 'caea', 'fch_venc_cae'):
150
+ self.factura[campo] = valor
151
+ return True
152
+ else:
153
+ return False
154
+
155
+ def AgregarCmpAsoc(self, tipo=1, pto_vta=0, nro=0, cuit=None, fecha=None, **kwargs):
156
+ "Agrego un comprobante asociado a una factura (interna)"
157
+ cmp_asoc = {
158
+ 'tipo': tipo,
159
+ 'pto_vta': pto_vta,
160
+ 'nro': nro}
161
+ if cuit is not None:
162
+ cmp_asoc['cuit'] = cuit
163
+ if fecha is not None:
164
+ cmp_asoc['fecha'] = fecha
165
+ self.factura['cbtes_asoc'].append(cmp_asoc)
166
+ return True
167
+
168
+ def AgregarTributo(self, tributo_id, desc, base_imp, alic, importe, **kwargs):
169
+ "Agrego un tributo a una factura (interna)"
170
+ tributo = {
171
+ 'tributo_id': tributo_id,
172
+ 'desc': desc,
173
+ 'base_imp': base_imp,
174
+ 'importe': importe,
175
+ }
176
+ self.factura['tributos'].append(tributo)
177
+ return True
178
+
179
+ def AgregarIva(self, iva_id, base_imp, importe, **kwargs):
180
+ "Agrego un tributo a una factura (interna)"
181
+ iva = {
182
+ 'iva_id': iva_id,
183
+ 'importe': importe,
184
+ }
185
+ self.factura['iva'].append(iva)
186
+ return True
187
+
188
+ def AgregarItem(self, u_mtx=None, cod_mtx=None, codigo=None, ds=None, qty=None, umed=None, precio=None, bonif=None,
189
+ iva_id=None, imp_iva=None, imp_subtotal=None, **kwargs):
190
+ "Agrego un item a una factura (interna)"
191
+ # ds = unicode(ds, "latin1") # convierto a latin1
192
+ # Nota: no se calcula neto, iva, etc (deben venir calculados!)
193
+ umed = int(umed)
194
+ if umed == 99:
195
+ imp_subtotal = -abs(float(imp_subtotal))
196
+ imp_iva = -abs(float(imp_iva))
197
+ item = {
198
+ 'u_mtx': u_mtx,
199
+ 'cod_mtx': cod_mtx,
200
+ 'codigo': codigo,
201
+ 'ds': ds,
202
+ 'qty': qty if umed != 99 else None,
203
+ 'umed': umed,
204
+ 'precio': precio if umed != 99 else None,
205
+ 'bonif': bonif if umed != 99 else None,
206
+ 'iva_id': iva_id,
207
+ 'imp_iva': imp_iva,
208
+ 'imp_subtotal': imp_subtotal,
209
+ }
210
+ self.factura['detalles'].append(item)
211
+ return True
212
+
213
+ def EstablecerCampoItem(self, campo, valor):
214
+ if self.factura['detalles'] and campo in self.factura['detalles'][-1]:
215
+ self.factura['detalles'][-1][campo] = valor
216
+ return True
217
+ else:
218
+ return False
219
+
220
+ def AgregarOpcional(self, opcional_id=0, valor=None, valor2=None,
221
+ valor3=None, valor4=None, valor5=None,
222
+ valor6=None, **kwarg):
223
+ "Agrego un dato adicional a una factura (interna)"
224
+ op = { 'opcional_id': opcional_id, 'valor': valor, 'valor2': valor2,
225
+ 'valor3': valor3, 'valor4': valor4, 'valor5': valor5,
226
+ 'valor6': valor6 }
227
+ self.factura['opcionales'].append(op)
228
+ return True
229
+
230
+ @inicializar_y_capturar_excepciones
231
+ def AutorizarComprobante(self):
232
+ f = self.factura
233
+ # contruyo la estructura a convertir en XML:
234
+ fact = {
235
+ 'codigoTipoDocumento': f['tipo_doc'], 'numeroDocumento': f['nro_doc'],
236
+ 'codigoTipoComprobante': f['tipo_cbte'], 'numeroPuntoVenta': f['punto_vta'],
237
+ 'numeroComprobante': f['cbt_desde'], 'numeroComprobante': f['cbt_hasta'],
238
+ 'importeTotal': f['imp_total'], 'importeNoGravado': f['imp_tot_conc'],
239
+ 'importeGravado': f['imp_neto'],
240
+ 'importeSubtotal': f['imp_subtotal'], # 'imp_iva': imp_iva,
241
+ 'importeOtrosTributos': f['tributos'] and f['imp_trib'] or None,
242
+ 'importeExento': f['imp_op_ex'],
243
+ 'fechaEmision': f['fecha_cbte'],
244
+ 'codigoMoneda': f['moneda_id'], 'cotizacionMoneda': f['moneda_ctz'],
245
+ 'codigoConcepto': f['concepto'],
246
+ 'observaciones': f['observaciones'],
247
+ 'fechaVencimientoPago': f.get('fecha_venc_pago'),
248
+ 'fechaServicioDesde': f.get('fecha_serv_desde'),
249
+ 'fechaServicioHasta': f.get('fecha_serv_hasta'),
250
+ 'arrayComprobantesAsociados': f['cbtes_asoc'] and [{'comprobanteAsociado': {
251
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
252
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
253
+ 'numeroComprobante': cbte_asoc['nro'],
254
+ 'cuit': cbte_asoc.get('cuit'),
255
+ 'fechaEmision': cbte_asoc.get('fecha'),
256
+ }} for cbte_asoc in f['cbtes_asoc']] or None,
257
+ 'arrayOtrosTributos': f['tributos'] and [ {'otroTributo': {
258
+ 'codigo': tributo['tributo_id'],
259
+ 'descripcion': tributo['desc'],
260
+ 'baseImponible': tributo['base_imp'],
261
+ 'importe': tributo['importe'],
262
+ }} for tributo in f['tributos']] or None,
263
+ 'arraySubtotalesIVA': f['iva'] and [{'subtotalIVA': {
264
+ 'codigo': iva['iva_id'],
265
+ 'importe': iva['importe'],
266
+ }} for iva in f['iva']] or None,
267
+ 'arrayItems': f['detalles'] and [{'item': {
268
+ 'unidadesMtx': it['u_mtx'],
269
+ 'codigoMtx': it['cod_mtx'],
270
+ 'codigo': it['codigo'],
271
+ 'descripcion': it['ds'],
272
+ 'cantidad': it['qty'],
273
+ 'codigoUnidadMedida': it['umed'],
274
+ 'precioUnitario': it['precio'],
275
+ 'importeBonificacion': it['bonif'],
276
+ 'codigoCondicionIVA': it['iva_id'],
277
+ 'importeIVA': it['imp_iva'] if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
278
+ 'importeItem': it['imp_subtotal'],
279
+ }} for it in f['detalles']] or None,
280
+ 'arrayDatosAdicionales': f['opcionales'] and [{'datoAdicional': {
281
+ 't': dato['opcional_id'],
282
+ 'c1': dato.get('valor'),
283
+ 'c2': dato.get('valor2'),
284
+ 'c3': dato.get('valor3'),
285
+ 'c4': dato.get('valor4'),
286
+ 'c5': dato.get('valor5'),
287
+ 'c6': dato.get('valor6'),
288
+ }} for dato in f['opcionales']] or None,
289
+ }
290
+
291
+ ret = self.client.autorizarComprobante(
292
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
293
+ comprobanteCAERequest=fact,
294
+ )
295
+
296
+ # Reprocesar en caso de error (recuperar CAE emitido anteriormente)
297
+ if self.Reprocesar and ('arrayErrores' in ret):
298
+ for error in ret['arrayErrores']:
299
+ err_code = error['codigoDescripcion']['codigo']
300
+ if ret['resultado'] == 'R' and err_code == 102:
301
+ # guardo los mensajes xml originales
302
+ xml_request = self.client.xml_request
303
+ xml_response = self.client.xml_response
304
+ cae = self.ConsultarComprobante(f['tipo_cbte'], f['punto_vta'], f['cbt_desde'], reproceso=True)
305
+ if cae and self.EmisionTipo == 'CAE':
306
+ self.Reproceso = 'S'
307
+ self.Resultado = 'A' # verificar O
308
+ return cae
309
+ self.Reproceso = 'N'
310
+ # reestablesco los mensajes xml originales
311
+ self.client.xml_request = xml_request
312
+ self.client.xml_response = xml_response
313
+
314
+ self.Resultado = ret['resultado'] # u'A'
315
+ if ret['resultado'] in ("A", "O"):
316
+ cbteresp = ret['comprobanteResponse']
317
+ self.FechaCbte = cbteresp['fechaEmision'].strftime("%Y/%m/%d")
318
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
319
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
320
+ # self. = cbteresp['cuit'] # 20267565393L
321
+ # self. = cbteresp['codigoTipoComprobante']
322
+ self.Vencimiento = cbteresp['fechaVencimientoCAE'].strftime("%Y/%m/%d")
323
+ self.CAE = str(cbteresp['CAE']) # 60423794871430L
324
+ self.__analizar_errores(ret)
325
+
326
+ for error in ret.get('arrayObservaciones', []):
327
+ self.Observaciones.append("%(codigo)s: %(descripcion)s" % (
328
+ error['codigoDescripcion']))
329
+ self.Obs = '\n'.join(self.Observaciones)
330
+
331
+ if 'evento' in ret:
332
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
333
+ return self.CAE
334
+
335
+ @inicializar_y_capturar_excepciones
336
+ def CAESolicitar(self):
337
+ try:
338
+ cae = self.AutorizarComprobante() or ''
339
+ self.Excepcion = "OK!"
340
+ except BaseException:
341
+ cae = "ERR"
342
+ finally:
343
+ return cae
344
+
345
+ @inicializar_y_capturar_excepciones
346
+ def AutorizarAjusteIVA(self):
347
+ "EnvοΏ½a la informaciοΏ½n del comprobante de ajuste de IVA que desea autorizar"
348
+ f = self.factura
349
+ # contruyo la estructura a convertir en XML:
350
+ fact = {
351
+ 'codigoTipoDocumento': f['tipo_doc'], 'numeroDocumento': f['nro_doc'],
352
+ 'codigoTipoComprobante': f['tipo_cbte'], 'numeroPuntoVenta': f['punto_vta'],
353
+ 'numeroComprobante': f['cbt_desde'], 'numeroComprobante': f['cbt_hasta'],
354
+ 'importeTotal': f['imp_total'], 'importeNoGravado': f['imp_tot_conc'],
355
+ 'importeGravado': f['imp_neto'],
356
+ 'importeSubtotal': f['imp_subtotal'], # 'imp_iva': imp_iva,
357
+ 'importeOtrosTributos': f['tributos'] and f['imp_trib'] or None,
358
+ 'importeExento': f['imp_op_ex'],
359
+ 'fechaEmision': f['fecha_cbte'],
360
+ 'codigoMoneda': f['moneda_id'], 'cotizacionMoneda': f['moneda_ctz'],
361
+ 'codigoConcepto': f['concepto'],
362
+ 'observaciones': f['observaciones'],
363
+ 'fechaVencimientoPago': f.get('fecha_venc_pago'),
364
+ 'fechaServicioDesde': f.get('fecha_serv_desde'),
365
+ 'fechaServicioHasta': f.get('fecha_serv_hasta'),
366
+ 'arrayComprobantesAsociados': f['cbtes_asoc'] and [{'comprobanteAsociado': {
367
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
368
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
369
+ 'numeroComprobante': cbte_asoc['nro'],
370
+ 'cuit': cbte_asoc.get('cuit'),
371
+ }} for cbte_asoc in f['cbtes_asoc']] or None,
372
+ 'arrayOtrosTributos': f['tributos'] and [{'otroTributo': {
373
+ 'codigo': tributo['tributo_id'],
374
+ 'descripcion': tributo['desc'],
375
+ 'baseImponible': tributo['base_imp'],
376
+ 'importe': tributo['importe'],
377
+ }} for tributo in f['tributos']] or None,
378
+ 'arraySubtotalesIVA': f['iva'] and [{'subtotalIVA': {
379
+ 'codigo': iva['iva_id'],
380
+ 'importe': iva['importe'],
381
+ }} for iva in f['iva']] or None,
382
+ 'arrayItems': f['detalles'] and [{'item': {
383
+ 'unidadesMtx': it['u_mtx'],
384
+ 'codigoMtx': it['cod_mtx'],
385
+ 'codigo': it['codigo'],
386
+ 'descripcion': it['ds'],
387
+ 'cantidad': it['qty'],
388
+ 'codigoUnidadMedida': it['umed'],
389
+ 'precioUnitario': it['precio'],
390
+ 'importeBonificacion': it['bonif'],
391
+ 'codigoCondicionIVA': it['iva_id'],
392
+ 'importeIVA': it['imp_iva'] if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
393
+ 'importeItem': it['imp_subtotal'],
394
+ }} for it in f['detalles']] or None,
395
+ 'arrayDatosAdicionales': f['opcionales'] and [{'datoAdicional': {
396
+ 't': dato['opcional_id'],
397
+ 'c1': dato.get('valor'),
398
+ 'c2': dato.get('valor2'),
399
+ 'c3': dato.get('valor3'),
400
+ 'c4': dato.get('valor4'),
401
+ 'c5': dato.get('valor5'),
402
+ 'c6': dato.get('valor6'),
403
+ }} for dato in f['opcionales']] or None,
404
+ }
405
+
406
+ ret = self.client.autorizarAjusteIVA(
407
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
408
+ comprobanteCAERequest=fact,
409
+ )
410
+
411
+ self.Resultado = ret['resultado'] # u'A'
412
+ if ret['resultado'] in ("A", "O"):
413
+ cbteresp = ret['comprobanteResponse']
414
+ self.FechaCbte = cbteresp['fechaEmision'].strftime("%Y/%m/%d")
415
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
416
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
417
+ # self. = cbteresp['cuit'] # 20267565393L
418
+ # self. = cbteresp['codigoTipoComprobante']
419
+ self.Vencimiento = cbteresp['fechaVencimientoCAE'].strftime("%Y/%m/%d")
420
+ self.CAE = str(cbteresp['CAE']) # 60423794871430L
421
+ self.__analizar_errores(ret)
422
+
423
+ for error in ret.get('arrayObservaciones', []):
424
+ self.Observaciones.append("%(codigo)s: %(descripcion)s" % (
425
+ error['codigoDescripcion']))
426
+ self.Obs = '\n'.join(self.Observaciones)
427
+
428
+ if 'evento' in ret:
429
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
430
+ return self.CAE
431
+
432
+ @inicializar_y_capturar_excepciones
433
+ def SolicitarCAEA(self, periodo, orden):
434
+ "Obtener un CAEA y su respectivo perοΏ½odo de vigencia"
435
+ ret = self.client.solicitarCAEA(
436
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
437
+ solicitudCAEA={
438
+ 'periodo': periodo,
439
+ 'orden': orden},
440
+ )
441
+
442
+ self.__analizar_errores(ret)
443
+
444
+ if 'CAEAResponse' in ret:
445
+ res = ret['CAEAResponse']
446
+ self.CAEA = res['CAEA']
447
+ self.Periodo = res['periodo']
448
+ self.Orden = res['orden']
449
+ self.FchVigDesde = res['fechaDesde']
450
+ self.FchVigHasta = res['fechaHasta']
451
+ self.FchTopeInf = res['fechaTopeInforme']
452
+ self.FchProceso = res['fechaProceso']
453
+ return self.CAEA and str(self.CAEA) or ''
454
+
455
+ @inicializar_y_capturar_excepciones
456
+ def ConsultarCAEA(self, periodo=None, orden=None, caea=None):
457
+ "MοΏ½todo de consulta de CAEA"
458
+ if periodo and orden:
459
+ anio, mes = int(periodo[0:4]), int(periodo[4:6])
460
+ if int(orden) == 1:
461
+ dias = 1, 15
462
+ else:
463
+ if mes in (1, 3, 5, 7, 8, 10, 12):
464
+ dias = 16, 31
465
+ elif mes in (4, 6, 9, 11):
466
+ dias = 16, 30
467
+ else:
468
+ import calendar
469
+ if calendar.isleap(anio):
470
+ dias = 16, 29 # biciesto
471
+ else:
472
+ dias = 16, 28
473
+
474
+ fecha_desde = "%04d-%02d-%02d" % (anio, mes, dias[0])
475
+ fecha_hasta = "%04d-%02d-%02d" % (anio, mes, dias[1])
476
+
477
+ caeas = self.ConsultarCAEAEntreFechas(fecha_desde, fecha_hasta)
478
+ if caeas:
479
+ caea = caeas[0]
480
+
481
+ if caea:
482
+ ret = self.client.consultarCAEA(
483
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
484
+ CAEA=caea,
485
+ )
486
+
487
+ self.__analizar_errores(ret)
488
+
489
+ if 'CAEAResponse' in ret:
490
+ res = ret['CAEAResponse']
491
+ self.CAEA = res['CAEA']
492
+ self.Periodo = res['periodo']
493
+ self.Orden = res['orden']
494
+ self.FchVigDesde = res['fechaDesde']
495
+ self.FchVigHasta = res['fechaHasta']
496
+ self.FchTopeInf = res['fechaTopeInforme']
497
+ self.FchProceso = res['fechaProceso']
498
+ return self.CAEA and str(self.CAEA) or ''
499
+
500
+ @inicializar_y_capturar_excepciones
501
+ def ConsultarCAEAEntreFechas(self, fecha_desde, fecha_hasta):
502
+ "MοΏ½todo de consulta de CAEA"
503
+
504
+ ret = self.client.consultarCAEAEntreFechas(
505
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
506
+ fechaDesde=fecha_desde,
507
+ fechaHasta=fecha_hasta,
508
+ )
509
+
510
+ self.__analizar_errores(ret)
511
+
512
+ caeas = []
513
+ if 'arrayCAEAResponse' in ret:
514
+ return [res['CAEAResponse']['CAEA'] for res in ret['arrayCAEAResponse']]
515
+ return []
516
+
517
+ @inicializar_y_capturar_excepciones
518
+ def InformarComprobanteCAEA(self):
519
+ "EnvοΏ½a la informaciοΏ½n del comprobante emitido y asociado a un CAEA"
520
+ f = self.factura
521
+ # contruyo la estructura a convertir en XML:
522
+ fact = {
523
+ 'codigoTipoDocumento': f['tipo_doc'], 'numeroDocumento': f['nro_doc'],
524
+ 'codigoTipoComprobante': f['tipo_cbte'], 'numeroPuntoVenta': f['punto_vta'],
525
+ 'numeroComprobante': f['cbt_desde'], 'numeroComprobante': f['cbt_hasta'],
526
+ 'codigoTipoAutorizacion': 'A',
527
+ 'codigoAutorizacion': f['caea'],
528
+ 'importeTotal': f['imp_total'], 'importeNoGravado': f['imp_tot_conc'],
529
+ 'importeGravado': f['imp_neto'],
530
+ 'importeSubtotal': f['imp_subtotal'], # 'imp_iva': imp_iva,
531
+ 'importeOtrosTributos': f['tributos'] and f['imp_trib'] or None,
532
+ 'importeExento': f['imp_op_ex'],
533
+ 'fechaEmision': f['fecha_cbte'],
534
+ 'codigoMoneda': f['moneda_id'], 'cotizacionMoneda': f['moneda_ctz'],
535
+ 'codigoConcepto': f['concepto'],
536
+ 'observaciones': f['observaciones'],
537
+ 'fechaVencimientoPago': f.get('fecha_venc_pago'),
538
+ 'fechaServicioDesde': f.get('fecha_serv_desde'),
539
+ 'fechaServicioHasta': f.get('fecha_serv_hasta'),
540
+ 'arrayComprobantesAsociados': f['cbtes_asoc'] and [{'comprobanteAsociado': {
541
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
542
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
543
+ 'numeroComprobante': cbte_asoc['nro'],
544
+ 'cuit': cbte_asoc.get('cuit'),
545
+ }} for cbte_asoc in f['cbtes_asoc']] or None,
546
+ 'arrayOtrosTributos': f['tributos'] and [{'otroTributo': {
547
+ 'codigo': tributo['tributo_id'],
548
+ 'descripcion': tributo['desc'],
549
+ 'baseImponible': tributo['base_imp'],
550
+ 'importe': tributo['importe'],
551
+ }} for tributo in f['tributos']] or None,
552
+ 'arraySubtotalesIVA': f['iva'] and [{'subtotalIVA': {
553
+ 'codigo': iva['iva_id'],
554
+ 'importe': iva['importe'],
555
+ }} for iva in f['iva']] or None,
556
+ 'arrayItems': f['detalles'] and [{'item': {
557
+ 'unidadesMtx': it['u_mtx'],
558
+ 'codigoMtx': it['cod_mtx'],
559
+ 'codigo': it['codigo'],
560
+ 'descripcion': it['ds'],
561
+ 'cantidad': it['qty'],
562
+ 'codigoUnidadMedida': it['umed'],
563
+ 'precioUnitario': it['precio'],
564
+ 'importeBonificacion': it['bonif'],
565
+ 'codigoCondicionIVA': it['iva_id'],
566
+ 'importeIVA': it['imp_iva'] if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
567
+ 'importeItem': it['imp_subtotal'],
568
+ }} for it in f['detalles']] or None,
569
+ }
570
+
571
+ # fecha de vencimiento opcional (igual al οΏ½ltimo dοΏ½a de vigencia del CAEA)
572
+ if 'fch_venc_cae' in f:
573
+ fact['fechaVencimiento'] = f['fch_venc_cae']
574
+
575
+ ret = self.client.informarComprobanteCAEA(
576
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
577
+ comprobanteCAEARequest=fact,
578
+ )
579
+
580
+ # Reprocesar en caso de error (verifica datos informado anteriormente)
581
+ if self.Reprocesar and ('arrayErrores' in ret):
582
+ for error in ret['arrayErrores']:
583
+ err_code = error['codigoDescripcion']['codigo']
584
+ if ret['resultado'] == 'R' and err_code == 703:
585
+ # guardo los mensajes xml originales
586
+ xml_request = self.client.xml_request
587
+ xml_response = self.client.xml_response
588
+ cae = self.ConsultarComprobante(f['tipo_cbte'], f['punto_vta'], f['cbt_desde'], reproceso=True)
589
+ if cae == f['caea'] and self.EmisionTipo == 'CAEA':
590
+ self.Reproceso = 'S'
591
+ self.Resultado = 'A' # verificar O
592
+ return cae
593
+ self.Reproceso = 'N'
594
+ # reestablesco los mensajes xml originales
595
+ self.client.xml_request = xml_request
596
+ self.client.xml_response = xml_response
597
+
598
+ self.Resultado = ret['resultado'] # u'A'
599
+ self.Errores = []
600
+ if ret['resultado'] in ("A", "O"):
601
+ cbteresp = ret['comprobanteCAEAResponse']
602
+ self.FchProceso = ret['fechaProceso'].strftime("%Y-%m-%d")
603
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
604
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
605
+ if 'fechaVencimientoCAE' in cbteresp:
606
+ self.Vencimiento = cbteresp['fechaVencimientoCAE'].strftime("%Y-%m-%d")
607
+ else:
608
+ self.Vencimiento = ""
609
+ self.CAEA = str(cbteresp['CAEA']) # 60423794871430L
610
+ self.EmisionTipo = 'CAEA'
611
+ self.__analizar_errores(ret)
612
+
613
+ for error in ret.get('arrayObservaciones', []):
614
+ self.Observaciones.append("%(codigo)s: %(descripcion)s" % (
615
+ error['codigoDescripcion']))
616
+ self.Obs = '\n'.join(self.Observaciones)
617
+
618
+ if 'evento' in ret:
619
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
620
+ return self.CAEA
621
+
622
+ @inicializar_y_capturar_excepciones
623
+ def InformarAjusteIVACAEA(self):
624
+ "EnvοΏ½a la informaciοΏ½n del comprobante de ajuste de IVA emitidos"
625
+ f = self.factura
626
+ # contruyo la estructura a convertir en XML:
627
+ fact = {
628
+ 'codigoTipoDocumento': f['tipo_doc'], 'numeroDocumento': f['nro_doc'],
629
+ 'codigoTipoComprobante': f['tipo_cbte'], 'numeroPuntoVenta': f['punto_vta'],
630
+ 'numeroComprobante': f['cbt_desde'], 'numeroComprobante': f['cbt_hasta'],
631
+ 'codigoTipoAutorizacion': 'A',
632
+ 'codigoAutorizacion': f['caea'],
633
+ 'importeTotal': f['imp_total'], 'importeNoGravado': f['imp_tot_conc'],
634
+ 'importeGravado': f['imp_neto'],
635
+ 'importeSubtotal': f['imp_subtotal'], # 'imp_iva': imp_iva,
636
+ 'importeOtrosTributos': f['tributos'] and f['imp_trib'] or None,
637
+ 'importeExento': f['imp_op_ex'],
638
+ 'fechaEmision': f['fecha_cbte'],
639
+ 'codigoMoneda': f['moneda_id'], 'cotizacionMoneda': f['moneda_ctz'],
640
+ 'codigoConcepto': f['concepto'],
641
+ 'observaciones': f['observaciones'],
642
+ 'fechaVencimientoPago': f.get('fecha_venc_pago'),
643
+ 'fechaServicioDesde': f.get('fecha_serv_desde'),
644
+ 'fechaServicioHasta': f.get('fecha_serv_hasta'),
645
+ 'arrayComprobantesAsociados': f['cbtes_asoc'] and [{'comprobanteAsociado': {
646
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
647
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
648
+ 'numeroComprobante': cbte_asoc['nro'],
649
+ }} for cbte_asoc in f['cbtes_asoc']] or None,
650
+ 'arrayOtrosTributos': f['tributos'] and [{'otroTributo': {
651
+ 'codigo': tributo['tributo_id'],
652
+ 'descripcion': tributo['desc'],
653
+ 'baseImponible': tributo['base_imp'],
654
+ 'importe': tributo['importe'],
655
+ }} for tributo in f['tributos']] or None,
656
+ 'arraySubtotalesIVA': f['iva'] and [{'subtotalIVA': {
657
+ 'codigo': iva['iva_id'],
658
+ 'importe': iva['importe'],
659
+ }} for iva in f['iva']] or None,
660
+ 'arrayItems': f['detalles'] and [{'item': {
661
+ 'unidadesMtx': it['u_mtx'],
662
+ 'codigoMtx': it['cod_mtx'],
663
+ 'codigo': it['codigo'],
664
+ 'descripcion': it['ds'],
665
+ 'cantidad': it['qty'],
666
+ 'codigoUnidadMedida': it['umed'],
667
+ 'precioUnitario': it['precio'],
668
+ 'importeBonificacion': it['bonif'],
669
+ 'codigoCondicionIVA': it['iva_id'],
670
+ 'importeIVA': it['imp_iva'] if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
671
+ 'importeItem': it['imp_subtotal'],
672
+ }} for it in f['detalles']] or None,
673
+ }
674
+
675
+ # fecha de vencimiento opcional (igual al οΏ½ltimo dοΏ½a de vigencia del CAEA)
676
+ if 'fch_venc_cae' in f:
677
+ fact['fechaVencimiento'] = f['fch_venc_cae']
678
+
679
+ ret = self.client.informarAjusteIVACAEA(
680
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
681
+ comprobanteCAEARequest=fact,
682
+ )
683
+
684
+ self.Resultado = ret['resultado'] # u'A'
685
+ if ret['resultado'] in ("A", "O"):
686
+ cbteresp = ret['comprobanteCAEAResponse']
687
+ self.FchProceso = ret['fechaProceso'].strftime("%Y-%m-%d")
688
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
689
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
690
+ # self. = cbteresp['cuit'] # 20267565393L
691
+ # self. = cbteresp['codigoTipoComprobante']
692
+ if 'fechaVencimientoCAE' in cbteresp:
693
+ self.Vencimiento = cbteresp['fechaVencimientoCAE'].strftime("%Y-%m-%d")
694
+ else:
695
+ self.Vencimiento = ""
696
+ self.CAEA = str(cbteresp['CAEA']) # 60423794871430L
697
+ self.EmisionTipo = 'CAEA'
698
+ self.__analizar_errores(ret)
699
+
700
+ for error in ret.get('arrayObservaciones', []):
701
+ self.Observaciones.append("%(codigo)s: %(descripcion)s" % (
702
+ error['codigoDescripcion']))
703
+ self.Obs = '\n'.join(self.Observaciones)
704
+
705
+ if 'evento' in ret:
706
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
707
+ return self.CAE
708
+
709
+ @inicializar_y_capturar_excepciones
710
+ def InformarCAEANoUtilizado(self, caea):
711
+ ret = self.client.informarCAEANoUtilizado(
712
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
713
+ CAEA=caea,
714
+ )
715
+ self.Resultado = ret['resultado'] # u'A'
716
+ if ret['resultado'] in ("A", "O"):
717
+ self.FchProceso = ret['fechaProceso'].strftime("%Y-%m-%d")
718
+ self.CAEA = str(ret['CAEA']) # 60423794871430L
719
+ self.EmisionTipo = 'CAEA'
720
+ self.__analizar_errores(ret)
721
+ if 'evento' in ret:
722
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
723
+ return self.Resultado
724
+
725
+ @inicializar_y_capturar_excepciones
726
+ def InformarCAEANoUtilizadoPtoVta(self, caea, punto_vta):
727
+ ret = self.client.informarCAEANoUtilizadoPtoVta(
728
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
729
+ CAEA=caea,
730
+ numeroPuntoVenta=punto_vta,
731
+ )
732
+ self.Resultado = ret['resultado'] # u'A'
733
+ if ret['resultado'] in ("A", "O"):
734
+ self.FchProceso = ret['fechaProceso'].strftime("%Y-%m-%d")
735
+ self.CAEA = str(ret['CAEA']) # 60423794871430L
736
+ self.EmisionTipo = 'CAEA'
737
+ self.PuntoVenta = ret['numeroPuntoVenta'] # 4000
738
+ self.__analizar_errores(ret)
739
+ if 'evento' in ret:
740
+ self.Evento = '%(codigo)s: %(descripcion)s' % ret['evento']
741
+ return self.Resultado
742
+
743
+ @inicializar_y_capturar_excepciones
744
+ def ConsultarUltimoComprobanteAutorizado(self, tipo_cbte, punto_vta):
745
+ ret = self.client.consultarUltimoComprobanteAutorizado(
746
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
747
+ consultaUltimoComprobanteAutorizadoRequest={
748
+ 'codigoTipoComprobante': tipo_cbte,
749
+ 'numeroPuntoVenta': punto_vta},
750
+ )
751
+ nro = ret.get('numeroComprobante')
752
+ self.__analizar_errores(ret)
753
+ self.CbteNro = nro
754
+ return nro is not None and str(nro) or 0
755
+
756
+ CompUltimoAutorizado = ConsultarUltimoComprobanteAutorizado
757
+
758
+ @inicializar_y_capturar_excepciones
759
+ def ConsultarComprobante(self, tipo_cbte, punto_vta, cbte_nro, reproceso=False):
760
+ "Recuperar los datos completos de un comprobante ya autorizado"
761
+ ret = self.client.consultarComprobante(
762
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
763
+ consultaComprobanteRequest={
764
+ 'codigoTipoComprobante': tipo_cbte,
765
+ 'numeroPuntoVenta': punto_vta,
766
+ 'numeroComprobante': cbte_nro,
767
+ },
768
+ )
769
+ # diferencias si hay reproceso:
770
+ difs = []
771
+ # analizo el resultado:
772
+ if 'comprobante' in ret:
773
+ cbteresp = ret['comprobante']
774
+ if reproceso:
775
+ # verifico los campos registrados coincidan con los enviados:
776
+ f = self.factura
777
+ verificaciones = {
778
+ 'codigoTipoComprobante': f['tipo_cbte'],
779
+ 'numeroPuntoVenta': f['punto_vta'],
780
+ 'codigoConcepto': f['concepto'],
781
+ 'codigoTipoDocumento': f['tipo_doc'],
782
+ 'numeroDocumento': f['nro_doc'],
783
+ 'numeroComprobante': f['cbt_desde'],
784
+ 'numeroComprobante': f['cbt_hasta'],
785
+ 'fechaEmision': f['fecha_cbte'],
786
+ 'importeTotal': decimal.Decimal(str(f['imp_total'])),
787
+ 'importeNoGravado': decimal.Decimal(str(f['imp_tot_conc'])),
788
+ 'importeGravado': decimal.Decimal(str(f['imp_neto'])),
789
+ 'importeExento': decimal.Decimal(str(f['imp_op_ex'])),
790
+ 'importeOtrosTributos': f['tributos'] and decimal.Decimal(str(f['imp_trib'])) or None,
791
+ 'importeSubtotal': f['imp_subtotal'],
792
+ 'fechaServicioDesde': f.get('fecha_serv_desde'),
793
+ 'fechaServicioHasta': f.get('fecha_serv_hasta'),
794
+ 'fechaVencimientoPago': f.get('fecha_venc_pago'),
795
+ 'codigoMoneda': f['moneda_id'],
796
+ 'cotizacionMoneda': str(decimal.Decimal(str(f['moneda_ctz']))),
797
+ 'arrayItems': [
798
+ {'item': {
799
+ 'unidadesMtx': it['u_mtx'],
800
+ 'codigoMtx': it['cod_mtx'],
801
+ 'codigo': it['codigo'],
802
+ 'descripcion': it['ds'],
803
+ 'cantidad': it['qty'] and decimal.Decimal(str(it['qty'])),
804
+ 'codigoUnidadMedida': it['umed'],
805
+ 'precioUnitario': it['precio'] is not None and decimal.Decimal(str(it['precio'])) or None,
806
+ # 'importeBonificacion': it['bonif'],
807
+ 'codigoCondicionIVA': decimal.Decimal(str(it['iva_id'])),
808
+ 'importeIVA': decimal.Decimal(str(it['imp_iva'])) if int(f['tipo_cbte']) not in (6, 7, 8) and it['imp_iva'] is not None else None,
809
+ 'importeItem': decimal.Decimal(str(it['imp_subtotal'])),
810
+ }}
811
+ for it in f['detalles']],
812
+ 'arrayComprobantesAsociados': [
813
+ {'comprobanteAsociado': {
814
+ 'codigoTipoComprobante': cbte_asoc['tipo'],
815
+ 'numeroPuntoVenta': cbte_asoc['pto_vta'],
816
+ 'numeroComprobante': cbte_asoc['nro']}}
817
+ for cbte_asoc in f['cbtes_asoc']],
818
+ 'arrayOtrosTributos': [
819
+ {'otroTributo': {
820
+ 'codigo': tributo['tributo_id'],
821
+ 'descripcion': tributo['desc'],
822
+ 'baseImponible': decimal.Decimal(str(tributo['base_imp'])),
823
+ 'importe': decimal.Decimal(str(tributo['importe'])),
824
+ }}
825
+ for tributo in f['tributos']],
826
+ 'arraySubtotalesIVA': [
827
+ {'subtotalIVA': {
828
+ 'codigo': iva['iva_id'],
829
+ 'importe': decimal.Decimal(str(iva['importe'])),
830
+ }}
831
+ for iva in f['iva']],
832
+ }
833
+ verifica(verificaciones, cbteresp, difs)
834
+ if difs:
835
+ print("Diferencias:", difs)
836
+ self.log("Diferencias: %s" % difs)
837
+ self.FechaCbte = cbteresp['fechaEmision'].strftime("%Y/%m/%d")
838
+ self.CbteNro = cbteresp['numeroComprobante'] # 1L
839
+ self.PuntoVenta = cbteresp['numeroPuntoVenta'] # 4000
840
+ self.Vencimiento = cbteresp['fechaVencimiento'].strftime("%Y/%m/%d")
841
+ self.ImpTotal = str(cbteresp['importeTotal'])
842
+ self.CAE = str(cbteresp['codigoAutorizacion']) # 60423794871430L
843
+ self.EmisionTipo = cbteresp['codigoTipoAutorizacion'] == 'A' and 'CAEA' or 'CAE'
844
+ self.__analizar_errores(ret)
845
+ if not difs:
846
+ return self.CAE
847
+
848
+ @inicializar_y_capturar_excepciones
849
+ def ConsultarTiposComprobante(self):
850
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
851
+ ret = self.client.consultarTiposComprobante(
852
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
853
+ )
854
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
855
+ for p in ret['arrayTiposComprobante']]
856
+
857
+ @inicializar_y_capturar_excepciones
858
+ def ConsultarTiposDocumento(self):
859
+ ret = self.client.consultarTiposDocumento(
860
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
861
+ )
862
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
863
+ for p in ret['arrayTiposDocumento']]
864
+
865
+ @inicializar_y_capturar_excepciones
866
+ def ConsultarAlicuotasIVA(self):
867
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
868
+ ret = self.client.consultarAlicuotasIVA(
869
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
870
+ )
871
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
872
+ for p in ret['arrayAlicuotasIVA']]
873
+
874
+ @inicializar_y_capturar_excepciones
875
+ def ConsultarCondicionesIVA(self):
876
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
877
+ ret = self.client.consultarCondicionesIVA(
878
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
879
+ )
880
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
881
+ for p in ret['arrayCondicionesIVA']]
882
+
883
+ @inicializar_y_capturar_excepciones
884
+ def ConsultarMonedas(self):
885
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
886
+ ret = self.client.consultarMonedas(
887
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
888
+ )
889
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
890
+ for p in ret['arrayMonedas']]
891
+
892
+ @inicializar_y_capturar_excepciones
893
+ def ConsultarUnidadesMedida(self):
894
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
895
+ ret = self.client.consultarUnidadesMedida(
896
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
897
+ )
898
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
899
+ for p in ret['arrayUnidadesMedida']]
900
+
901
+ @inicializar_y_capturar_excepciones
902
+ def ConsultarTiposTributo(self):
903
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
904
+ ret = self.client.consultarTiposTributo(
905
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
906
+ )
907
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
908
+ for p in ret['arrayTiposTributo']]
909
+
910
+ @inicializar_y_capturar_excepciones
911
+ def ConsultarTiposDatosAdicionales(self):
912
+ "Este mοΏ½todo permite consultar los tipos de datos adicionales."
913
+ ret = self.client.consultarTiposDatosAdicionales(
914
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
915
+ )
916
+ return ["%(codigo)s: %(descripcion)s" % p['codigoDescripcion']
917
+ for p in ret['arrayTiposDatosAdicionales']]
918
+
919
+ @inicializar_y_capturar_excepciones
920
+ def ConsultarCotizacionMoneda(self, moneda_id):
921
+ "Este mοΏ½todo permite consultar los tipos de comprobantes habilitados en este WS"
922
+ ret = self.client.consultarCotizacionMoneda(
923
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
924
+ codigoMoneda=moneda_id,
925
+ )
926
+ self.__analizar_errores(ret)
927
+ if 'cotizacionMoneda' in ret:
928
+ return str(ret['cotizacionMoneda'])
929
+
930
+ @inicializar_y_capturar_excepciones
931
+ def ConsultarPuntosVentaCAE(self, fmt="%(numeroPuntoVenta)s: bloqueado=%(bloqueado)s baja=%(fechaBaja)s"):
932
+ "Este mοΏ½todo permite consultar los puntos de venta habilitados para CAE en este WS"
933
+ res = self.client.consultarPuntosVentaCAE(
934
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
935
+ )
936
+ ret = []
937
+ for p in res['arrayPuntosVenta']:
938
+ p = p['puntoVenta']
939
+ if 'fechaBaja' not in p:
940
+ p['fechaBaja'] = ""
941
+ ret.append(fmt % p if fmt else p)
942
+ return ret
943
+
944
+ @inicializar_y_capturar_excepciones
945
+ def ConsultarPuntosVentaCAEA(self, fmt="%(numeroPuntoVenta)s: bloqueado=%(bloqueado)s baja=%(fechaBaja)s"):
946
+ "Este mοΏ½todo permite consultar los puntos de venta habilitados para CAEA en este WS"
947
+ res = self.client.consultarPuntosVentaCAEA(
948
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
949
+ )
950
+ ret = []
951
+ for p in res['arrayPuntosVenta']:
952
+ p = p['puntoVenta']
953
+ if 'fechaBaja' not in p:
954
+ p['fechaBaja'] = ""
955
+ ret.append(fmt % p if fmt else p)
956
+ return ret
957
+
958
+ @inicializar_y_capturar_excepciones
959
+ def ConsultarPtosVtaCAEANoInformados(self, caea):
960
+ "Este mοΏ½todo permite consultar que puntos de venta aοΏ½n no fueron informados para un CAEA determinado."
961
+ ret = self.client.consultarPtosVtaCAEANoInformados(
962
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
963
+ CAEA=caea,
964
+ )
965
+ return [" ".join([("%s=%s" % (k, v)) for k, v in list(p['puntoVenta'].items())])
966
+ for p in ret['arrayPuntosVenta']]
967
+
968
+
969
+ def main():
970
+ "FunciοΏ½n principal de pruebas (obtener CAE)"
971
+ import os
972
+ import time
973
+
974
+ DEBUG = '--debug' in sys.argv
975
+
976
+ # obteniendo el TA para pruebas
977
+ from .wsaa import WSAA
978
+ ta = WSAA().Autenticar("wsmtxca", "reingart.crt", "reingart.key")
979
+
980
+ wsmtxca = WSMTXCA()
981
+ wsmtxca.SetTicketAcceso(ta)
982
+ wsmtxca.Cuit = "20267565393"
983
+
984
+ cache = ""
985
+ if "--prod" in sys.argv:
986
+ wsdl = "https://serviciosjava.afip.gob.ar/wsmtxca/services/MTXCAService?wsdl"
987
+ else:
988
+ wsdl = WSDL
989
+ wsmtxca.Conectar(cache, wsdl, cacert="conf/afip_ca_info.crt")
990
+
991
+ if "--dummy" in sys.argv:
992
+ print(wsmtxca.client.help("dummy"))
993
+ wsmtxca.Dummy()
994
+ print("AppServerStatus", wsmtxca.AppServerStatus)
995
+ print("DbServerStatus", wsmtxca.DbServerStatus)
996
+ print("AuthServerStatus", wsmtxca.AuthServerStatus)
997
+
998
+ if "--prueba" in sys.argv:
999
+ # print wsmtxca.client.help("autorizarComprobante").encode("latin1")
1000
+ try:
1001
+ tipo_cbte = 1
1002
+ punto_vta = 4000
1003
+ cbte_nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
1004
+ fecha = datetime.datetime.now().strftime("%Y-%m-%d")
1005
+ concepto = 3
1006
+ tipo_doc = 80
1007
+ nro_doc = "30000000007"
1008
+ cbte_nro = int(cbte_nro) + 1
1009
+ cbt_desde = cbte_nro
1010
+ cbt_hasta = cbt_desde
1011
+ imp_total = "122.00"
1012
+ imp_tot_conc = "0.00"
1013
+ imp_neto = "100.00"
1014
+ imp_trib = "1.00"
1015
+ imp_op_ex = "0.00"
1016
+ imp_subtotal = "100.00"
1017
+ fecha_cbte = fecha
1018
+ fecha_venc_pago = fecha
1019
+ # Fechas del perοΏ½odo del servicio facturado (solo si concepto = 1?)
1020
+ fecha_serv_desde = fecha
1021
+ fecha_serv_hasta = fecha
1022
+ moneda_id = 'PES'
1023
+ moneda_ctz = '1.000'
1024
+ obs = "Observaciones Comerciales, libre"
1025
+ if '--caea' in sys.argv:
1026
+ periodo = fecha.replace("-", "")[:6]
1027
+ orden = 1 if fecha[-2:] < 16 else 2
1028
+ caea = wsmtxca.ConsultarCAEA(periodo, orden)
1029
+ else:
1030
+ caea = None
1031
+
1032
+ wsmtxca.CrearFactura(concepto, tipo_doc, nro_doc, tipo_cbte, punto_vta,
1033
+ cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto,
1034
+ imp_subtotal, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
1035
+ fecha_serv_desde, fecha_serv_hasta, # --
1036
+ moneda_id, moneda_ctz, obs, caea)
1037
+
1038
+ #tipo = 19
1039
+ #pto_vta = 2
1040
+ #nro = 1234
1041
+ #wsmtxca.AgregarCmpAsoc(tipo, pto_vta, nro)
1042
+
1043
+ tributo_id = 99
1044
+ desc = 'Impuesto Municipal Matanza'
1045
+ base_imp = "100.00"
1046
+ alic = "1.00"
1047
+ importe = "1.00"
1048
+ wsmtxca.AgregarTributo(tributo_id, desc, base_imp, alic, importe)
1049
+
1050
+ iva_id = 5 # 21%
1051
+ base_imp = 100
1052
+ importe = 21
1053
+ wsmtxca.AgregarIva(iva_id, base_imp, importe)
1054
+
1055
+ u_mtx = 123456
1056
+ cod_mtx = 1234567890123
1057
+ codigo = "P0001"
1058
+ ds = "Descripcion del producto P0001"
1059
+ qty = 2.00
1060
+ umed = 7
1061
+ precio = 100.00
1062
+ bonif = 0.00
1063
+ iva_id = 5
1064
+ imp_iva = 42.00
1065
+ imp_subtotal = 242.00
1066
+ wsmtxca.AgregarItem(u_mtx, cod_mtx, codigo, ds, qty, umed, precio, bonif,
1067
+ iva_id, imp_iva, imp_subtotal)
1068
+
1069
+ if not "--caea" in sys.argv:
1070
+ # ejemplo descuento (sin precio unitario)
1071
+ wsmtxca.AgregarItem(None, None, None, 'bonificacion',
1072
+ None, 99, None, None, 5, -21, -121)
1073
+ # ejemplo item solo descripciοΏ½n:
1074
+ # wsmtxca.AgregarItem(u_mtx, cod_mtx, codigo, ds, 1, umed,
1075
+ # 0, 0, iva_id, 0, 0)
1076
+
1077
+ # datos de Factura de CrοΏ½dito ElectrοΏ½nica MiPyMEs (FCE):
1078
+ if '--fce' in sys.argv:
1079
+ wsmtxca.AgregarOpcional(21, "2850590940090418135201") # CBU
1080
+
1081
+ print(wsmtxca.factura)
1082
+
1083
+ if '--caea' in sys.argv:
1084
+ wsmtxca.InformarComprobanteCAEA()
1085
+ else:
1086
+ wsmtxca.AutorizarComprobante()
1087
+
1088
+ print("Resultado", wsmtxca.Resultado)
1089
+ print("CAE", wsmtxca.CAE)
1090
+ print("Vencimiento", wsmtxca.Vencimiento)
1091
+ print("Reproceso", wsmtxca.Reproceso)
1092
+
1093
+ print(wsmtxca.Excepcion)
1094
+ print(wsmtxca.ErrMsg)
1095
+
1096
+ cae = wsmtxca.CAE
1097
+
1098
+ if cae:
1099
+
1100
+ wsmtxca.ConsultarComprobante(tipo_cbte, punto_vta, cbte_nro)
1101
+ print("CAE consulta", wsmtxca.CAE, wsmtxca.CAE == cae)
1102
+ print("NRO consulta", wsmtxca.CbteNro, wsmtxca.CbteNro == cbte_nro)
1103
+ print("TOTAL consulta", wsmtxca.ImpTotal, wsmtxca.ImpTotal == imp_total)
1104
+
1105
+ wsmtxca.AnalizarXml("XmlResponse")
1106
+ assert wsmtxca.ObtenerTagXml('codigoAutorizacion') == str(wsmtxca.CAE)
1107
+ assert wsmtxca.ObtenerTagXml('codigoConcepto') == str(concepto)
1108
+ assert wsmtxca.ObtenerTagXml('arrayItems', 0, 'item', 'unidadesMtx') == '123456'
1109
+
1110
+ except BaseException:
1111
+ print(wsmtxca.XmlRequest)
1112
+ print(wsmtxca.XmlResponse)
1113
+ print(wsmtxca.ErrCode)
1114
+ print(wsmtxca.ErrMsg)
1115
+
1116
+ if "--ajustar" in sys.argv:
1117
+ # print wsmtxca.client.help("autorizarComprobante").encode("latin1")
1118
+ try:
1119
+ tipo_cbte = 2
1120
+ punto_vta = 4000
1121
+ cbte_nro = wsmtxca.ConsultarUltimoComprobanteAutorizado(tipo_cbte, punto_vta)
1122
+ fecha = datetime.datetime.now().strftime("%Y-%m-%d")
1123
+ concepto = 3
1124
+ tipo_doc = 80
1125
+ nro_doc = "30000000007"
1126
+ cbte_nro = int(cbte_nro) + 1
1127
+ cbt_desde = cbte_nro
1128
+ cbt_hasta = cbt_desde
1129
+ imp_total = "21.00"
1130
+ imp_tot_conc = "0.00"
1131
+ imp_neto = None
1132
+ imp_trib = "0.00"
1133
+ imp_op_ex = "0.00"
1134
+ imp_subtotal = "0.00"
1135
+ fecha_cbte = fecha
1136
+ fecha_venc_pago = fecha
1137
+ # Fechas del perοΏ½odo del servicio facturado (solo si concepto = 1?)
1138
+ fecha_serv_desde = fecha
1139
+ fecha_serv_hasta = fecha
1140
+ moneda_id = 'PES'
1141
+ moneda_ctz = '1.000'
1142
+ obs = "Observaciones Comerciales, libre"
1143
+ caea = "24163778394093"
1144
+ fch_venc_cae = None
1145
+
1146
+ wsmtxca.CrearFactura(concepto, tipo_doc, nro_doc, tipo_cbte, punto_vta,
1147
+ cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto,
1148
+ imp_subtotal, imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
1149
+ fecha_serv_desde, fecha_serv_hasta, # --
1150
+ moneda_id, moneda_ctz, obs, caea, fch_venc_cae)
1151
+
1152
+ iva_id = 5 # 21%
1153
+ base_imp = 100
1154
+ importe = 21
1155
+ wsmtxca.AgregarIva(iva_id, base_imp, importe)
1156
+
1157
+ u_mtx = 1
1158
+ cod_mtx = 7790001001139
1159
+ codigo = None
1160
+ ds = "Descripcion del producto P0001"
1161
+ qty = None
1162
+ umed = 7
1163
+ precio = None
1164
+ bonif = None
1165
+ iva_id = 5
1166
+ imp_iva = 21.00
1167
+ imp_subtotal = 21.00
1168
+ wsmtxca.AgregarItem(u_mtx, cod_mtx, codigo, ds, qty, umed, precio, bonif,
1169
+ iva_id, imp_iva, imp_subtotal)
1170
+
1171
+ print(wsmtxca.factura)
1172
+
1173
+ if not caea:
1174
+ wsmtxca.AutorizarAjusteIVA()
1175
+ else:
1176
+ wsmtxca.InformarAjusteIVACAEA()
1177
+
1178
+ print("Resultado", wsmtxca.Resultado)
1179
+ print("CAE", wsmtxca.CAE)
1180
+ print("Vencimiento", wsmtxca.Vencimiento)
1181
+
1182
+ print(wsmtxca.Excepcion)
1183
+ print(wsmtxca.ErrMsg)
1184
+
1185
+ except BaseException:
1186
+ print(wsmtxca.XmlRequest)
1187
+ print(wsmtxca.XmlResponse)
1188
+ print(wsmtxca.ErrCode)
1189
+ print(wsmtxca.ErrMsg)
1190
+ raise
1191
+
1192
+ if "--parametros" in sys.argv:
1193
+ print(wsmtxca.ConsultarTiposComprobante())
1194
+ print(wsmtxca.ConsultarTiposDocumento())
1195
+ print(wsmtxca.ConsultarAlicuotasIVA())
1196
+ print(wsmtxca.ConsultarCondicionesIVA())
1197
+ print(wsmtxca.ConsultarMonedas())
1198
+ print(wsmtxca.ConsultarUnidadesMedida())
1199
+ print(wsmtxca.ConsultarTiposTributo())
1200
+ print(wsmtxca.ConsultarTiposDatosAdicionales())
1201
+
1202
+ if "--cotizacion" in sys.argv:
1203
+ print(wsmtxca.ConsultarCotizacionMoneda('DOL'))
1204
+
1205
+ if "--solicitar-caea" in sys.argv:
1206
+ periodo = sys.argv[sys.argv.index("--solicitar-caea") + 1]
1207
+ orden = sys.argv[sys.argv.index("--solicitar-caea") + 2]
1208
+
1209
+ if DEBUG:
1210
+ print("Consultando CAEA para periodo %s orden %s" % (periodo, orden))
1211
+
1212
+ caea = wsmtxca.ConsultarCAEA(periodo, orden)
1213
+ if not caea:
1214
+ print("Solicitando CAEA para periodo %s orden %s" % (periodo, orden))
1215
+ caea = wsmtxca.SolicitarCAEA(periodo, orden)
1216
+
1217
+ print("CAEA:", caea)
1218
+
1219
+ if wsmtxca.Errores:
1220
+ print("Errores:")
1221
+ for error in wsmtxca.Errores:
1222
+ print(error)
1223
+
1224
+ if DEBUG:
1225
+ print("periodo:", wsmtxca.Periodo)
1226
+ print("orden:", wsmtxca.Orden)
1227
+ print("fch_vig_desde:", wsmtxca.FchVigDesde)
1228
+ print("fch_vig_hasta:", wsmtxca.FchVigHasta)
1229
+ print("fch_tope_inf:", wsmtxca.FchTopeInf)
1230
+ print("fch_proceso:", wsmtxca.FchProceso)
1231
+
1232
+
1233
+ # busco el directorio de instalaciοΏ½n (global para que no cambie si usan otra dll)
1234
+ INSTALL_DIR = WSMTXCA.InstallDir = get_install_dir()
1235
+
1236
+
1237
+ if __name__ == '__main__':
1238
+
1239
+ if "--register" in sys.argv or "--unregister" in sys.argv:
1240
+ import win32com.server.register
1241
+ win32com.server.register.UseCommandLine(WSMTXCA)
1242
+ else:
1243
+ main()
app/pyafipws/wsremcarne.py ADDED
@@ -0,0 +1,691 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ # -*- coding: utf8 -*-
3
+ # This program is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3, or (at your option) any later
6
+ # version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but
9
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11
+ # for more details.
12
+
13
+ import base64
14
+ import time
15
+ import sys
16
+ import os
17
+ from .utils import json, BaseWS, inicializar_y_capturar_excepciones, get_install_dir
18
+ from . import utils
19
+ from pysimplesoap.client import SoapFault
20
+ import traceback
21
+ from .utils import date
22
+ """MΓ³dulo para obtener Remito Electronico Carnico:
23
+ del web service WSRemCarne versiΓ³n 1.0 de AFIP (RG4256/18 y RG4303/18)
24
+ """
25
+
26
+ __author__ = "Mariano Reingart <reingart@gmail.com>"
27
+ __copyright__ = "Copyright (C) 2018 Mariano Reingart"
28
+ __license__ = "LGPL 3.0"
29
+ __version__ = "1.01a"
30
+
31
+ LICENCIA = """
32
+ wsremcarne.py: Interfaz para generar Remito ElectrΓ³nico CΓ‘rnico AFIP v1.0
33
+ Remito de Carnes y subproductos derivados de la faena de bovinos y porcinos
34
+ ResoluciΓ³n General 4256/18 y ResoluciΓ³n General 4303/18.
35
+ Copyright (C) 2018 Mariano Reingart reingart@gmail.com
36
+ http://www.sistemasagiles.com.ar/trac/wiki/RemitoElectronicoCarnico
37
+
38
+ Este progarma es software libre, se entrega ABSOLUTAMENTE SIN GARANTIA
39
+ y es bienvenido a redistribuirlo bajo la licencia GPLv3.
40
+
41
+ Para informaciΓ³n adicional sobre garantΓ­a, soporte tΓ©cnico comercial
42
+ e incorporaciΓ³n/distribuciΓ³n en programas propietarios ver PyAfipWs:
43
+ http://www.sistemasagiles.com.ar/trac/wiki/PyAfipWs
44
+ """
45
+
46
+ AYUDA = """
47
+ Opciones:
48
+ --ayuda: este mensaje
49
+
50
+ --debug: modo depuraciΓ³n (detalla y confirma las operaciones)
51
+ --prueba: genera y autoriza una rec de prueba (no usar en producciΓ³n!)
52
+ --xml: almacena los requerimientos y respuestas XML (depuraciΓ³n)
53
+ --dummy: consulta estado de servidores
54
+
55
+ --generar: generar un remito
56
+ --emitir: emite un remito
57
+ --anular: anula un remito
58
+ --autorizar: autoriza un remito
59
+
60
+ --ult: consulta ultimo nro remito emitido
61
+ --consultar: consulta un remito generado
62
+
63
+ --tipos_comprobante: tabla de parametros para tipo de comprobante
64
+ --tipos_contingencia: tipo de contingencia que puede reportar
65
+ --tipos_categoria_emisor: tipos de categorΓ­as de emisor
66
+ --tipos_categoria_receptor: tipos de categorΓ­as de receptor
67
+ --tipos_estados: estados posibles en los que puede estar un remito cΓ‘rnico
68
+ --grupos_carne' grupos de los distintos tipos de cortes de carne
69
+ --tipos_carne': tipos de corte de carne
70
+ --codigos_domicilio: codigos de depositos habilitados para el cuit
71
+
72
+ Ver wsremcarne.ini para parΓ‘metros de configuraciΓ³n (URL, certificados, etc.)"
73
+ """
74
+
75
+
76
+ # importo funciones compartidas:
77
+
78
+
79
+ # constantes de configuraciΓ³n (producciΓ³n/homologaciΓ³n):
80
+
81
+ WSDL = ["https://serviciosjava.afip.gob.ar/wsremcarne/RemCarneService?wsdl",
82
+ "https://fwshomo.afip.gov.ar/wsremcarne/RemCarneService?wsdl"]
83
+
84
+ DEBUG = False
85
+ XML = False
86
+ CONFIG_FILE = "wsremcarne.ini"
87
+ HOMO = False
88
+ ENCABEZADO = []
89
+
90
+
91
+ class WSRemCarne(BaseWS):
92
+ "Interfaz para el WebService de Remito Electronico Carnico (Version 3)"
93
+ _public_methods_ = ['Conectar', 'Dummy', 'SetTicketAcceso', 'DebugLog',
94
+ 'GenerarRemito', 'EmitirRemito', 'AutorizarRemito', 'AnularRemito', 'ConsultarRemito',
95
+ 'InformarContingencia', 'ModificarViaje', 'RegistrarRecepcion', 'ConsultarUltimoRemitoEmitido',
96
+ 'CrearRemito', 'AgregarViaje', 'AgregarVehiculo', 'AgregarMercaderia',
97
+ 'AgregarDatosAutorizacion', 'AgregarContingencia',
98
+ 'ConsultarTiposCarne', 'ConsultarTiposCategoriaEmisor', 'ConsultarTiposCategoriaReceptor',
99
+ 'ConsultarTiposComprobante', 'ConsultarTiposContingencia', 'ConsultarTiposEstado',
100
+ 'ConsultarCodigosDomicilio', 'ConsultarGruposCarne, ConsultarPuntosEmision',
101
+ 'SetParametros', 'SetParametro', 'GetParametro', 'AnalizarXml', 'ObtenerTagXml', 'LoadTestXML',
102
+ ]
103
+ _public_attrs_ = ['XmlRequest', 'XmlResponse', 'Version', 'Traceback', 'Excepcion', 'LanzarExcepciones',
104
+ 'Token', 'Sign', 'Cuit', 'AppServerStatus', 'DbServerStatus', 'AuthServerStatus',
105
+ 'CodRemito', 'TipoComprobante', 'PuntoEmision',
106
+ 'NroRemito', 'CodAutorizacion', 'FechaVencimiento', 'FechaEmision', 'Estado', 'Resultado', 'QR',
107
+ 'ErrCode', 'ErrMsg', 'Errores', 'ErroresFormato', 'Observaciones', 'Obs', 'Evento', 'Eventos',
108
+ ]
109
+ _reg_progid_ = "WSRemCarne"
110
+ _reg_clsid_ = "{71DB0CB9-2ED7-4226-A1E6-C3FA7FB18F41}"
111
+
112
+ # Variables globales para BaseWS:
113
+ HOMO = HOMO
114
+ WSDL = WSDL[HOMO]
115
+ LanzarExcepciones = False
116
+ Version = "%s %s" % (__version__, HOMO and 'HomologaciΓ³n' or '')
117
+
118
+ def Conectar(self, *args, **kwargs):
119
+ ret = BaseWS.Conectar(self, *args, **kwargs)
120
+ return ret
121
+
122
+ def inicializar(self):
123
+ self.AppServerStatus = self.DbServerStatus = self.AuthServerStatus = None
124
+ self.CodRemito = self.TipoComprobante = self.PuntoEmision = None
125
+ self.NroRemito = self.CodAutorizacion = self.FechaVencimiento = self.FechaEmision = None
126
+ self.Estado = self.Resultado = self.QR = None
127
+ self.Errores = []
128
+ self.ErroresFormato = []
129
+ self.Observaciones = []
130
+ self.Eventos = []
131
+ self.Evento = self.ErrCode = self.ErrMsg = self.Obs = ""
132
+
133
+ def __analizar_errores(self, ret):
134
+ "Comprueba y extrae errores si existen en la respuesta XML"
135
+ self.Errores = [err['codigoDescripcion'] for err in ret.get('arrayErrores', [])]
136
+ self.ErroresFormato = [err['codigoDescripcionString'] for err in ret.get('arrayErroresFormato', [])]
137
+ errores = self.Errores + self.ErroresFormato
138
+ self.ErrCode = ' '.join(["%(codigo)s" % err for err in errores])
139
+ self.ErrMsg = '\n'.join(["%(codigo)s: %(descripcion)s" % err for err in errores])
140
+
141
+ def __analizar_observaciones(self, ret):
142
+ "Comprueba y extrae observaciones si existen en la respuesta XML"
143
+ self.Observaciones = [obs["codigoDescripcion"] for obs in ret.get('arrayObservaciones', [])]
144
+ self.Obs = '\n'.join(["%(codigo)s: %(descripcion)s" % obs for obs in self.Observaciones])
145
+
146
+ def __analizar_evento(self, ret):
147
+ "Comprueba y extrae el wvento informativo si existen en la respuesta XML"
148
+ evt = ret.get('evento')
149
+ if evt:
150
+ self.Eventos = [evt]
151
+ self.Evento = "%(codigo)s: %(descripcion)s" % evt
152
+
153
+ @inicializar_y_capturar_excepciones
154
+ def CrearRemito(self, tipo_comprobante, punto_emision, tipo_movimiento, categoria_emisor, cuit_titular_mercaderia, cod_dom_origen,
155
+ tipo_receptor, categoria_receptor=None, cuit_receptor=None, cuit_depositario=None,
156
+ cod_dom_destino=None, cod_rem_redestinar=None, cod_remito=None, estado=None,
157
+ **kwargs):
158
+ "Inicializa internamente los datos de un remito para autorizar"
159
+ self.remito = {'tipoComprobante': tipo_comprobante, 'puntoEmision': punto_emision, 'categoriaEmisor': categoria_emisor,
160
+ 'cuitTitularMercaderia': cuit_titular_mercaderia, 'cuitDepositario': cuit_depositario,
161
+ 'tipoReceptor': tipo_receptor, 'categoriaReceptor': categoria_receptor, 'cuitReceptor': cuit_receptor,
162
+ 'codDomOrigen': cod_dom_origen, 'codDomDestino': cod_dom_destino, 'tipoMovimiento': tipo_movimiento,
163
+ 'estado': estado, 'codRemito': cod_remito,
164
+ 'codRemRedestinado': cod_rem_redestinar,
165
+ 'arrayMercaderias': [], 'arrayContingencias': [],
166
+ }
167
+ return True
168
+
169
+ @inicializar_y_capturar_excepciones
170
+ def AgregarViaje(self, cuit_transportista=None, cuit_conductor=None, fecha_inicio_viaje=None, distancia_km=None, **kwargs):
171
+ "Agrega la informaciΓ³n referente al viaje del remito electrΓ³nico cΓ‘rnico"
172
+ self.remito['viaje'] = {'cuitTransportista': cuit_transportista,
173
+ 'cuitConductor': cuit_conductor,
174
+ 'fechaInicioViaje': fecha_inicio_viaje,
175
+ 'distanciaKm': distancia_km,
176
+ 'vehiculo': {}
177
+ }
178
+ return True
179
+
180
+ @inicializar_y_capturar_excepciones
181
+ def AgregarVehiculo(self, dominio_vehiculo=None, dominio_acoplado=None, **kwargs):
182
+ "Agrega la informaciΓ³n referente al vehiculo usado en el viaje del remito electrΓ³nico cΓ‘rnico"
183
+ self.remito['viaje']['vehiculo'] = {'dominioVehiculo': dominio_vehiculo, 'dominioAcoplado': dominio_acoplado}
184
+ return True
185
+
186
+ @inicializar_y_capturar_excepciones
187
+ def AgregarMercaderia(self, orden=None, cod_tipo_prod=None, cantidad=None, unidades=None, tropa=None, **kwargs):
188
+ "Agrega la informaciΓ³n referente a la mercaderΓ­a del remito electrΓ³nico cΓ‘rnico"
189
+ mercaderia = dict(orden=orden, tropa=tropa, codTipoProd=cod_tipo_prod, cantidad=cantidad, unidadMedida=unidades)
190
+ self.remito['arrayMercaderias'].append(dict(mercaderia=mercaderia))
191
+ return True
192
+
193
+ @inicializar_y_capturar_excepciones
194
+ def AgregarDatosAutorizacion(self, nro_remito=None, cod_autorizacion=None, fecha_emision=None, fecha_vencimiento=None, **kwargs):
195
+ "Agrega la informaciΓ³n referente a los datos de autorizaciΓ³n del remito electrΓ³nico cΓ‘rnico"
196
+ self.remito['datosEmision'] = dict(nroRemito=nro_remito, codAutorizacion=cod_autorizacion,
197
+ fechaEmision=fecha_emision, fechaVencimiento=fecha_vencimiento,
198
+ )
199
+ return True
200
+
201
+ @inicializar_y_capturar_excepciones
202
+ def AgregarContingencias(self, tipo=None, observacion=None, **kwargs):
203
+ "Agrega la informaciΓ³n referente a los opcionales de la liq. seq."
204
+ contingencia = dict(tipoContingencia=tipo, observacion=observacion)
205
+ self.remito['arrayContingencias'].append(dict(contingencia=contingencia))
206
+ return True
207
+
208
+ @inicializar_y_capturar_excepciones
209
+ def GenerarRemito(self, id_req, archivo="qr.png"):
210
+ "Informar los datos necesarios para la generaciΓ³n de un remito nuevo"
211
+ if not self.remito['arrayContingencias']:
212
+ del self.remito['arrayContingencias']
213
+ response = self.client.generarRemito(
214
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
215
+ idReq=id_req, remito=self.remito)
216
+ ret = response.get("generarRemitoReturn")
217
+ if ret:
218
+ self.__analizar_errores(ret)
219
+ self.__analizar_observaciones(ret)
220
+ self.__analizar_evento(ret)
221
+ self.AnalizarRemito(ret, archivo)
222
+ return bool(self.CodRemito)
223
+
224
+ def AnalizarRemito(self, ret, archivo=None):
225
+ "Extrae el resultado del remito, si existen en la respuesta XML"
226
+ if ret:
227
+ self.CodRemito = ret.get("codRemito")
228
+ self.TipoComprobante = ret.get("tipoComprobante")
229
+ self.PuntoEmision = ret.get("puntoEmision")
230
+ datos_aut = ret.get('datosAutorizacion')
231
+ if datos_aut:
232
+ self.NroRemito = datos_aut.get('nroRemito')
233
+ self.CodAutorizacion = datos_aut.get('codAutorizacion')
234
+ self.FechaEmision = datos_aut.get('fechaEmision')
235
+ self.FechaVencimiento = datos_aut.get('fechaVencimiento')
236
+ self.Estado = ret.get('estado')
237
+ self.Resultado = ret.get('resultado')
238
+ self.QR = ret.get('qr') or ""
239
+ if archivo:
240
+ qr = base64.b64decode(self.QR)
241
+ f = open(archivo, "wb")
242
+ f.write(qr)
243
+ f.close()
244
+
245
+ @inicializar_y_capturar_excepciones
246
+ def EmitirRemito(self, archivo="qr.png"):
247
+ "Emitir Remitos que se encuentren en estado Pendiente de Emitir."
248
+ response = self.client.emitirRemito(
249
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
250
+ codRemito=self.remito['codRemito'],
251
+ viaje=self.remito.get('viaje'))
252
+ ret = response.get("emitirRemitoReturn")
253
+ if ret:
254
+ self.__analizar_errores(ret)
255
+ self.__analizar_observaciones(ret)
256
+ self.__analizar_evento(ret)
257
+ self.AnalizarRemito(ret, archivo)
258
+ return bool(self.CodRemito)
259
+
260
+ @inicializar_y_capturar_excepciones
261
+ def AutorizarRemito(self, archivo="qr.png"):
262
+ "Autorizar o denegar un remito (cuando corresponde autorizacion) por parte del titular/depositario"
263
+ response = self.client.autorizarRemito(
264
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
265
+ codRemito=self.remito['codRemito'],
266
+ estado=self.remito['estado'])
267
+ ret = response.get("autorizarRemitoReturn")
268
+ if ret:
269
+ self.__analizar_errores(ret)
270
+ self.__analizar_observaciones(ret)
271
+ self.__analizar_evento(ret)
272
+ self.AnalizarRemito(ret, archivo)
273
+ return bool(self.CodRemito)
274
+
275
+ @inicializar_y_capturar_excepciones
276
+ def AnularRemito(self):
277
+ "Anular un remito generado que aΓΊn no haya sido emitido"
278
+ response = self.client.anularRemito(
279
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
280
+ codRemito=self.remito['codRemito'])
281
+ ret = response.get("anularRemitoReturn")
282
+ if ret:
283
+ self.__analizar_errores(ret)
284
+ self.__analizar_observaciones(ret)
285
+ self.__analizar_evento(ret)
286
+ self.AnalizarRemito(ret)
287
+ return bool(self.CodRemito)
288
+
289
+ @inicializar_y_capturar_excepciones
290
+ def ConsultarUltimoRemitoEmitido(self, tipo_comprobante=995, punto_emision=1):
291
+ "Obtener el ΓΊltimo nΓΊmero de remito que se emitiΓ³ por tipo de comprobante y punto de emisiΓ³n"
292
+ response = self.client.consultarUltimoRemitoEmitido(
293
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
294
+ tipoComprobante=tipo_comprobante,
295
+ puntoEmision=punto_emision)
296
+ ret = response.get("consultarUltimoRemitoReturn", {})
297
+ id_req = ret.get("idReq", 0)
298
+ rec = ret.get("remito", {})
299
+ self.__analizar_errores(ret)
300
+ self.__analizar_observaciones(ret)
301
+ self.__analizar_evento(ret)
302
+ self.AnalizarRemito(rec)
303
+ return id_req
304
+
305
+ @inicializar_y_capturar_excepciones
306
+ def ConsultarRemito(self, cod_remito=None, id_req=None,
307
+ tipo_comprobante=None, punto_emision=None, nro_comprobante=None):
308
+ "Obtener los datos de un remito generado"
309
+ print((self.client.help("consultarRemito")))
310
+ response = self.client.consultarRemito(
311
+ authRequest={'token': self.Token, 'sign': self.Sign, 'cuitRepresentada': self.Cuit},
312
+ codRemito=cod_remito,
313
+ idReq=id_req,
314
+ tipoComprobante=tipo_comprobante,
315
+ puntoEmision=punto_emision,
316
+ nroComprobante=nro_comprobante)
317
+ ret = response.get("consultarRemitoReturn", {})
318
+ id_req = ret.get("idReq", 0)
319
+ self.remito = rec = ret.get("remito", {})
320
+ self.__analizar_errores(ret)
321
+ self.__analizar_observaciones(ret)
322
+ self.__analizar_evento(ret)
323
+ self.AnalizarRemito(rec)
324
+ return id_req
325
+
326
+ @inicializar_y_capturar_excepciones
327
+ def Dummy(self):
328
+ "Obtener el estado de los servidores de la AFIP"
329
+ results = self.client.dummy()['dummyReturn']
330
+ self.AppServerStatus = str(results['appserver'])
331
+ self.DbServerStatus = str(results['dbserver'])
332
+ self.AuthServerStatus = str(results['authserver'])
333
+
334
+ @inicializar_y_capturar_excepciones
335
+ def ConsultarTiposComprobante(self, sep="||"):
336
+ "Obtener el cΓ³digo y descripciΓ³n para tipo de comprobante"
337
+ ret = self.client.consultarTiposComprobante(
338
+ authRequest={
339
+ 'token': self.Token, 'sign': self.Sign,
340
+ 'cuitRepresentada': self.Cuit, },
341
+ )['consultarTiposComprobanteReturn']
342
+ self.__analizar_errores(ret)
343
+ array = ret.get('arrayTiposComprobante', [])
344
+ lista = [it['codigoDescripcion'] for it in array]
345
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
346
+
347
+ @inicializar_y_capturar_excepciones
348
+ def ConsultarTiposContingencia(self, sep="||"):
349
+ "Obtener el cΓ³digo y descripciΓ³n para cada tipo de contingencia que puede reportar"
350
+ ret = self.client.consultarTiposContingencia(
351
+ authRequest={
352
+ 'token': self.Token, 'sign': self.Sign,
353
+ 'cuitRepresentada': self.Cuit, },
354
+ )['consultarTiposContingenciaReturn']
355
+ self.__analizar_errores(ret)
356
+ array = ret.get('arrayTiposContingencia', [])
357
+ lista = [it['codigoDescripcion'] for it in array]
358
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
359
+
360
+ @inicializar_y_capturar_excepciones
361
+ def ConsultarTiposCategoriaEmisor(self, sep="||"):
362
+ "Obtener el cΓ³digo y descripciΓ³n para tipos de categorΓ­as de emisor"
363
+ ret = self.client.consultarTiposCategoriaEmisor(
364
+ authRequest={
365
+ 'token': self.Token, 'sign': self.Sign,
366
+ 'cuitRepresentada': self.Cuit, },
367
+ )['consultarCategoriasEmisorReturn']
368
+ self.__analizar_errores(ret)
369
+ array = ret.get('arrayCategoriasEmisor', [])
370
+ lista = [it['codigoDescripcionString'] for it in array]
371
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
372
+
373
+ @inicializar_y_capturar_excepciones
374
+ def ConsultarTiposCategoriaReceptor(self, sep="||"):
375
+ "Obtener el cΓ³digo y descripciΓ³n para cada tipos de categorΓ­as de receptor"
376
+ ret = self.client.consultarTiposCategoriaReceptor(
377
+ authRequest={
378
+ 'token': self.Token, 'sign': self.Sign,
379
+ 'cuitRepresentada': self.Cuit, },
380
+ )['consultarCategoriasReceptorReturn']
381
+ self.__analizar_errores(ret)
382
+ array = ret.get('arrayCategoriasReceptor', [])
383
+ lista = [it['codigoDescripcionString'] for it in array]
384
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
385
+
386
+ @inicializar_y_capturar_excepciones
387
+ def ConsultarTiposEstado(self, sep="||"):
388
+ "Obtener el cΓ³digo y descripciΓ³n para cada estado posibles en los que puede estar un remito cΓ‘rnico"
389
+ ret = self.client.consultarTiposEstado(
390
+ authRequest={
391
+ 'token': self.Token, 'sign': self.Sign,
392
+ 'cuitRepresentada': self.Cuit, },
393
+ )['consultarTiposEstadoReturn']
394
+ self.__analizar_errores(ret)
395
+ array = ret.get('arrayTiposEstado', [])
396
+ lista = [it['codigoDescripcionString'] for it in array]
397
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
398
+
399
+ @inicializar_y_capturar_excepciones
400
+ def ConsultarGruposCarne(self, sep="||"):
401
+ "Obtener el cΓ³digo y descripciΓ³n para los grupos de los distintos tipos de cortes de carne"
402
+ ret = self.client.consultarGruposCarne(
403
+ authRequest={
404
+ 'token': self.Token, 'sign': self.Sign,
405
+ 'cuitRepresentada': self.Cuit, },
406
+ )['consultarGruposCarneReturn']
407
+ self.__analizar_errores(ret)
408
+ array = ret.get('arrayGruposCarne', [])
409
+ lista = [it['codigoDescripcionString'] for it in array]
410
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
411
+
412
+ @inicializar_y_capturar_excepciones
413
+ def ConsultarTiposCarne(self, cod_grupo_carne=1, sep="||"):
414
+ "Obtener el cΓ³digo y descripciΓ³n para tipos de corte de carne"
415
+ ret = self.client.consultarTiposCarne(
416
+ authRequest={
417
+ 'token': self.Token, 'sign': self.Sign,
418
+ 'cuitRepresentada': self.Cuit, },
419
+ codGrupoCarne=cod_grupo_carne,
420
+ )['consultarTiposCarneReturn']
421
+ self.__analizar_errores(ret)
422
+ array = ret.get('arrayTiposCarne', [])
423
+ lista = [it['codigoDescripcionString'] for it in array]
424
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
425
+
426
+ @inicializar_y_capturar_excepciones
427
+ def ConsultarCodigosDomicilio(self, cuit_titular=1, sep="||"):
428
+ "Obtener el cΓ³digo de depositos que tiene habilitados para operar el cuit informado"
429
+ ret = self.client.consultarCodigosDomicilio(
430
+ authRequest={
431
+ 'token': self.Token, 'sign': self.Sign,
432
+ 'cuitRepresentada': self.Cuit, },
433
+ cuitTitularDomicilio=cuit_titular,
434
+ )['consultarCodigosDomicilioReturn']
435
+ self.__analizar_errores(ret)
436
+ array = ret.get('arrayDomicilios', [])
437
+ lista = [it['codigoDescripcion'] for it in array]
438
+ return [("%s {codigo} %s {descripcion} %s" % (sep, sep, sep)).format(**it) if sep else it for it in lista]
439
+
440
+
441
+ # busco el directorio de instalaciΓ³n (global para que no cambie si usan otra dll)
442
+ if not hasattr(sys, "frozen"):
443
+ basepath = __file__
444
+ elif sys.frozen == 'dll':
445
+ import win32api
446
+ basepath = win32api.GetModuleFileName(sys.frozendllhandle)
447
+ else:
448
+ basepath = sys.executable
449
+ INSTALL_DIR = WSRemCarne.InstallDir = get_install_dir()
450
+
451
+
452
+ if __name__ == '__main__':
453
+ if '--ayuda' in sys.argv:
454
+ print(LICENCIA)
455
+ print(AYUDA)
456
+ sys.exit(0)
457
+
458
+ if "--register" in sys.argv or "--unregister" in sys.argv:
459
+ import win32com.server.register
460
+ win32com.server.register.UseCommandLine(WSRemCarne)
461
+ sys.exit(0)
462
+
463
+ from configparser import SafeConfigParser
464
+
465
+ try:
466
+
467
+ if "--version" in sys.argv:
468
+ print("VersiΓ³n: ", __version__)
469
+
470
+ for arg in sys.argv[1:]:
471
+ if arg.startswith("--"):
472
+ break
473
+ print("Usando configuraciΓ³n:", arg)
474
+ CONFIG_FILE = arg
475
+
476
+ config = SafeConfigParser()
477
+ config.read(CONFIG_FILE)
478
+ CERT = config.get('WSAA', 'CERT')
479
+ PRIVATEKEY = config.get('WSAA', 'PRIVATEKEY')
480
+ CUIT = config.get('WSRemCarne', 'CUIT')
481
+ ENTRADA = config.get('WSRemCarne', 'ENTRADA')
482
+ SALIDA = config.get('WSRemCarne', 'SALIDA')
483
+
484
+ if config.has_option('WSAA', 'URL') and not HOMO:
485
+ wsaa_url = config.get('WSAA', 'URL')
486
+ else:
487
+ wsaa_url = None
488
+ if config.has_option('WSRemCarne', 'URL') and not HOMO:
489
+ wsremcarne_url = config.get('WSRemCarne', 'URL')
490
+ else:
491
+ wsremcarne_url = WSDL[HOMO]
492
+
493
+ if config.has_section('DBF'):
494
+ conf_dbf = dict(config.items('DBF'))
495
+ if DEBUG:
496
+ print("conf_dbf", conf_dbf)
497
+ else:
498
+ conf_dbf = {}
499
+
500
+ DEBUG = '--debug' in sys.argv
501
+ XML = '--xml' in sys.argv
502
+
503
+ if DEBUG:
504
+ print("Usando ConfiguraciΓ³n:")
505
+ print("wsaa_url:", wsaa_url)
506
+ print("wsremcarne_url:", wsremcarne_url)
507
+
508
+ # obteniendo el TA
509
+ from .wsaa import WSAA
510
+ wsaa = WSAA()
511
+ ta = wsaa.Autenticar("wsremcarne", CERT, PRIVATEKEY, wsaa_url, debug=DEBUG)
512
+ if not ta:
513
+ sys.exit("Imposible autenticar con WSAA: %s" % wsaa.Excepcion)
514
+
515
+ # cliente soap del web service
516
+ wsremcarne = WSRemCarne()
517
+ wsremcarne.Conectar(wsdl=wsremcarne_url)
518
+ wsremcarne.SetTicketAcceso(ta)
519
+ wsremcarne.Cuit = CUIT
520
+ ok = None
521
+
522
+ if '--dummy' in sys.argv:
523
+ ret = wsremcarne.Dummy()
524
+ print("AppServerStatus", wsremcarne.AppServerStatus)
525
+ print("DbServerStatus", wsremcarne.DbServerStatus)
526
+ print("AuthServerStatus", wsremcarne.AuthServerStatus)
527
+ sys.exit(0)
528
+
529
+ if '--ult' in sys.argv:
530
+ try:
531
+ pto_emision = int(sys.argv[sys.argv.index("--ult") + 1])
532
+ except IndexError as ValueError:
533
+ pto_emision = 1
534
+ try:
535
+ tipo_cbte = int(sys.argv[sys.argv.index("--ult") + 1])
536
+ except IndexError as ValueError:
537
+ tipo_comprobante = 995
538
+ rec = {}
539
+ print("Consultando ultimo remito pto_emision=%s tipo_comprobante=%s" % (pto_emision, tipo_comprobante))
540
+ ok = wsremcarne.ConsultarUltimoRemitoEmitido(tipo_comprobante, pto_emision)
541
+ if wsremcarne.Excepcion:
542
+ print("EXCEPCION:", wsremcarne.Excepcion, file=sys.stderr)
543
+ if DEBUG:
544
+ print(wsremcarne.Traceback, file=sys.stderr)
545
+ print("Ultimo Nro de Remito", wsremcarne.NroRemito)
546
+ print("Errores:", wsremcarne.Errores)
547
+
548
+ if '--consultar' in sys.argv:
549
+ try:
550
+ cod_remito = sys.argv[sys.argv.index("--consultar") + 1]
551
+ except IndexError as ValueError:
552
+ cod_remito = None
553
+ rec = {}
554
+ print("Consultando remito cod_remito=%s" % (cod_remito, ))
555
+ ok = wsremcarne.ConsultarRemito(cod_remito)
556
+ if wsremcarne.Excepcion:
557
+ print("EXCEPCION:", wsremcarne.Excepcion, file=sys.stderr)
558
+ if DEBUG:
559
+ print(wsremcarne.Traceback, file=sys.stderr)
560
+ print("Ultimo Nro de Remito", wsremcarne.NroRemito)
561
+ print("Errores:", wsremcarne.Errores)
562
+ if DEBUG:
563
+ import pprint
564
+ pprint.pprint(wsremcarne.remito)
565
+
566
+ if '--prueba' in sys.argv:
567
+ rec = dict(tipo_comprobante=995, punto_emision=1, categoria_emisor=1,
568
+ tipo_movimiento='ENV', # ENV: Envio Normal, PLA: Retiro en planta, REP: Reparto, RED: Redestino
569
+ cuit_titular_mercaderia='20222222223', cod_dom_origen=1,
570
+ tipo_receptor='EM', # 'EM': DEPOSITO EMISOR, 'MI': MERCADO INTERNO, 'RP': REPARTO
571
+ categoria_receptor=1, id_req=int(time.time()),
572
+ cuit_receptor='20111111112', cuit_depositario=None,
573
+ cod_dom_destino=1, cod_rem_redestinar=None,
574
+ cod_remito=30,
575
+ )
576
+ if "--autorizar" in sys.argv:
577
+ rec["estado"] = 'A' # 'A': Autorizar, 'D': Denegar
578
+ rec['viaje'] = dict(cuit_transportista='20333333334', cuit_conductor='20333333334',
579
+ fecha_inicio_viaje='2018-10-01', distancia_km=999)
580
+ rec['viaje']['vehiculo'] = dict(dominio_vehiculo='AAA000', dominio_acoplado='ZZZ000')
581
+ rec['mercaderias'] = [dict(orden=1, tropa=1, cod_tipo_prod='2.13', cantidad=10, unidades=1)]
582
+ rec['datos_autorizacion'] = None # dict(nro_remito=None, cod_autorizacion=None, fecha_emision=None, fecha_vencimiento=None)
583
+ rec['contingencias'] = [dict(tipo=1, observacion="anulacion")]
584
+ with open(ENTRADA, "w") as archivo:
585
+ json.dump(rec, archivo, sort_keys=True, indent=4)
586
+
587
+ if '--cargar' in sys.argv:
588
+ with open(ENTRADA, "r") as archivo:
589
+ rec = json.load(archivo)
590
+ wsremcarne.CrearRemito(**rec)
591
+ wsremcarne.AgregarViaje(**rec['viaje'])
592
+ wsremcarne.AgregarVehiculo(**rec['viaje']['vehiculo'])
593
+ for mercaderia in rec['mercaderias']:
594
+ wsremcarne.AgregarMercaderia(**mercaderia)
595
+ datos_aut = rec['datos_autorizacion']
596
+ if datos_aut:
597
+ wsremcarne.AgregarDatosAutorizacion(**datos_aut)
598
+ for contingencia in rec['contingencias']:
599
+ wsremcarne.AgregarContingencias(**contingencia)
600
+
601
+ if '--generar' in sys.argv:
602
+ if '--testing' in sys.argv:
603
+ wsremcarne.LoadTestXML("tests/xml/wsremcarne_generar_response_ok_beta.xml") # cargo respuesta
604
+
605
+ ok = wsremcarne.GenerarRemito(id_req=rec['id_req'])
606
+
607
+ if '--emitir' in sys.argv:
608
+ ok = wsremcarne.EmitirRemito()
609
+
610
+ if '--autorizar' in sys.argv:
611
+ ok = wsremcarne.AutorizarRemito()
612
+
613
+ if '--anular' in sys.argv:
614
+ ok = wsremcarne.AnularRemito()
615
+
616
+ if ok is not None:
617
+ print("Resultado: ", wsremcarne.Resultado)
618
+ print("Cod Remito: ", wsremcarne.CodRemito)
619
+ if wsremcarne.CodAutorizacion:
620
+ print("Numero Remito: ", wsremcarne.NroRemito)
621
+ print("Cod Autorizacion: ", wsremcarne.CodAutorizacion)
622
+ print("Fecha Emision", wsremcarne.FechaEmision)
623
+ print("Fecha Vencimiento", wsremcarne.FechaVencimiento)
624
+ print("Estado: ", wsremcarne.Estado)
625
+ print("Observaciones: ", wsremcarne.Observaciones)
626
+ print("Errores:", wsremcarne.Errores)
627
+ print("Errores Formato:", wsremcarne.ErroresFormato)
628
+ print("Evento:", wsremcarne.Evento)
629
+ rec['cod_remito'] = wsremcarne.CodRemito
630
+ rec['resultado'] = wsremcarne.Resultado
631
+ rec['observaciones'] = wsremcarne.Observaciones
632
+ rec['fecha_emision'] = wsremcarne.FechaEmision
633
+ rec['fecha_vencimiento'] = wsremcarne.FechaVencimiento
634
+ rec['errores'] = wsremcarne.Errores
635
+ rec['errores_formato'] = wsremcarne.ErroresFormato
636
+ rec['evento'] = wsremcarne.Evento
637
+
638
+ if '--grabar' in sys.argv:
639
+ with open(SALIDA, "w") as archivo:
640
+ json.dump(rec, archivo, sort_keys=True, indent=4)
641
+
642
+ # Recuperar parΓ‘metros:
643
+
644
+ if '--tipos_comprobante' in sys.argv:
645
+ ret = wsremcarne.ConsultarTiposComprobante()
646
+ print("\n".join(ret))
647
+
648
+ if '--tipos_contingencia' in sys.argv:
649
+ ret = wsremcarne.ConsultarTiposContingencia()
650
+ print("\n".join(ret))
651
+
652
+ if '--tipos_categoria_emisor' in sys.argv:
653
+ ret = wsremcarne.ConsultarTiposCategoriaEmisor()
654
+ print("\n".join(ret))
655
+
656
+ if '--tipos_categoria_receptor' in sys.argv:
657
+ ret = wsremcarne.ConsultarTiposCategoriaReceptor()
658
+ print("\n".join(ret))
659
+
660
+ if '--tipos_estados' in sys.argv:
661
+ ret = wsremcarne.ConsultarTiposEstado()
662
+ print("\n".join(ret))
663
+
664
+ if '--grupos_carne' in sys.argv:
665
+ ret = wsremcarne.ConsultarGruposCarne()
666
+ print("\n".join(ret))
667
+
668
+ if '--tipos_carne' in sys.argv:
669
+ for grupo_carne in wsremcarne.ConsultarGruposCarne(sep=None):
670
+ ret = wsremcarne.ConsultarTiposCarne(grupo_carne['codigo'])
671
+ print("\n".join(ret))
672
+
673
+ if '--codigos_domicilio' in sys.argv:
674
+ cuit = input("Cuit Titular Domicilio: ")
675
+ ret = wsremcarne.ConsultarCodigosDomicilio(cuit)
676
+ print("\n".join(ret))
677
+
678
+ if wsremcarne.Errores or wsremcarne.ErroresFormato:
679
+ print("Errores:", wsremcarne.Errores, wsremcarne.ErroresFormato)
680
+
681
+ print("hecho.")
682
+
683
+ except SoapFault as e:
684
+ print("Falla SOAP:", e.faultcode, e.faultstring.encode("ascii", "ignore"))
685
+ sys.exit(3)
686
+ except Exception as e:
687
+ ex = utils.exception_info()
688
+ print(ex)
689
+ if DEBUG:
690
+ raise
691
+ sys.exit(5)
app/reports/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ ο»Ώ
app/reports/builders.py ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Report Builders β€” Transform raw scraper data into structured report models.
3
+ Each builder handles a specific report type.
4
+ """
5
+ import logging
6
+ from typing import Any, Optional
7
+ from datetime import datetime, timezone
8
+
9
+ from app.reports.schemas import (
10
+ ScoreHistorial, ScoreCrediticio,
11
+ PersonReport, CompanyReport, Identificacion, Contacto, Domicilio,
12
+ DatosFiscales, DatosFinancieros, DatosSocietarios, DatosPatrimoniales,
13
+ DatosJudiciales, PublicacionBO, BcraHistorial, ChequeRechazado,
14
+ CausaJudicial, Vehiculo, InfraccionTransito, MarcaINPI, Vinculo,
15
+ DatosPrevisionales, DatosAcademicos, TituloAcademico, IndicadorRiesgo,
16
+ Inmueble, Inhibicion, DatosRegistroCivil, GroupReport, VehicleReport,
17
+ PropertyReport, ReportMeta, ActividadFiscal, CompanyIdentificacion, RedSocial,
18
+ DatosIGJ, ViasSalud, ContratoEstado, MatriculaProfesional,
19
+ DatosBilleterasVirtuales, BilleteraVirtual, TimelineEvent, DeudorAlimentario, Monotributo
20
+ )
21
+ from app.reports.schemas import (
22
+ CompanyReport, VehicleReport, PropertyReport, GroupReport
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ # ─── Helper Functions ───
29
+
30
+ def _safe_get(data: dict, key: str, default=None):
31
+ """Obtiene valor de dict de forma segura."""
32
+ if not isinstance(data, dict):
33
+ return default
34
+ return data.get(key, default)
35
+
36
+ def _safe_list(data: dict, key: str) -> list:
37
+ """Obtiene lista de forma segura."""
38
+ val = _safe_get(data, key)
39
+ return val if isinstance(val, list) else []
40
+
41
+ def _safe_dict(data: dict, key: str) -> dict:
42
+ val = _safe_get(data, key)
43
+ return val if isinstance(val, dict) else {}
44
+
45
+
46
+ # ─── Builders ───
47
+
48
+ def build_identificacion(data: dict, cuit: str) -> Identificacion:
49
+ """Construye secciΓ³n de identificaciΓ³n."""
50
+ return Identificacion(
51
+ cuit=cuit,
52
+ tipo_clave=_safe_get(data, "tipo_clave"),
53
+ tipo_documento=_safe_get(data, "tipo_documento"),
54
+ numero_documento=_safe_get(data, "numero_documento"),
55
+ nombres=_safe_get(data, "nombres"),
56
+ apellido=_safe_get(data, "apellido"),
57
+ nombres_completo=_safe_get(data, "nombres_completo"),
58
+ fecha_nacimiento=_safe_get(data, "fecha_nacimiento"),
59
+ fecha_fallecimiento=_safe_get(data, "fecha_fallecimiento"),
60
+ sexo=_safe_get(data, "sexo"),
61
+ )
62
+
63
+
64
+ def build_contacto(data: dict) -> Contacto:
65
+ """Construye secciΓ³n de contacto."""
66
+ # Intentar desde redes sociales
67
+ redes = _safe_list(data.get("redes_sociales", {}), "redes_sociales") if isinstance(data.get("redes_sociales"), dict) else _safe_list(data, "redes_sociales")
68
+ emails = []
69
+ phones = []
70
+ for r in redes:
71
+ if isinstance(r, dict):
72
+ if r.get("tipo") == "email":
73
+ emails.append(r.get("valor", ""))
74
+ elif r.get("tipo") == "telefono":
75
+ phones.append(r.get("valor", ""))
76
+
77
+ return Contacto(
78
+ emails=emails,
79
+ telefonos=phones,
80
+ redes_sociales=_safe_list(data, "redes_sociales"),
81
+ )
82
+
83
+
84
+ def build_domicilios(data: dict) -> list[Domicilio]:
85
+ """Construye lista de domicilios."""
86
+ domicilios_raw = _safe_list(data, "domicilios")
87
+ domicilios = []
88
+ for d in domicilios_raw:
89
+ if isinstance(d, dict):
90
+ domicilios.append(Domicilio(
91
+ calle=_safe_get(d, "calle"),
92
+ numero=_safe_get(d, "numero"),
93
+ piso=_safe_get(d, "piso"),
94
+ depto=_safe_get(d, "depto"),
95
+ localidad=_safe_get(d, "localidad"),
96
+ provincia=_safe_get(d, "provincia"),
97
+ codigo_postal=_safe_get(d, "codigo_postal"),
98
+ tipo=_safe_get(d, "tipo"),
99
+ ))
100
+ return domicilios
101
+
102
+
103
+ def build_datos_fiscales(arca_data: dict, cuit: str, actividades: list[ActividadFiscal] = None) -> DatosFiscales:
104
+ """Construye datos fiscales desde ARCA/AFIP."""
105
+ if not isinstance(arca_data, dict):
106
+ arca_data = {}
107
+
108
+ domicilios = build_domicilios(arca_data)
109
+ domicilio_fiscal_raw = _safe_get(arca_data, "domicilio_fiscal")
110
+ domicilio_fiscal = Domicilio(**domicilio_fiscal_raw) if isinstance(domicilio_fiscal_raw, dict) and domicilio_fiscal_raw else None
111
+
112
+ if domicilio_fiscal and not domicilios:
113
+ domicilios = [domicilio_fiscal]
114
+
115
+ monotributo_raw = _safe_get(arca_data, "monotributo")
116
+ monotributo_obj = Monotributo(**monotributo_raw) if isinstance(monotributo_raw, dict) else None
117
+
118
+ return DatosFiscales(
119
+ cuit=arca_data.get("cuit", cuit),
120
+ tipo_clave=_safe_get(arca_data, "tipo_clave"),
121
+ tipo_documento=_safe_get(arca_data, "tipo_documento"),
122
+ numero_documento=_safe_get(arca_data, "numero_documento"),
123
+ nombres=_safe_get(arca_data, "nombres"),
124
+ apellido=_safe_get(arca_data, "apellido"),
125
+ nombres_completo=_safe_get(arca_data, "nombres_completo"),
126
+ condicion_iva=_safe_get(arca_data, "condicion_iva"),
127
+ estado_afip=_safe_get(arca_data, "estado_afip"),
128
+ fecha_inicio_actividad=_safe_get(arca_data, "fecha_inicio_actividad"),
129
+ fecha_nacimiento=_safe_get(arca_data, "fecha_nacimiento"),
130
+ fecha_contrato_social=_safe_get(arca_data, "fecha_contrato_social"),
131
+ fecha_fallecimiento=_safe_get(arca_data, "fecha_fallecimiento"),
132
+ forma_juridica=_safe_get(arca_data, "forma_juridica"),
133
+ mes_cierre=_safe_get(arca_data, "mes_cierre"),
134
+ periodo_actividad_principal=_safe_get(arca_data, "periodo_actividad_principal"),
135
+ actividades=actividades or [],
136
+ domicilios=domicilios,
137
+ domicilio_fiscal=domicilio_fiscal,
138
+ monotributo=monotributo_obj,
139
+ )
140
+
141
+
142
+ def build_datos_financieros(bcra_data: dict, historial_financiero_anterior: dict = None) -> DatosFinancieros:
143
+ """Construye datos financieros desde BCRA + fallback histΓ³rico."""
144
+ if not isinstance(bcra_data, dict):
145
+ bcra_data = {}
146
+
147
+ deudas = []
148
+ for ent in _safe_list(bcra_data, "entidades"):
149
+ if isinstance(ent, dict):
150
+ deudas.append(BcraHistorial(
151
+ entidad=ent.get("entidad"),
152
+ situacion=ent.get("situacion"),
153
+ fecha=ent.get("fecha"),
154
+ monto=ent.get("monto"),
155
+ tipo=ent.get("tipo"),
156
+ ))
157
+
158
+ cheques = []
159
+ for ch in _safe_list(bcra_data, "cheques_rechazados"):
160
+ if isinstance(ch, dict):
161
+ cheques.append(ChequeRechazado(
162
+ fecha=ch.get("fecha"),
163
+ banco=ch.get("banco"),
164
+ sucursal=ch.get("sucursal"),
165
+ monto=ch.get("monto"),
166
+ motivo=ch.get("motivo"),
167
+ ))
168
+
169
+ # Fallback histΓ³rico si BCRA fallΓ³
170
+ if not _safe_get(bcra_data, "entidades") and historial_financiero_anterior:
171
+ hfa = historial_financiero_anterior
172
+ situacion = hfa.get("bcra_situacion_actual")
173
+ if situacion:
174
+ bcra_data = {
175
+ "bcra_situacion_actual": situacion,
176
+ "entidades": hfa.get("entidades", []),
177
+ "cheques_rechazados": hfa.get("cheques_rechazados", []),
178
+ }
179
+
180
+ situacion = _safe_get(bcra_data, "bcra_situacion_actual")
181
+ if situacion is None:
182
+ situacion = 1 # Normal por defecto
183
+
184
+ return DatosFinancieros(
185
+ bcra_situacion_actual=situacion,
186
+ bcra_situacion_descripcion=SITUACIONES_BCRA.get(situacion, "Desconocida"),
187
+ bcra_historial=_safe_list(bcra_data, "entidades"),
188
+ bcra_total_deuda_miles=_safe_get(bcra_data, "bcra_total_deuda_miles", 0.0),
189
+ bcra_dias_atraso_max=_safe_get(bcra_data, "bcra_dias_atraso_max", 0),
190
+ cheques_rechazados=_safe_list(bcra_data, "cheques_rechazados"),
191
+ tiene_deuda=_safe_get(bcra_data, "tiene_deuda", False),
192
+ )
193
+
194
+
195
+ def build_datos_societarios(igj_data: dict, inpi_data: dict, compras_data: dict) -> DatosSocietarios:
196
+ """Construye datos societarios."""
197
+ return DatosSocietarios(
198
+ igj=_safe_dict(igj_data, "igj"),
199
+ inpi=_safe_dict(inpi_data, "inpi"),
200
+ compras_estatales=_safe_dict(compras_data, "compras_estatales"),
201
+ )
202
+
203
+
204
+ def build_datos_patrimoniales(
205
+ arba_auto_data: dict, arba_catastro_data: dict,
206
+ dnrpa_data: dict, infracciones_data: dict,
207
+ carto_arba_data: dict, sinais_data: dict
208
+ ) -> DatosPatrimoniales:
209
+ """Construye datos patrimoniales."""
210
+ inmuebles = []
211
+ for inm in _safe_list(arba_catastro_data, "inmuebles"):
212
+ if isinstance(inm, dict):
213
+ inmuebles.append(Inmueble(
214
+ partida=inm.get("partida"),
215
+ nomenclatura=inm.get("nomenclatura"),
216
+ direccion=inm.get("direccion"),
217
+ superficie=inm.get("superficie"),
218
+ valuacion=inm.get("valuacion"),
219
+ tipo=inm.get("tipo"),
220
+ ))
221
+
222
+ vehiculos = []
223
+ for v in _safe_list(dnrpa_data, "vehiculos"):
224
+ if isinstance(v, dict):
225
+ vehiculos.append(Vehiculo(
226
+ dominio=v.get("dominio"),
227
+ marca=v.get("marca"),
228
+ modelo=v.get("modelo"),
229
+ anio=v.get("anio"),
230
+ tipo=v.get("tipo"),
231
+ registro=v.get("registro"),
232
+ titular=v.get("titular"),
233
+ estado=v.get("estado"),
234
+ ))
235
+
236
+ infracciones = []
237
+ for inf in _safe_list(infracciones_data, "infracciones"):
238
+ if isinstance(inf, dict):
239
+ infracciones.append(InfraccionTransito(
240
+ fecha=inf.get("fecha"),
241
+ tipo=inf.get("tipo"),
242
+ jurisdiccion=inf.get("jurisdiccion"),
243
+ monto=inf.get("monto"),
244
+ estado=inf.get("estado"),
245
+ ))
246
+
247
+ return DatosPatrimoniales(
248
+ inmuebles=inmuebles,
249
+ vehiculos=vehiculos,
250
+ infracciones_transito=infracciones,
251
+ )
252
+
253
+
254
+ def build_datos_judiciales(
255
+ pj_data: dict, pj_prov_data: dict,
256
+ bo_data: dict, bo_prov_data: dict,
257
+ inhibiciones_data: dict, juba_data: dict,
258
+ siscop_data: dict
259
+ ) -> DatosJudiciales:
260
+ """Construye datos judiciales."""
261
+ causas = []
262
+ for c in _safe_list(pj_data, "causas"):
263
+ if isinstance(c, dict):
264
+ causas.append(CausaJudicial(
265
+ numero=c.get("numero"),
266
+ fuero=c.get("fuero"),
267
+ caratula=c.get("caratula"),
268
+ estado=c.get("estado"),
269
+ fecha=c.get("fecha"),
270
+ rol=c.get("rol"),
271
+ ))
272
+
273
+ publicaciones = []
274
+ for p in _safe_list(bo_data, "publicaciones"):
275
+ if isinstance(p, dict):
276
+ publicaciones.append(PublicacionBO(
277
+ fecha=p.get("fecha"),
278
+ seccion=p.get("seccion"),
279
+ texto=p.get("texto"),
280
+ url=p.get("url"),
281
+ ))
282
+
283
+ inhibiciones = []
284
+ for inh in _safe_list(inhibiciones_data, "inhibiciones"):
285
+ if isinstance(inh, dict):
286
+ inhibiciones.append(Inhibicion(
287
+ numero=inh.get("numero"),
288
+ juzgado=inh.get("juzgado"),
289
+ fecha=inh.get("fecha"),
290
+ monto=inh.get("monto"),
291
+ estado=inh.get("estado"),
292
+ ))
293
+
294
+ return DatosJudiciales(
295
+ causas=causas,
296
+ publicaciones_bo=publicaciones,
297
+ inhibiciones=inhibiciones,
298
+ )
299
+
300
+
301
+ def build_vinculos(igj_data: dict, domicilios: list[Domicilio]) -> list[Vinculo]:
302
+ """Detecta vΓ­nculos por domicilio compartido (IGJ)."""
303
+ vinculos = []
304
+ if not igj_data or not domicilios:
305
+ return vinculos
306
+
307
+ doms_persona = set()
308
+ for d in domicilios:
309
+ parts = [d.calle, d.numero, d.localidad]
310
+ normalized = " ".join(p.lower().strip() for p in parts if p).strip()
311
+ if normalized and len(normalized) > 5:
312
+ doms_persona.add(normalized)
313
+
314
+ for socio in igj_data.get("socios_directivos", []):
315
+ if not isinstance(socio, dict):
316
+ continue
317
+ dom_socio = socio.get("domicilio", "")
318
+ if dom_socio:
319
+ dom_norm = " ".join(dom_socio.lower().split()[:5])
320
+ for dom_p in doms_persona:
321
+ if dom_norm and dom_p and (dom_norm in dom_p or dom_p in dom_norm):
322
+ vinculos.append(Vinculo(
323
+ cuit=socio.get("cuil"),
324
+ nombre=socio.get("nombre", ""),
325
+ tipo="Domiciliario",
326
+ detalle=f"Domicilio compartido: {dom_socio}"
327
+ ))
328
+ break
329
+ return vinculos
330
+
331
+
332
+ def build_riesgo_indicadores(data: dict, all_results: dict) -> list[IndicadorRiesgo]:
333
+ """Construye indicadores de riesgo."""
334
+ indicadores = []
335
+
336
+ # BCRA
337
+ financiero = _safe_dict(data, "financiero")
338
+ situacion = financiero.get("bcra_situacion_actual")
339
+ if situacion and situacion > 1:
340
+ indicadores.append(IndicadorRiesgo(
341
+ codigo="BCRA_SITUACION",
342
+ nivel="Alto" if situacion >= 4 else "Medio",
343
+ descripcion=f"SituaciΓ³n BCRA: {SITUACIONES_BCRA.get(situacion, 'Desconocida')}",
344
+ detalle=f"SituaciΓ³n actual: {situacion}",
345
+ ))
346
+
347
+ # Judicial
348
+ judicial = _safe_dict(data, "judicial")
349
+ causas = _safe_list(judicial, "causas")
350
+ if len(causas) > 3:
351
+ indicadores.append(IndicadorRiesgo(
352
+ codigo="JUDICIAL_CAUSAS_MULTIPLES",
353
+ nivel="Medio",
354
+ descripcion="MΓΊltiples causas judiciales",
355
+ detalle=f"Se detectaron {len(causas)} causas judiciales",
356
+ ))
357
+
358
+ # Inhibiciones
359
+ inhibiciones = _safe_list(judicial, "inhibiciones")
360
+ if inhibiciones:
361
+ indicadores.append(IndicadorRiesgo(
362
+ codigo="INHIBICIONES",
363
+ nivel="Alto",
364
+ descripcion="Inhibiciones registradas",
365
+ detalle=f"{len(inhibiciones)} inhibiciΓ³n(es) activa(s)",
366
+ ))
367
+
368
+ # Deudores alimentarios
369
+ deudores = _safe_list(data.get("deudores_alimentarios", {}), "deudores")
370
+ if deudores:
371
+ indicadores.append(IndicadorRiesgo(
372
+ codigo="DEUDOR_ALIMENTARIO",
373
+ nivel="Alto",
374
+ descripcion="Registro de deudor alimentario moroso",
375
+ detalle="Aparece en RDAM",
376
+ ))
377
+
378
+ # BCRA cheques rechazados
379
+ cheques = _safe_list(financiero, "cheques_rechazados")
380
+ if cheques:
381
+ indicadores.append(IndicadorRiesgo(
382
+ codigo="CHEQUES_RECHAZADOS",
383
+ nivel="Medio",
384
+ descripcion="Cheques rechazados registrados",
385
+ detalle=f"{len(cheques)} cheque(s) rechazado(s)",
386
+ ))
387
+
388
+ return indicadores
389
+
390
+
391
+ def build_person_report(
392
+ cuit: str,
393
+ data: dict,
394
+ results: dict,
395
+ force_refresh: bool = False,
396
+ ) -> PersonReport:
397
+ """Construye reporte completo de persona."""
398
+
399
+ # Datos base
400
+ arca_data = _safe_dict(results.get("arca_afip", {}), "data") if "arca_afip" in results else _safe_dict(data, "fiscal")
401
+ bcra_data = _safe_dict(results.get("bcra", {}), "data") if "bcra" in results else _safe_dict(data, "financiero")
402
+ igj_data = _safe_dict(results.get("igj", {}), "data") if "igj" in results else _safe_dict(data, "societario", {}).get("igj", {})
403
+ inpi_data = _safe_dict(results.get("inpi", {}), "data") if "inpi" in results else _safe_dict(data, "societario", {}).get("inpi", {})
404
+ compras_data = _safe_dict(results.get("compras_estatales", {}), "data") if "compras_estatales" in results else _safe_dict(data, "societario", {}).get("compras_estatales", {})
405
+
406
+ # IdentificaciΓ³n
407
+ identificacion = build_identificacion(arca_data, cuit)
408
+ contacto = build_contacto(_safe_dict(data, "contacto"))
409
+ domicilios = build_domicilios(arca_data)
410
+
411
+ # Secciones principales
412
+ fiscal = build_datos_fiscales(arca_data, cuit)
413
+ financiero = build_datos_financieros(bcra_data, _safe_dict(data, "financiero"))
414
+ societario = build_datos_societarios(igj_data, inpi_data, compras_data)
415
+ patrimonial = build_datos_patrimoniales(
416
+ _safe_dict(data, "arba_automotores"),
417
+ _safe_dict(data, "arba_catastro"),
418
+ _safe_dict(data, "dnrpa"),
419
+ _safe_dict(data, "infracciones"),
420
+ _safe_dict(data, "carto_arba"),
421
+ _safe_dict(data, "sinai"),
422
+ )
423
+ judicial = build_datos_judiciales(
424
+ _safe_dict(data, "poder_judicial"),
425
+ _safe_dict(data, "poder_judicial_provincial"),
426
+ _safe_dict(data, "boletin_oficial"),
427
+ _safe_dict(data, "boletines_provinciales"),
428
+ _safe_dict(data, "inhibiciones"),
429
+ _safe_dict(data, "juba"),
430
+ _safe_dict(data, "siscop"),
431
+ )
432
+ previsional = DatosPrevisionales(
433
+ anses=_safe_dict(data, "anses"),
434
+ monotributo_historial=_safe_dict(data, "monotributo_historial"),
435
+ )
436
+ academico = DatosAcademicos(
437
+ sgarhu=_safe_dict(data, "sgarhu"),
438
+ titulos=[],
439
+ )
440
+ registro_civil = DatosRegistroCivil(
441
+ siscop=_safe_dict(data, "siscop"),
442
+ defunciones=[],
443
+ )
444
+ billeteras = DatosBilleterasVirtuales(
445
+ billeteras=_safe_list(data.get("billeteras_virtuales", {}), "billeteras"),
446
+ )
447
+ compras_estatales = ContratoEstado(**_safe_dict(data, "compras_estatales")) if data.get("compras_estatales") else None
448
+
449
+ # Marcas INPI
450
+ marcas_inpi = []
451
+ for m in _safe_list(inpi_data, "marcas"):
452
+ if isinstance(m, dict):
453
+ marcas_inpi.append(MarcaINPI(
454
+ numero=m.get("numero"),
455
+ denominacion=m.get("denominacion"),
456
+ clase=m.get("clase"),
457
+ estado=m.get("estado"),
458
+ titular=m.get("titular"),
459
+ fecha_presentacion=m.get("fecha_presentacion"),
460
+ ))
461
+
462
+ # VΓ­nculos
463
+ domicilios_objs = [Domicilio(**d) if isinstance(d, dict) else d for d in _safe_list(arca_data, "domicilios")]
464
+ vinculos = build_vinculos(igj_data, domicilios_objs)
465
+
466
+ # Riesgo
467
+ riesgo = build_riesgo_indicadores(data, _safe_dict(data, "resultados"))
468
+
469
+ # Timeline BO
470
+ timeline = []
471
+ for t in _safe_list(data.get("timeline_boa", {}), "eventos"):
472
+ if isinstance(t, dict):
473
+ timeline.append(TimelineEvent(
474
+ fecha=t.get("fecha"),
475
+ tipo=t.get("tipo"),
476
+ descripcion=t.get("descripcion"),
477
+ fuente=t.get("fuente"),
478
+ ))
479
+
480
+ # Deudores alimentarios
481
+ deudores = []
482
+ for d in _safe_list(data.get("deudores_alimentarios", {}), "deudores"):
483
+ if isinstance(d, dict):
484
+ deudores.append(DeudorAlimentario(
485
+ dni=d.get("dni"),
486
+ nombre=d.get("nombre"),
487
+ estado=d.get("estado"),
488
+ detalle=d.get("detalle"),
489
+ ))
490
+
491
+ # Monotributo historial
492
+ monotributo = None
493
+ mh = _safe_dict(data, "monotributo_historial")
494
+ if mh:
495
+ monotributo = Monotributo(
496
+ historial_categorias=_safe_list(mh, "historial_categorias"),
497
+ categoria_actual=mh.get("categoria_actual"),
498
+ es_monotributista=mh.get("es_monotributista", False),
499
+ nota=mh.get("nota"),
500
+ )
501
+
502
+ # Registro conductores
503
+ licencia = None
504
+ rc = _safe_dict(data, "registro_conductores")
505
+ if rc:
506
+ licencia = {
507
+ "numero": rc.get("numero"),
508
+ "categoria": rc.get("categoria"),
509
+ "vencimiento": rc.get("vencimiento"),
510
+ }
511
+
512
+ # Score
513
+ score = compute_score(PersonReport(
514
+ meta=ReportMeta(report_id="", generated_at=datetime.now(timezone.utc), sources=[]),
515
+ identificacion=identificacion,
516
+ contacto=contacto,
517
+ domicilios=domicilios,
518
+ fiscal=fiscal,
519
+ financiero=financiero,
520
+ societario=societario,
521
+ patrimonial=patrimonial,
522
+ judicial=judicial,
523
+ previsional=previsional,
524
+ academico=academico,
525
+ registro_civil=registro_civil,
526
+ billeteras_virtuales=billeteras,
527
+ compras_estatales=compras_estatales,
528
+ marcas_inpi=marcas_inpi,
529
+ timeline=timeline,
530
+ deudores_alimentarios=deudores,
531
+ monotributo_historial=monotributo,
532
+ licencia_conducir=licencia,
533
+ vinculos=[],
534
+ riesgo=riesgo,
535
+ ))
536
+
537
+ return PersonReport(
538
+ meta=ReportMeta(
539
+ report_id=uuid.uuid4().hex[:12],
540
+ generated_at=datetime.now(timezone.utc),
541
+ sources=[s for s in ["arca_afip", "bcra", "igj", "inpi", "bcra", "dnrpa", "poder_judicial",
542
+ "renaper", "anses", "cnv", "uif", "boletin_oficial", "boletines_provinciales",
543
+ "anses", "sinai", "inpi", "juba", "inhibiciones", "poder_judicial_provincial",
544
+ "siscop", "billeteras_virtuales", "ruido", "sgarhu", "contratar",
545
+ "colegios_profesionales", "renaper_facial", "archive_org", "padron_electoral",
546
+ "compras_estatales", "cnv"] if s in results],
547
+ ),
548
+ score=score,
549
+ identificacion=identificacion,
550
+ contacto=contacto,
551
+ domicilios=domicilios,
552
+ fiscal=fiscal,
553
+ financiero=financiero,
554
+ societario=societario,
555
+ patrimonial=patrimonial,
556
+ judicial=judicial,
557
+ previsional=previsional,
558
+ academico=academico,
559
+ registro_civil=registro_civil,
560
+ billeteras_virtuales=billeteras,
561
+ compras_estatales=compras_estatales,
562
+ marcas_inpi=marcas_inpi,
563
+ timeline=timeline,
564
+ deudores_alimentarios=deudores,
565
+ monotributo_historial=monotributo,
566
+ registro_conductores=licencia,
567
+ vinculos=vinculos,
568
+ riesgo=riesgo,
569
+ )
570
+
571
+
572
+ def build_company_report(cuit: str, data: dict, results: dict) -> CompanyReport:
573
+ """Construye reporte de empresa."""
574
+ # Similar a build_person_report pero para empresa
575
+ # ... implementaciΓ³n simplificada
576
+ return CompanyReport(
577
+ meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=[]),
578
+ score=compute_company_score(CompanyReport(
579
+ meta=ReportMeta(report_id="", generated_at=datetime.now(timezone.utc), sources=[]),
580
+ identificacion=CompanyIdentificacion(cuit=cuit, razon_social=""),
581
+ fiscal=DatosFiscales(cuit=cuit),
582
+ financiero=DatosFinancieros(bcra_situacion_actual=1, bcra_situacion_descripcion="Normal"),
583
+ societario=DatosSocietarios(),
584
+ judicial=DatosJudiciales(),
585
+ patrimonial=DatosPatrimoniales(),
586
+ billeteras_virtuales=DatosBilleterasVirtuales(),
587
+ riesgo=[],
588
+ )),
589
+ identificacion=CompanyIdentificacion(cuit=cuit, razon_social=""),
590
+ fiscal=DatosFiscales(cuit=cuit),
591
+ financiero=DatosFinancieros(bcra_situacion_actual=1, bcra_situacion_descripcion="Normal"),
592
+ societario=DatosSocietarios(),
593
+ judicial=DatosJudiciales(),
594
+ patrimonial=DatosPatrimoniales(),
595
+ billeteras_virtuales=DatosBilleterasVirtuales(),
596
+ riesgo=[],
597
+ )
598
+
599
+
600
+ def build_vehicle_report(dominio: str, data: dict, results: dict) -> VehicleReport:
601
+ """Construye reporte de vehΓ­culo."""
602
+ dnrpa = _safe_dict(data, "dnrpa")
603
+ infracciones = _safe_dict(data, "infracciones")
604
+ siniestros = _safe_dict(data, "siniestros")
605
+ sinais = _safe_dict(data, "sinais")
606
+
607
+ vehiculo = None
608
+ for v in _safe_list(dnrpa, "vehiculos"):
609
+ if isinstance(v, dict) and v.get("dominio") == dominio:
610
+ vehiculo = Vehiculo(
611
+ dominio=v.get("dominio"),
612
+ marca=v.get("marca"),
613
+ modelo=v.get("modelo"),
614
+ anio=v.get("anio"),
615
+ tipo=v.get("tipo"),
616
+ registro=v.get("registro"),
617
+ titular=v.get("titular"),
618
+ estado=v.get("estado"),
619
+ )
620
+ break
621
+
622
+ infracciones_list = []
623
+ for inf in _safe_list(infracciones, "infracciones"):
624
+ if isinstance(inf, dict):
625
+ infracciones_list.append(InfraccionTransito(
626
+ fecha=inf.get("fecha"),
627
+ tipo=inf.get("tipo"),
628
+ jurisdiccion=inf.get("jurisdiccion"),
629
+ monto=inf.get("monto"),
630
+ estado=inf.get("estado"),
631
+ ))
632
+
633
+ return VehicleReport(
634
+ meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=list(results.keys())),
635
+ vehiculo=vehiculo,
636
+ infracciones=infracciones_list,
637
+ siniestros=[],
638
+ riesgo=[],
639
+ )
640
+
641
+
642
+ def build_property_report(calle: str, numero: str, localidad: str, provincia: str, data: dict, results: dict) -> PropertyReport:
643
+ """Construye reporte de propiedad."""
644
+ arba_catastro = _safe_dict(data, "arba_catastro")
645
+ carto_arba = _safe_dict(data, "carto_arba")
646
+ bo_data = _safe_dict(data, "boletin_oficial")
647
+ bo_prov = _safe_dict(data, "boletin_oficial_pba")
648
+
649
+ inmuebles = []
650
+ for inm in _safe_list(arba_catastro, "inmuebles"):
651
+ if isinstance(inm, dict):
652
+ inmuebles.append(Inmueble(
653
+ partida=inm.get("partida"),
654
+ nomenclatura=inm.get("nomenclatura"),
655
+ direccion=inm.get("direccion"),
656
+ superficie=inm.get("superficie"),
657
+ valuacion=inm.get("valuacion"),
658
+ tipo=inm.get("tipo"),
659
+ ))
660
+
661
+ return PropertyReport(
662
+ meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=list(results.keys())),
663
+ direccion=f"{calle} {numero}, {localidad}, {provincia}",
664
+ inmuebles=inmuebles,
665
+ publicaciones_bo=[],
666
+ riesgo=[],
667
+ )
668
+
669
+
670
+ def build_group_report(cuit: str, data: dict, results: dict) -> GroupReport:
671
+ """Construye reporte de grupo econΓ³mico."""
672
+ persona = build_person_report(cuit, data, results)
673
+
674
+ # TODO: Empresas vinculadas, vehΓ­culos, inmuebles
675
+ return GroupReport(
676
+ meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=list(results.keys())),
677
+ score_grupo=compute_group_score(PersonReport(...), [], [], []), # Simplificado
678
+ persona=persona,
679
+ empresas=[],
680
+ vehiculos=[],
681
+ inmuebles=[],
682
+ )
app/reports/fallback.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fallback Logic β€” Aplica datos histΓ³ricos cuando scrapers fallan.
3
+ """
4
+ import logging
5
+ from typing import Any, Optional
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ SITUACIONES_BCRA = {
10
+ 1: "Normal",
11
+ 2: "Con seguimiento especial / Riesgo bajo",
12
+ 3: "Con problemas / Riesgo medio",
13
+ 4: "Con alto riesgo de insolvencia / Riesgo alto",
14
+ 5: "Irrecuperable",
15
+ 6: "Irrecuperable por disposiciΓ³n tΓ©cnica",
16
+ }
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ async def apply_fallbacks(
22
+ cuit: str,
23
+ results: dict,
24
+ cached_report: Optional[dict] = None,
25
+ ) -> dict:
26
+ """
27
+ Aplica fallbacks desde reporte cacheado anterior cuando scrapers fallan.
28
+
29
+ Args:
30
+ cuit: CUIT consultado
31
+ results: Resultados de scrapers actuales (puede tener errores)
32
+ cached_report: Reporte cacheado anterior (ya parseado a dict)
33
+
34
+ Returns:
35
+ dict con datos combinados (actuales + fallback)
36
+ """
37
+ if not cached_report:
38
+ logger.debug(f"No cached report for fallback: {cuit}")
39
+ return {r.name: r.data for r in results.values() if r.data}
40
+
41
+ merged = {}
42
+
43
+ for name, result in results.items():
44
+ if result.data:
45
+ merged[name] = result.data
46
+ continue
47
+
48
+ # Scraper fallΓ³ - intentar fallback
49
+ cached_key = name
50
+ if name in cached_report:
51
+ cached_data = cached_report[name]
52
+ if cached_data:
53
+ logger.info(f"[FALLBACK] Using cached data for {name} ({cuit})")
54
+ merged[name] = cached_data
55
+ else:
56
+ merged[name] = {}
57
+ else:
58
+ merged[name] = {}
59
+
60
+ # Fallback especΓ­fico BCRA con lΓ³gica de negocio
61
+ if "bcra" not in merged or not merged["bcra"]:
62
+ merged["bcra"] = _bcra_fallback(cached_report)
63
+
64
+ return merged
65
+
66
+
67
+ def _bcra_fallback(cached_report: Optional[dict]) -> dict:
68
+ """Aplica lΓ³gica de fallback especΓ­fica para BCRA."""
69
+ if not cached_report or "bcra" not in cached_report:
70
+ return {
71
+ "denominacion": "",
72
+ "bcra_situacion_actual": 1,
73
+ "bcra_situacion_descripcion": SITUACIONES_BCRA[1],
74
+ "bcra_historial": [],
75
+ "bcra_total_deuda_miles": 0.0,
76
+ "bcra_dias_atraso_max": 0,
77
+ "cheques_rechazados": [],
78
+ "tiene_deuda": False,
79
+ }
80
+
81
+ old = cached_report["bcra"]
82
+ if not isinstance(old, dict):
83
+ return {"bcra_situacion_actual": 1}
84
+
85
+ # Si la situaciΓ³n histΓ³rica es peor (mayor nΓΊmero), usar esa
86
+ # Si la actual fallΓ³, usar la histΓ³rica
87
+ situacion_actual = old.get("bcra_situacion_actual", 1)
88
+
89
+ return {
90
+ "denominacion": old.get("denominacion", ""),
91
+ "bcra_situacion_actual": situacion_actual,
92
+ "bcra_situacion_descripcion": SITUACIONES_BCRA.get(situacion_actual, "Normal"),
93
+ "bcra_historial": old.get("bcra_historial", []),
94
+ "bcra_total_deuda_miles": old.get("bcra_total_deuda_miles", 0.0),
95
+ "bcra_dias_atraso_max": old.get("bcra_dias_atraso_max", 0),
96
+ "cheques_rechazados": old.get("cheques_rechazados", []),
97
+ "tiene_deuda": old.get("tiene_deuda", False),
98
+ }
99
+
100
+
101
+ async def apply_fallbacks_to_person_report(
102
+ cuit: str,
103
+ raw_results: dict,
104
+ cached_report: Optional[dict] = None,
105
+ ) -> dict:
106
+ """
107
+ Aplica fallbacks a resultados de reporte persona.
108
+ Retorna datos listos para build_person_report.
109
+ """
110
+ merged = await apply_fallbacks(cuit, raw_results, cached_report)
111
+ return merged
app/reports/kyc_service.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Servicio KYC (Know Your Customer) Simplificado β€” CrowData.
3
+
4
+ Combina datos de RENAPER, BCRA y AFIP para ofrecer verificaciΓ³n
5
+ de identidad simplificada para empresas de fintech, inmobiliarias, etc.
6
+
7
+ NOTA: Este es un KYC simplificado basado en datos pΓΊblicos.
8
+ No reemplaza la debida diligencia legal requerida por normativas.
9
+ """
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+ from typing import Optional
13
+ from app.scrapers.arca_afip import ArcaAfipScraper
14
+ from app.scrapers.bcra import BcraScraper
15
+ from app.scrapers.anses import AnsesScraper
16
+ from app.scrapers.renaper import RenaperScraper
17
+ from app.scrapers.renaper_facial import RenaperFacialScraper
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class KYCResult:
24
+ """Resultado de verificaciΓ³n KYC simplificada."""
25
+ cuit: str
26
+ nombre_completo: str = ""
27
+ dni: str = ""
28
+ estado_civil: str = ""
29
+ fecha_nacimiento: str = ""
30
+ sexo: str = ""
31
+ nacionalidad: str = ""
32
+
33
+ # Verificaciones
34
+ verificado_renaper: bool = False
35
+ verificado_afip: bool = False
36
+ verificado_bcra: bool = False
37
+ verificado_anses: bool = False
38
+
39
+ # Riesgo
40
+ nivel_riesgo: str = "BAJO"
41
+ score_kyc: int = 0
42
+ observaciones: list = field(default_factory=list)
43
+
44
+ # Datos financieros
45
+ situacion_bcra: str = ""
46
+ estado_afip: str = ""
47
+ condicion_iva: str = ""
48
+ categoria_monotributo: str = ""
49
+
50
+ # Fuentes consultadas
51
+ fuentes_consultadas: list = field(default_factory=list)
52
+
53
+
54
+ class KYCService:
55
+ """Servicio KYC simplificado."""
56
+
57
+ async def verify(self, cuit: str) -> KYCResult:
58
+ """
59
+ Realiza verificaciΓ³n KYC simplificada.
60
+ Retorna resultado con verificaciones y nivel de riesgo.
61
+ """
62
+ cuit_clean = cuit.replace("-", "").strip()
63
+ if len(cuit_clean) != 11 or not cuit_clean.isdigit():
64
+ return KYCResult(
65
+ cuit=cuit,
66
+ nivel_riesgo="ALTO",
67
+ observaciones=["CUIT invΓ‘lido"],
68
+ )
69
+
70
+ result = KYCResult(cuit=cuit)
71
+ score = 0
72
+
73
+ # 1. Verificar con RENAPER
74
+ try:
75
+ renaper = RenaperScraper()
76
+ renaper_data = await renaper.fetch(cuit_clean)
77
+ if renaper_data and renaper_data.get("dni"):
78
+ result.verificado_renaper = True
79
+ result.dni = renaper_data.get("dni", "")
80
+ result.nombre_completo = renaper_data.get("nombre_completo", "")
81
+ result.fecha_nacimiento = renaper_data.get("fecha_nacimiento", "")
82
+ result.sexo = renaper_data.get("sexo", "")
83
+ result.nacionalidad = renaper_data.get("nacionalidad", "ARGENTINA")
84
+ score += 30
85
+ result.fuentes_consultadas.append("RENAPER")
86
+ except Exception as e:
87
+ logger.debug(f"[KYC] RENAPER fallΓ³: {e}")
88
+
89
+ # 2. Verificar con ARCA/AFIP
90
+ try:
91
+ arca = ArcaAfipScraper()
92
+ arca_data = await arca.fetch(cuit_clean)
93
+ if arca_data and arca_data.get("estado"):
94
+ result.verificado_afip = True
95
+ result.estado_afip = arca_data.get("estado", "")
96
+ result.condicion_iva = arca_data.get("condicion_iva", "")
97
+ result.categoria_monotributo = arca_data.get("monotributo", {}).get("categoria", "")
98
+ if not result.nombre_completo and arca_data.get("nombre"):
99
+ result.nombre_completo = f"{arca_data.get('apellido', '')} {arca_data.get('nombre', '')}".strip()
100
+ score += 25
101
+ result.fuentes_consultadas.append("ARCA/AFIP")
102
+ except Exception as e:
103
+ logger.debug(f"[KYC] ARCA fallΓ³: {e}")
104
+
105
+ # 3. Verificar con BCRA
106
+ try:
107
+ bcra = BcraScraper()
108
+ bcra_data = await bcra.fetch(cuit_clean)
109
+ if bcra_data:
110
+ result.verificado_bcra = True
111
+ result.situacion_bcra = bcra_data.get("situacion", "")
112
+ score += 25
113
+ result.fuentes_consultadas.append("BCRA")
114
+
115
+ # Evaluar riesgo financiero
116
+ situacion = bcra_data.get("situacion_normal", 0)
117
+ if situacion and situacion > 3:
118
+ result.nivel_riesgo = "ALTO"
119
+ result.observaciones.append(f"BCRA situaciΓ³n: {situacion}/5")
120
+ elif situacion and situacion > 1:
121
+ result.nivel_riesgo = "MEDIO"
122
+ result.observaciones.append(f"BCRA situaciΓ³n: {situacion}/5")
123
+ except Exception as e:
124
+ logger.debug(f"[KYC] BCRA fallΓ³: {e}")
125
+
126
+ # 4. Verificar con ANSES
127
+ try:
128
+ anses = AnsesScraper()
129
+ anses_data = await anses.fetch(cuit_clean)
130
+ if anses_data and anses_data.get("resultado"):
131
+ result.verificado_anses = True
132
+ score += 20
133
+ result.fuentes_consultadas.append("ANSES")
134
+ except Exception as e:
135
+ logger.debug(f"[KYC] ANSES fallΓ³: {e}")
136
+
137
+ # Calcular score final
138
+ result.score_kyc = min(score, 100)
139
+
140
+ # Determinar nivel de riesgo basado en score
141
+ if result.score_kyc >= 70:
142
+ result.nivel_riesgo = "BAJO"
143
+ elif result.score_kyc >= 40:
144
+ result.nivel_riesgo = "MEDIO"
145
+ else:
146
+ result.nivel_riesgo = "ALTO"
147
+
148
+ # Agregar observaciones
149
+ if not result.verificado_renaper:
150
+ result.observaciones.append("No verificado por RENAPER")
151
+ if not result.verificado_afip:
152
+ result.observaciones.append("No verificado por ARCA/AFIP")
153
+ if not result.verificado_bcra:
154
+ result.observaciones.append("No verificado por BCRA")
155
+
156
+ return result
app/reports/models.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, String, Text, DateTime, Boolean, Integer
2
+ from sqlalchemy.sql import func
3
+ from app.database import Base
4
+
5
+
6
+ class ReportCache(Base):
7
+ __tablename__ = "report_cache"
8
+
9
+ id = Column(String(36), primary_key=True)
10
+ cache_key = Column(String(255), unique=True, index=True, nullable=False)
11
+ report_type = Column(String(50), nullable=False) # persona | empresa | vehiculo
12
+ identifier = Column(String(100), nullable=False, index=True) # cuit / dominio
13
+ data = Column(Text, nullable=False) # JSON string
14
+ sources_used = Column(String(500)) # comma-separated sources
15
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
16
+ expires_at = Column(DateTime(timezone=True))
17
+ hit_count = Column(Integer, default=0)
18
+
19
+
20
+ class SearchHistory(Base):
21
+ __tablename__ = "search_history"
22
+
23
+ id = Column(Integer, primary_key=True, index=True)
24
+ user_id = Column(String(36), index=True, nullable=False)
25
+ identifier = Column(String(100), nullable=False) # CUIT / Dominio
26
+ type = Column(String(50)) # persona | empresa | vehiculo
27
+ name = Column(String(255)) # Razon social / Nombre
28
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
29
+ report_id = Column(String(36)) # Link to cache
30
+
31
+
32
+ class MonitorTask(Base):
33
+ __tablename__ = "monitor_tasks"
34
+
35
+ id = Column(Integer, primary_key=True, index=True)
36
+ user_id = Column(String(36), index=True, nullable=False)
37
+ identifier = Column(String(100), nullable=False) # CUIT / Dominio
38
+ type = Column(String(50)) # persona | empresa | vehiculo
39
+ last_check = Column(DateTime(timezone=True), server_default=func.now())
40
+ active = Column(Boolean, default=True)
41
+ alert_count = Column(Integer, default=0)
42
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
app/reports/orchestrator.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ orchestrator.py β€” Parallel orchestrator for report generation.
3
+ Executes scrapers in waves respecting dependencies, with timeout and circuit breaker.
4
+ """
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from dataclasses import dataclass
9
+ from typing import Any, Optional
10
+ from datetime import datetime, timezone
11
+
12
+ from app.reports.registry import registry, ScraperDef
13
+ from app.utils.circuit_breaker import get_circuit_breaker, CircuitBreakerConfig, CircuitState
14
+ from app.utils.telemetry import record_scraper_result
15
+ from app.cache.redis_client import cache_get, cache_set
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ScraperResult:
22
+ """Resultado de ejecuciΓ³n de un scraper."""
23
+ name: str
24
+ status: str # "ok", "empty", "error", "blocked", "timeout", "skipped"
25
+ latency_ms: float
26
+ records_found: int = 0
27
+ data: Any = None
28
+ error: Optional[str] = None
29
+ circuit_open: bool = False
30
+
31
+
32
+ class ScraperOrchestrator:
33
+ """
34
+ Orquestador de ejecuciΓ³n paralela de scrapers.
35
+ - Ejecuta en waves respetando dependencias
36
+ - Maneja timeouts por scraper
37
+ - Integra circuit breaker
38
+ - Soporta cachΓ© (hit/miss) y fallback
39
+ """
40
+
41
+ def __init__(self, report_type: str, identifier: str, force_refresh: bool = False):
42
+ self.report_type = report_type
43
+ self.identifier = identifier
44
+ self.force_refresh = force_refresh
45
+ self.cache_prefix = f"{report_type}:{identifier}"
46
+ self.start_time = time.time()
47
+ self.results: dict[str, ScraperResult] = {}
48
+
49
+ def _get_cache_key(self, scraper_name: str) -> str:
50
+ return f"{self.cache_prefix}:{scraper_name}"
51
+
52
+ async def _check_cache(self, scraper: ScraperDef) -> Optional[Any]:
53
+ """Verifica cachΓ© si no es force_refresh."""
54
+ if self.force_refresh:
55
+ return None
56
+ key = self._get_cache_key(scraper.name)
57
+ return await cache_get(key)
58
+
59
+ async def _save_cache(self, scraper: ScraperDef, data: Any) -> None:
60
+ if data:
61
+ key = self._get_cache_key(scraper.name)
62
+ await cache_set(key, data, ttl=86400) # 24h TTL
63
+
64
+ def _get_circuit_breaker(self, scraper: ScraperDef):
65
+ return get_circuit_breaker(
66
+ scraper.name,
67
+ CircuitBreakerConfig(
68
+ failure_threshold=5,
69
+ timeout=60.0,
70
+ excluded_exceptions=(),
71
+ )
72
+ )
73
+
74
+ async def _execute_single(self, scraper: ScraperDef) -> ScraperResult:
75
+ """Ejecuta un solo scraper con circuit breaker y timeout."""
76
+ # Check circuit breaker
77
+ cb = self._get_circuit_breaker(scraper)
78
+ if cb.state == CircuitState.OPEN:
79
+ logger.warning(f"Circuit breaker OPEN for {scraper.name}, skipping")
80
+ return ScraperResult(
81
+ name=scraper.name, status="skipped", latency_ms=0,
82
+ error="Circuit breaker open", circuit_open=True
83
+ )
84
+
85
+ # Check cache first
86
+ cached = await self._check_cache(scraper)
87
+ if cached is not None:
88
+ logger.debug(f"Cache HIT for {scraper.name}")
89
+ return ScraperResult(
90
+ name=scraper.name, status="ok", latency_ms=1,
91
+ data=cached, records_found=len(cached) if isinstance(cached, list) else 1
92
+ )
93
+
94
+ # Execute with circuit breaker
95
+ start = time.time()
96
+ try:
97
+ async def _fetch():
98
+ return await scraper.instance.safe_fetch(self.identifier)
99
+
100
+ result = await asyncio.wait_for(
101
+ cb.call(_fetch),
102
+ timeout=scraper.timeout
103
+ )
104
+
105
+ latency = (time.time() - start) * 1000
106
+
107
+ # Record success in circuit breaker
108
+ async with cb._lock:
109
+ cb._failure_count = 0
110
+ if cb._state == CircuitState.HALF_OPEN:
111
+ cb._success_count += 1
112
+ if cb._success_count >= cb.config.success_threshold:
113
+ cb._state = CircuitState.CLOSED
114
+ cb._success_count = 0
115
+
116
+ # Save to cache
117
+ await self._save_cache(scraper, result)
118
+
119
+ records = 0
120
+ if result:
121
+ if isinstance(result, list):
122
+ records = len(result)
123
+ elif isinstance(result, dict):
124
+ list_keys = [k for k, v in result.items() if isinstance(v, list)]
125
+ records = sum(len(result[k]) for k in list_keys) if list_keys else 1
126
+ else:
127
+ records = 1
128
+
129
+ status = "ok" if records > 0 else "empty"
130
+ record_scraper_result(scraper.name, status, latency, records_found=records)
131
+
132
+ return ScraperResult(
133
+ name=scraper.name, status=status, latency_ms=latency,
134
+ records_found=records, data=result
135
+ )
136
+
137
+ except asyncio.TimeoutError:
138
+ return ScraperResult(
139
+ name=scraper.name, status="timeout",
140
+ latency_ms=(time.time() - start) * 1000,
141
+ error=f"Timeout after {scraper.timeout}s"
142
+ )
143
+ except Exception as e:
144
+ return ScraperResult(
145
+ name=scraper.name, status="error",
146
+ latency_ms=(time.time() - start) * 1000,
147
+ error=str(e)
148
+ )
149
+
150
+ async def execute_wave(self, scrapers: list[ScraperDef]) -> list[ScraperResult]:
151
+ """Ejecuta una wave de scrapers en paralelo."""
152
+ logger.info(f"Executing wave with {len(scrapers)} scrapers: {[s.name for s in scrapers]}")
153
+
154
+ tasks = [self._execute_single(s) for s in scrapers]
155
+ results = await asyncio.gather(*tasks, return_exceptions=True)
156
+
157
+ results_list = []
158
+ for i, result in enumerate(results):
159
+ if isinstance(result, Exception):
160
+ logger.error(f"Scraper {scrapers[i].name} failed with exception: {result}")
161
+ results_list.append(ScraperResult(
162
+ name=scrapers[i].name, status="error", latency_ms=0,
163
+ error=str(result)
164
+ ))
165
+ else:
166
+ results_list.append(result)
167
+
168
+ return results_list
169
+
170
+ async def run(self) -> dict[str, ScraperResult]:
171
+ """Ejecuta todos los scrapers segΓΊn waves de dependencias.
172
+
173
+ Aplica un tope duro global de 10 minutos (asyncio.wait_for). Si Render
174
+ free-tier (Cloudflare corta a ~80s en CDN) o algΓΊn scraper
175
+ extremadamente lento no termina en ese plazo, cortamos y devolvemos
176
+ el reporte parcial con los scrapers que sΓ­ terminaron. Los
177
+ scrapers individuales siguen respetando SAFE_FETCH_TIMEOUT y
178
+ max_retries (esos NO se modifican).
179
+ """
180
+ ORCHESTRATOR_TIMEOUT = 600 # 10 min tope global (NO bajar - respetar scrapers)
181
+
182
+ waves = registry.get_execution_groups()
183
+ logger.info(f"Starting orchestration for {self.report_type}:{self.identifier} "
184
+ f"({len(waves)} waves, {len(registry.all())} total scrapers)")
185
+
186
+ async def _run_all_waves():
187
+ for wave_idx, wave in enumerate(waves):
188
+ active_scrapers = [s for s in wave if self._is_active_for_report(s)]
189
+ if not active_scrapers:
190
+ continue
191
+
192
+ wave_results = await self.execute_wave(active_scrapers)
193
+ for r in wave_results:
194
+ self.results[r.name] = r
195
+
196
+ logger.info(f"Wave {wave_idx + 1}/{len(waves)} completed: "
197
+ f"{sum(1 for r in wave_results if r.status in ('ok', 'empty'))}/{len(wave_results)} ok")
198
+
199
+ try:
200
+ await asyncio.wait_for(_run_all_waves(), timeout=ORCHESTRATOR_TIMEOUT)
201
+ except asyncio.TimeoutError:
202
+ logger.warning(
203
+ f"Orchestration for {self.report_type}:{self.identifier} timed out "
204
+ f"despuΓ©s de {ORCHESTRATOR_TIMEOUT}s. Devolviendo reporte parcial con "
205
+ f"{len([r for r in self.results.values() if r.status in ('ok', 'empty')])}/{len(self.results)} scrapers OK."
206
+ )
207
+ # Marcar scrapers que quedaron sin terminar como 'timeout' para que se vean en meta.failures.
208
+ executed_names = {r.name for r in self.results.values()}
209
+ for s in registry.all():
210
+ if self._is_active_for_report(s) and s.name not in executed_names:
211
+ self.results[s.name] = ScraperResult(
212
+ name=s.name, status="timeout", latency_ms=0,
213
+ error=f"Orchestrator timeout ({ORCHESTRATOR_TIMEOUT}s): scraper no ejecutado"
214
+ )
215
+
216
+ total_time = (time.time() - self.start_time) * 1000
217
+ logger.info(f"Orchestration completed in {total_time:.0f}ms ({len(self.results)} scrapers)")
218
+
219
+ return self.results
220
+
221
+ def _is_active_for_report(self, scraper: ScraperDef) -> bool:
222
+ """Determina si un scraper aplica para este tipo de reporte."""
223
+ # Por ahora todos activos; se puede especializar por report_type
224
+ return True
225
+
226
+ def get_summary(self) -> dict:
227
+ """Resumen de resultados para logging/mΓ©tricas."""
228
+ ok = sum(1 for r in self.results.values() if r.status == "ok")
229
+ empty = sum(1 for r in self.results.values() if r.status == "empty")
230
+ errors = sum(1 for r in self.results.values() if r.status == "error")
231
+ timeouts = sum(1 for r in self.results.values() if r.status == "timeout")
232
+ blocked = sum(1 for r in self.results.values() if r.status == "blocked")
233
+ skipped = sum(1 for r in self.results.values() if r.status == "skipped")
234
+
235
+ return {
236
+ "total": len(self.results),
237
+ "ok": ok,
238
+ "empty": empty,
239
+ "errors": errors,
240
+ "timeouts": timeouts,
241
+ "blocked": blocked,
242
+ "skipped": skipped,
243
+ "total_latency_ms": (time.time() - self.start_time) * 1000,
244
+ }
app/reports/registry.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Scraper Registry β€” Central configuration registry.py β€” Registry of all available scrapers for report generation.
3
+ Centralizes scraper definitions and metadata.
4
+ """
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Any, Optional
7
+ from enum import Enum
8
+
9
+
10
+ class ScraperCategory(Enum):
11
+ """CategorΓ­as de scrapers por criticidad y tipo."""
12
+ CORE = "core" # CrΓ­ticos: ARCA, BCRA, DNRPA, PJ, RENAPER, IGJ, INPI, UIF
13
+ FINANCIAL = "financial" # ARBA, Boletines, ANSES, CNV, COMPR.AR, Monotributo, Registro Conductores
14
+ JUDICIAL = "judicial" # Infracciones, Deudores Alimentarios, JUBA, Inhibiciones
15
+ PATRIMONIAL = "patrimonial" # Catastro, CartografΓ­a, SINAI, SISCOP
16
+ OSINT = "osint" # Google Images, Redes Sociales, TelefonΓ­a, Name Search
17
+ OTHER = "other" # Billeteras, Ruido, SGARHU, CONTRATAR, Colegios, Timeline BO, Archive.org
18
+
19
+
20
+ @dataclass
21
+ class ScraperDef:
22
+ """DefiniciΓ³n de un scraper con metadatos."""
23
+ name: str # Nombre ΓΊnico (ej: "bcra")
24
+ display_name: str # Nombre legible (ej: "BCRA")
25
+ category: ScraperCategory
26
+ instance: Any # Instancia del scraper
27
+ timeout: float = 60.0 # Timeout en segundos
28
+ critical: bool = False # Si falla, ΒΏes crΓ­tico para el reporte?
29
+ dependencies: list[str] = None # Nombres de scrapers que deben ejecutarse antes
30
+ fallback_supported: bool = True # Si se puede usar cachΓ© como fallback
31
+
32
+ def __post_init__(self):
33
+ if self.dependencies is None:
34
+ self.dependencies = []
35
+
36
+
37
+ class ScraperRegistry:
38
+ """Registro central de todos los scrapers disponibles."""
39
+
40
+ def __init__(self):
41
+ self._scrapers: dict[str, ScraperDef] = {}
42
+ self._categories: dict[ScraperCategory, list[str]] = {c: [] for c in ScraperCategory}
43
+
44
+ def register(self, defn: ScraperDef) -> None:
45
+ """Registra un scraper."""
46
+ self._scrapers[defn.name] = defn
47
+ self._categories[defn.category].append(defn.name)
48
+
49
+ def get(self, name: str) -> Optional[ScraperDef]:
50
+ return self._scrapers.get(name)
51
+
52
+ def all(self) -> list[ScraperDef]:
53
+ return list(self._scrapers.values())
54
+
55
+ def by_category(self, category: ScraperCategory) -> list[ScraperDef]:
56
+ return [self._scrapers[n] for n in self._categories[category]]
57
+
58
+ def critical(self) -> list[ScraperDef]:
59
+ return [s for s in self._scrapers.values() if s.critical]
60
+
61
+ def ordered_by_dependencies(self) -> list[ScraperDef]:
62
+ """Ordena scrapers respetando dependencias (topological sort simplificado)."""
63
+ # Para simplificar: critical first, luego por category priority
64
+ priority = {
65
+ ScraperCategory.CORE: 0,
66
+ ScraperCategory.FINANCIAL: 1,
67
+ ScraperCategory.JUDICIAL: 2,
68
+ ScraperCategory.PATRIMONIAL: 3,
69
+ ScraperCategory.OSINT: 4,
70
+ ScraperCategory.OTHER: 5,
71
+ }
72
+ return sorted(self.all(), key=lambda s: (not s.critical, priority[s.category]))
73
+
74
+ def get_execution_groups(self) -> list[list[ScraperDef]]:
75
+ """
76
+ Agrupa scrapers por waves de ejecuciΓ³n paralela.
77
+ Respeta dependencias: un scraper no se ejecuta hasta que sus dependencias terminen.
78
+ """
79
+ executed = set()
80
+ waves = []
81
+ remaining = {s.name: s for s in self.ordered_by_dependencies()}
82
+
83
+ while remaining:
84
+ wave = []
85
+ for name, s in list(remaining.items()):
86
+ deps_met = all(d in executed for d in s.dependencies)
87
+ if deps_met:
88
+ wave.append(s)
89
+ executed.add(name)
90
+
91
+ if not wave:
92
+ # Circular dependency or missing - force execute
93
+ wave.append(next(iter(remaining.values())))
94
+ executed.add(wave[0].name)
95
+
96
+ for s in wave:
97
+ remaining.pop(s.name, None)
98
+ waves.append(wave)
99
+
100
+ return waves
101
+
102
+
103
+ # Instancia global del registry
104
+ registry = ScraperRegistry()
app/reports/router.py ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Query, Response
2
+ from sqlalchemy import update
3
+ from app.reports.schemas import (
4
+ PersonReport, CompanyReport, VehicleReport, GroupReport,
5
+ PropertyReport, SearchPropertyRequest,
6
+ SearchPersonaRequest, SearchEmpresaRequest,
7
+ HistoryItem, MonitorItem,
8
+ )
9
+ from app.reports import service
10
+ from app.utils.pdf_generator import generate_report_pdf
11
+ from app.auth.router import current_active_user
12
+ from app.auth.models import User
13
+ from app.scrapers.base import AntiBotBlockedError
14
+ from app.utils.security import mask_cuit, mask_email
15
+ import logging
16
+ import re
17
+ from datetime import datetime
18
+
19
+ logger = logging.getLogger(__name__)
20
+ router = APIRouter(prefix="/reports", tags=["reports"])
21
+
22
+
23
+ from app.utils.dni_to_cuit import find_real_cuit
24
+
25
+ async def validate_cuit(cuit: str) -> str:
26
+ """Valida y normaliza formato CUIT/CUIL. Si es DNI, intenta resolver el CUIT real."""
27
+ clean = re.sub(r"[^0-9]", "", cuit)
28
+
29
+ if len(clean) in (7, 8):
30
+ real_cuit = await find_real_cuit(clean)
31
+ if real_cuit:
32
+ return real_cuit
33
+ else:
34
+ raise HTTPException(status_code=404, detail="No se encontrΓ³ un CUIT asociado a este DNI en AFIP.")
35
+
36
+ if len(clean) != 11:
37
+ raise HTTPException(status_code=422, detail="CUIT/CUIL invΓ‘lido. Debe tener 11 dΓ­gitos o ser un DNI vΓ‘lido.")
38
+ return clean
39
+
40
+
41
+ async def validate_cuit_empresa(cuit: str) -> str:
42
+ """Valida CUIT de empresa (debe tener prefijo 30 o 33)."""
43
+ cuit_clean = await validate_cuit(cuit)
44
+ prefijo = cuit_clean[:2]
45
+ if prefijo not in ("30", "33"):
46
+ raise HTTPException(
47
+ status_code=422,
48
+ detail=f"El CUIT {cuit_clean} no corresponde a una empresa (prefijo {prefijo}). "
49
+ "Los CUIT de empresas comienzan con 30 o 33."
50
+ )
51
+ return cuit_clean
52
+
53
+
54
+ @router.get("/public/persona/{cuit}", response_model=PersonReport, summary="Informe pΓΊblico (modelo/ejemplo)")
55
+ async def get_public_persona_report(cuit: str):
56
+ cuit_clean = await validate_cuit(cuit)
57
+ if cuit_clean != "20301234562":
58
+ raise HTTPException(status_code=403, detail="Acceso denegado. Este informe requiere suscripciΓ³n.")
59
+ report = await service.get_person_report(cuit_clean)
60
+ report.meta.cached = True
61
+ return report
62
+
63
+
64
+ @router.get("/persona/{cuit}", response_model=PersonReport, summary="Informe de Persona FΓ­sica")
65
+ async def get_persona_report(
66
+ cuit: str,
67
+ force_refresh: bool = Query(False, description="Si es true, ignora el cachΓ© y regenera el informe completo"),
68
+ skip_redes: bool = Query(False, description="Si es true, omite el scraper de redes sociales (OSINT)"),
69
+ current_user: User = Depends(current_active_user),
70
+ ):
71
+ """
72
+ Genera un informe completo de persona fΓ­sica consultando:
73
+ - ARCA/AFIP (datos fiscales)
74
+ - BCRA (situaciΓ³n crediticia)
75
+ - BoletΓ­n Oficial (publicaciones)
76
+ """
77
+ cuit_clean = await validate_cuit(cuit)
78
+
79
+ if current_user.credits <= 0 and current_user.plan == "free":
80
+ logger.info(f"Informe de muestra enviado por email a {mask_email(current_user.email)} para CUIT {mask_cuit(cuit_clean)}")
81
+ return {
82
+ "meta": {
83
+ "report_id": "limit_reached",
84
+ "generated_at": str(datetime.now()),
85
+ "message": "Se ha alcanzado el lΓ­mite de 1 bΓΊsqueda gratuita. Se ha enviado una muestra reducida por email.",
86
+ "action": "upgrade_plan",
87
+ "sample_sent_to": current_user.email
88
+ },
89
+ "identificacion": {"nota": "Para ver nombre, DNI y fecha de nacimiento adquirΓ­ un plan."},
90
+ "fiscal": {"nota": "Para ver actividades y situaciΓ³n en AFIP adquirΓ­ un plan."}
91
+ }
92
+
93
+ # Si force_refresh=True, eliminar cachΓ© antes de generar
94
+ if force_refresh:
95
+ from app.cache.redis_client import cache_delete
96
+ await cache_delete(f"persona:{cuit_clean}")
97
+ logger.info(f"[force_refresh] CachΓ© invalidado para persona:{mask_cuit(cuit_clean)}")
98
+
99
+ try:
100
+ from app.database import AsyncSessionLocal
101
+ report = await service.get_person_report(cuit_clean, skip_redes=skip_redes)
102
+ # Descontar crΓ©dito solo si no fue cacheado y es plan free
103
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
104
+ async with AsyncSessionLocal() as db:
105
+ await db.execute(
106
+ update(User)
107
+ .where(User.id == current_user.id)
108
+ .values(credits=User.credits - 1)
109
+ )
110
+ await db.commit()
111
+ logger.info(f"CrΓ©dito descontado para {mask_email(current_user.email)}. Nuevo saldo: {current_user.credits - 1}")
112
+
113
+ # Guardar en historial
114
+ await service.save_search_history(
115
+ user_id=str(current_user.id),
116
+ identifier=cuit_clean,
117
+ report_type="persona",
118
+ name=f"{report.identificacion.apellido or ''} {report.identificacion.nombres or ''}".strip(),
119
+ report_id=report.meta.report_id
120
+ )
121
+ return report
122
+ except AntiBotBlockedError as e:
123
+ logger.warning(f"Anti-Bot blocked during persona report: {e}")
124
+ raise HTTPException(status_code=503, detail="La fuente de datos estΓ‘ bloqueando consultas. IntentΓ‘ mΓ‘s tarde.")
125
+ except Exception as e:
126
+ logger.error(f"Error generating persona report for {cuit}: {e}", exc_info=True)
127
+ raise HTTPException(status_code=500, detail="Error al generar el informe. IntentΓ‘ nuevamente.")
128
+
129
+
130
+ @router.get("/empresa/{cuit}", response_model=CompanyReport, summary="Informe de Empresa")
131
+ async def get_empresa_report(
132
+ cuit: str,
133
+ force_refresh: bool = Query(False, description="Si es true, ignora el cachΓ© y regenera el informe completo"),
134
+ current_user: User = Depends(current_active_user),
135
+ ):
136
+ """
137
+ Genera un informe completo de persona jurΓ­dica consultando:
138
+ - ARCA/AFIP (datos fiscales)
139
+ - BCRA (situaciΓ³n crediticia)
140
+ - BoletΓ­n Oficial (publicaciones)
141
+ """
142
+ cuit_clean = await validate_cuit_empresa(cuit)
143
+
144
+ if current_user.credits <= 0 and current_user.plan == "free":
145
+ logger.info(f"Informe de muestra enviado por email a {mask_email(current_user.email)} para Empresa CUIT {mask_cuit(cuit_clean)}")
146
+ return {
147
+ "meta": {
148
+ "report_id": "limit_reached",
149
+ "generated_at": str(datetime.now()),
150
+ "message": "Se ha alcanzado el lΓ­mite de 1 bΓΊsqueda gratuita.",
151
+ "action": "upgrade_plan",
152
+ "sample_sent_to": current_user.email
153
+ },
154
+ "identificacion": {"nota": "Para ver razΓ³n social y fecha de constituciΓ³n adquirΓ­ un plan."}
155
+ }
156
+
157
+ try:
158
+ from app.database import AsyncSessionLocal
159
+ report = await service.get_company_report(cuit_clean)
160
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
161
+ async with AsyncSessionLocal() as db:
162
+ await db.execute(
163
+ update(User)
164
+ .where(User.id == current_user.id)
165
+ .values(credits=User.credits - 1)
166
+ )
167
+ await db.commit()
168
+
169
+ # Guardar en historial
170
+ await service.save_search_history(
171
+ user_id=str(current_user.id),
172
+ identifier=cuit_clean,
173
+ report_type="empresa",
174
+ name=report.identificacion.razon_social or cuit_clean,
175
+ report_id=report.meta.report_id
176
+ )
177
+ return report
178
+ except AntiBotBlockedError as e:
179
+ logger.warning(f"Anti-Bot blocked during empresa report: {e}")
180
+ raise HTTPException(status_code=503, detail="La fuente de datos estΓ‘ bloqueando consultas. IntentΓ‘ mΓ‘s tarde.")
181
+ except Exception as e:
182
+ logger.error(f"Error generating empresa report for {cuit}: {e}", exc_info=True)
183
+ raise HTTPException(status_code=500, detail="Error al generar el informe. IntentΓ‘ nuevamente.")
184
+
185
+
186
+ @router.get("/vehiculo/{dominio}", response_model=VehicleReport, summary="Obtener informe de vehΓ­culo")
187
+ async def get_vehiculo_report(
188
+ dominio: str,
189
+ current_user: User = Depends(current_active_user),
190
+ ):
191
+ try:
192
+ report = await service.get_vehicle_report(dominio)
193
+
194
+ # Cobrar crΓ©dito si no es cacheado
195
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
196
+ from app.database import AsyncSessionLocal
197
+ async with AsyncSessionLocal() as db:
198
+ await db.execute(
199
+ update(User)
200
+ .where(User.id == current_user.id)
201
+ .values(credits=User.credits - 1)
202
+ )
203
+ await db.commit()
204
+ logger.info(f"CrΓ©dito descontado para {mask_email(current_user.email)} (VehΓ­culo). Nuevo saldo: {current_user.credits - 1}")
205
+
206
+ # Guardar en historial
207
+ await service.save_search_history(
208
+ user_id=str(current_user.id),
209
+ identifier=dominio.upper(),
210
+ report_type="vehiculo",
211
+ name=f"{report.vehiculo.marca or ''} {report.vehiculo.modelo or ''}".strip() or "VehΓ­culo",
212
+ report_id=report.meta.report_id
213
+ )
214
+ return report
215
+ except AntiBotBlockedError as e:
216
+ logger.warning(f"Anti-Bot blocked during vehicle report: {e}")
217
+ raise HTTPException(status_code=503, detail="La fuente de datos estΓ‘ bloqueando consultas. IntentΓ‘ mΓ‘s tarde.")
218
+ except Exception as e:
219
+ logger.error(f"Error generating vehicle report for {dominio}: {e}", exc_info=True)
220
+ raise HTTPException(status_code=500, detail="Error al generar el informe. IntentΓ‘ nuevamente.")
221
+
222
+
223
+ @router.get("/vehiculo/{dominio}/pdf", summary="Descargar PDF de VehΓ­culo")
224
+ async def get_vehiculo_pdf(
225
+ dominio: str,
226
+ current_user: User = Depends(current_active_user),
227
+ ):
228
+ # Check credits for free users
229
+ if current_user.credits <= 0 and current_user.plan == "free":
230
+ raise HTTPException(status_code=429, detail="Alcanzaste el lΓ­mite de informes. ActualizΓ‘ tu plan para continuar.")
231
+
232
+ try:
233
+ report = await service.get_vehicle_report(dominio)
234
+
235
+ # Deduct credit for free users (only if not cached)
236
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
237
+ from app.database import AsyncSessionLocal
238
+ async with AsyncSessionLocal() as db:
239
+ await db.execute(
240
+ update(User)
241
+ .where(User.id == current_user.id)
242
+ .values(credits=User.credits - 1)
243
+ )
244
+ await db.commit()
245
+
246
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="vehiculo")
247
+ return Response(
248
+ content=bytes(pdf_bytes),
249
+ media_type="application/pdf",
250
+ headers={"Content-Disposition": f"attachment; filename=CrowData_Vehiculo_{dominio}.pdf"}
251
+ )
252
+ except HTTPException:
253
+ raise
254
+ except Exception as e:
255
+ logger.error(f"Error generating PDF for vehicle {dominio}: {e}")
256
+ raise HTTPException(status_code=500, detail="Error al generar PDF")
257
+
258
+
259
+ @router.get("/group/{cuit}", response_model=GroupReport, summary="Informe Consolidado de Grupo EconΓ³mico")
260
+ async def get_group_economic_report(
261
+ cuit: str,
262
+ current_user: User = Depends(current_active_user),
263
+ ):
264
+ try:
265
+ cuit_clean = await validate_cuit(cuit)
266
+ return await service.get_group_report(cuit_clean)
267
+ except Exception as e:
268
+ logger.error(f"Error generating group report: {e}")
269
+ raise HTTPException(status_code=500, detail="Error al consolidar grupo econΓ³mico")
270
+
271
+
272
+ @router.get("/group/{cuit}/pdf", summary="Descargar PDF de Grupo EconΓ³mico")
273
+ async def get_group_pdf(
274
+ cuit: str,
275
+ current_user: User = Depends(current_active_user),
276
+ ):
277
+ cuit_clean = await validate_cuit(cuit)
278
+ try:
279
+ report = await service.get_group_report(cuit_clean)
280
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="grupo")
281
+ return Response(
282
+ content=bytes(pdf_bytes),
283
+ media_type="application/pdf",
284
+ headers={"Content-Disposition": f"attachment; filename=CrowData_Grupo_{cuit_clean}.pdf"}
285
+ )
286
+ except Exception as e:
287
+ logger.error(f"Error generating group PDF for {cuit}: {e}")
288
+ raise HTTPException(status_code=500, detail="Error al generar PDF de grupo")
289
+
290
+
291
+ @router.post("/propiedad", response_model=PropertyReport, summary="Obtener informe de propiedad por direcciΓ³n")
292
+ async def get_property_report(
293
+ req: SearchPropertyRequest,
294
+ current_user: User = Depends(current_active_user),
295
+ ):
296
+ try:
297
+ report = await service.get_property_report(req.calle, req.numero, req.localidad, req.provincia)
298
+
299
+ # Cobrar crΓ©dito si no es cacheado
300
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
301
+ from app.database import AsyncSessionLocal
302
+ async with AsyncSessionLocal() as db:
303
+ await db.execute(
304
+ update(User)
305
+ .where(User.id == current_user.id)
306
+ .values(credits=User.credits - 1)
307
+ )
308
+ await db.commit()
309
+ logger.info(f"CrΓ©dito descontado para {mask_email(current_user.email)} (Propiedad). Nuevo saldo: {current_user.credits - 1}")
310
+
311
+ # Guardar en historial
312
+ await service.save_search_history(
313
+ user_id=str(current_user.id),
314
+ identifier=f"{req.calle} {req.numero}",
315
+ report_type="propiedad",
316
+ name=f"Inmueble: {req.calle} {req.numero}",
317
+ report_id=report.meta.report_id
318
+ )
319
+ return report
320
+ except AntiBotBlockedError as e:
321
+ logger.warning(f"Anti-Bot blocked during property search: {e}")
322
+ raise HTTPException(status_code=503, detail="La fuente de datos estΓ‘ bloqueando consultas. IntentΓ‘ mΓ‘s tarde.")
323
+ except Exception as e:
324
+ logger.error(f"Error searching property: {e}")
325
+ raise HTTPException(status_code=500, detail="Error al buscar propiedad")
326
+
327
+
328
+ @router.get("/search/persona", summary="Buscar Persona por nombre/DNI")
329
+ async def search_persona(
330
+ apellido: str | None = Query(None),
331
+ nombres: str | None = Query(None),
332
+ dni: str | None = Query(None),
333
+ cuit: str | None = Query(None),
334
+ provincia: str | None = Query(None),
335
+ current_user: User = Depends(current_active_user),
336
+ ):
337
+ """BΓΊsqueda de personas por apellido, nombre o DNI. Retorna lista de coincidencias bΓ‘sicas."""
338
+ if not any([apellido, nombres, dni, cuit]):
339
+ raise HTTPException(status_code=422, detail="IngresΓ‘ al menos un criterio de bΓΊsqueda.")
340
+
341
+ # Si viene CUIT directo de 11 dΓ­gitos, retornar como resultado directo
342
+ if cuit:
343
+ cuit_clean = re.sub(r"[^0-9]", "", cuit)
344
+ if len(cuit_clean) == 11:
345
+ # Retornar como resultado ΓΊnico para que el frontend navegue directamente
346
+ return {"results": [], "redirect_cuit": cuit_clean, "type": "direct"}
347
+
348
+ # Si viene DNI, intentar resolver directo a CUIT sin pasar por el buscador de nombres
349
+ if dni and not apellido and not nombres:
350
+ dni_clean = re.sub(r"[^0-9]", "", dni)
351
+ if len(dni_clean) in (7, 8):
352
+ real_cuit = await find_real_cuit(dni_clean)
353
+ if real_cuit:
354
+ return {"results": [], "redirect_cuit": real_cuit, "type": "direct"}
355
+
356
+ # BΓΊsqueda real usando NameSearchScraper
357
+ query = f"{apellido or ''} {nombres or ''}".strip() or dni
358
+ try:
359
+ from app.scrapers.name_search import NameSearchScraper
360
+ ns_scraper = NameSearchScraper()
361
+ results = await ns_scraper.fetch(query)
362
+ return {"results": results}
363
+ except Exception as e:
364
+ logger.error(f"Error in name search: {e}")
365
+ return {"results": [], "message": "Error en el motor de bΓΊsqueda."}
366
+
367
+
368
+ @router.get("/search/empresa", summary="Buscar Empresa por nombre/CUIT")
369
+ async def search_empresa(
370
+ razon_social: str | None = Query(None),
371
+ cuit: str | None = Query(None),
372
+ current_user: User = Depends(current_active_user),
373
+ ):
374
+ """BΓΊsqueda de empresas por razΓ³n social o CUIT."""
375
+ if not any([razon_social, cuit]):
376
+ raise HTTPException(status_code=422, detail="IngresΓ‘ razΓ³n social o CUIT.")
377
+
378
+ if cuit:
379
+ return {"redirect": f"/reports/empresa/{cuit}", "type": "direct"}
380
+
381
+ query = razon_social
382
+ try:
383
+ from app.scrapers.name_search import NameSearchScraper
384
+ ns_scraper = NameSearchScraper()
385
+ results = await ns_scraper.fetch(query)
386
+ return {"results": results}
387
+ except Exception as e:
388
+ logger.error(f"Error in company name search: {e}")
389
+ return {"results": [], "message": "Error en el motor de bΓΊsqueda."}
390
+
391
+
392
+ @router.get("/persona/{cuit}/pdf", summary="Descargar PDF de Persona")
393
+ async def get_persona_pdf(
394
+ cuit: str,
395
+ current_user: User = Depends(current_active_user),
396
+ force_refresh: bool = Query(False, description="Forzar regeneracion"),
397
+ ):
398
+ cuit_clean = await validate_cuit(cuit)
399
+
400
+ # Check credits for free users
401
+ if current_user.credits <= 0 and current_user.plan == "free":
402
+ raise HTTPException(status_code=429, detail="Alcanzaste el lΓ­mite de informes. ActualizΓ‘ tu plan para continuar.")
403
+
404
+ try:
405
+ if force_refresh:
406
+ from app.cache.redis_client import cache_delete
407
+ await cache_delete(f"persona:{cuit_clean}")
408
+ report = await service.get_person_report(cuit_clean)
409
+
410
+ # Deduct credit for free users (only if not cached)
411
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
412
+ from app.database import AsyncSessionLocal
413
+ async with AsyncSessionLocal() as db:
414
+ await db.execute(
415
+ update(User)
416
+ .where(User.id == current_user.id)
417
+ .values(credits=User.credits - 1)
418
+ )
419
+ await db.commit()
420
+
421
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="persona")
422
+ return Response(
423
+ content=bytes(pdf_bytes),
424
+ media_type="application/pdf",
425
+ headers={"Content-Disposition": f"attachment; filename=CrowData_Persona_{cuit_clean}.pdf"}
426
+ )
427
+ except HTTPException:
428
+ raise
429
+ except Exception as e:
430
+ logger.error(f"Error generating PDF for {cuit}: {e}")
431
+ raise HTTPException(status_code=500, detail="Error al generar PDF")
432
+
433
+
434
+ @router.get("/empresa/{cuit}/pdf", summary="Descargar PDF de Empresa")
435
+ async def get_empresa_pdf(
436
+ cuit: str,
437
+ current_user: User = Depends(current_active_user),
438
+ ):
439
+ cuit_clean = await validate_cuit(cuit)
440
+
441
+ # Check credits for free users
442
+ if current_user.credits <= 0 and current_user.plan == "free":
443
+ raise HTTPException(status_code=429, detail="Alcanzaste el lΓ­mite de informes. ActualizΓ‘ tu plan para continuar.")
444
+
445
+ try:
446
+ report = await service.get_company_report(cuit_clean)
447
+
448
+ # Deduct credit for free users (only if not cached)
449
+ if not getattr(report.meta, "cached", False) and current_user.plan == "free":
450
+ from app.database import AsyncSessionLocal
451
+ async with AsyncSessionLocal() as db:
452
+ await db.execute(
453
+ update(User)
454
+ .where(User.id == current_user.id)
455
+ .values(credits=User.credits - 1)
456
+ )
457
+ await db.commit()
458
+
459
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="empresa")
460
+ return Response(
461
+ content=bytes(pdf_bytes),
462
+ media_type="application/pdf",
463
+ headers={"Content-Disposition": f"attachment; filename=CrowData_Empresa_{cuit_clean}.pdf"}
464
+ )
465
+ except HTTPException:
466
+ raise
467
+ except Exception as e:
468
+ logger.error(f"Error generating PDF for {cuit}: {e}")
469
+ raise HTTPException(status_code=500, detail="Error al generar PDF")
470
+
471
+
472
+ @router.post("/persona/{cuit}/email-pdf", summary="Enviar PDF de Persona por email")
473
+ async def email_persona_pdf(
474
+ cuit: str,
475
+ current_user: User = Depends(current_active_user),
476
+ ):
477
+ """Genera el PDF de persona y lo envΓ­a por email al usuario."""
478
+ from app.utils.email_service import send_pdf_report
479
+ cuit_clean = await validate_cuit(cuit)
480
+ try:
481
+ report = await service.get_person_report(cuit_clean)
482
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="persona")
483
+ sent = await send_pdf_report(
484
+ to_email=current_user.email,
485
+ report_type="persona",
486
+ identifier=cuit_clean,
487
+ pdf_bytes=bytes(pdf_bytes),
488
+ user_name=current_user.full_name,
489
+ )
490
+ if sent:
491
+ return {"message": f"Informe enviado a {current_user.email}"}
492
+ raise HTTPException(status_code=500, detail="Error al enviar email")
493
+ except HTTPException:
494
+ raise
495
+ except Exception as e:
496
+ logger.error(f"Error emailing persona PDF for {mask_cuit(cuit)}: {e}")
497
+ raise HTTPException(status_code=500, detail="Error al generar o enviar PDF")
498
+
499
+
500
+ @router.post("/empresa/{cuit}/email-pdf", summary="Enviar PDF de Empresa por email")
501
+ async def email_empresa_pdf(
502
+ cuit: str,
503
+ current_user: User = Depends(current_active_user),
504
+ ):
505
+ """Genera el PDF de empresa y lo envΓ­a por email al usuario."""
506
+ from app.utils.email_service import send_pdf_report
507
+ cuit_clean = await validate_cuit(cuit)
508
+ try:
509
+ report = await service.get_company_report(cuit_clean)
510
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="empresa")
511
+ sent = await send_pdf_report(
512
+ to_email=current_user.email,
513
+ report_type="empresa",
514
+ identifier=cuit_clean,
515
+ pdf_bytes=bytes(pdf_bytes),
516
+ user_name=current_user.full_name,
517
+ )
518
+ if sent:
519
+ return {"message": f"Informe enviado a {current_user.email}"}
520
+ raise HTTPException(status_code=500, detail="Error al enviar email")
521
+ except HTTPException:
522
+ raise
523
+ except Exception as e:
524
+ logger.error(f"Error emailing empresa PDF for {mask_cuit(cuit)}: {e}")
525
+ raise HTTPException(status_code=500, detail="Error al generar o enviar PDF")
526
+
527
+
528
+ @router.post("/vehiculo/{dominio}/email-pdf", summary="Enviar PDF de VehΓ­culo por email")
529
+ async def email_vehiculo_pdf(
530
+ dominio: str,
531
+ current_user: User = Depends(current_active_user),
532
+ ):
533
+ """Genera el PDF de vehΓ­culo y lo envΓ­a por email al usuario."""
534
+ from app.utils.email_service import send_pdf_report
535
+ try:
536
+ report = await service.get_vehicle_report(dominio.upper())
537
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="vehiculo")
538
+ sent = await send_pdf_report(
539
+ to_email=current_user.email,
540
+ report_type="vehiculo",
541
+ identifier=dominio.upper(),
542
+ pdf_bytes=bytes(pdf_bytes),
543
+ user_name=current_user.full_name,
544
+ )
545
+ if sent:
546
+ return {"message": f"Informe enviado a {current_user.email}"}
547
+ raise HTTPException(status_code=500, detail="Error al enviar email")
548
+ except HTTPException:
549
+ raise
550
+ except Exception as e:
551
+ logger.error(f"Error emailing vehiculo PDF for {dominio}: {e}")
552
+ raise HTTPException(status_code=500, detail="Error al generar o enviar PDF")
553
+
554
+
555
+ @router.get("/propiedad/pdf", summary="Descargar PDF de Propiedad por direcciΓ³n")
556
+ async def get_propiedad_pdf(
557
+ calle: str = Query(..., description="Nombre de la calle"),
558
+ numero: str = Query(..., description="NΓΊmero de puerta"),
559
+ localidad: str = Query("", description="Localidad"),
560
+ provincia: str = Query("", description="Provincia"),
561
+ current_user: User = Depends(current_active_user),
562
+ ):
563
+ """Genera el PDF de propiedad y lo retorna como descarga."""
564
+ try:
565
+ report = await service.get_property_report(calle, numero, localidad, provincia)
566
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="propiedad")
567
+ identifier = f"{calle} {numero}"
568
+ return Response(
569
+ content=bytes(pdf_bytes),
570
+ media_type="application/pdf",
571
+ headers={"Content-Disposition": f"attachment; filename=CrowData_Propiedad_{identifier.replace(' ', '_')}.pdf"}
572
+ )
573
+ except Exception as e:
574
+ logger.error(f"Error generating PDF for property {calle} {numero}: {e}")
575
+ raise HTTPException(status_code=500, detail="Error al generar PDF")
576
+
577
+
578
+ @router.post("/propiedad/email-pdf", summary="Enviar PDF de Propiedad por email")
579
+ async def email_propiedad_pdf(
580
+ body: dict,
581
+ current_user: User = Depends(current_active_user),
582
+ ):
583
+ """Genera el PDF de propiedad y lo envΓ­a por email al usuario."""
584
+ from app.utils.email_service import send_pdf_report
585
+ try:
586
+ calle = body.get("calle", "")
587
+ numero = body.get("numero", "")
588
+ localidad = body.get("localidad", "")
589
+ provincia = body.get("provincia", "")
590
+ report = await service.get_property_report(calle, numero, localidad, provincia)
591
+ identifier = f"{calle} {numero}"
592
+ pdf_bytes = generate_report_pdf(report.model_dump(), report_type="propiedad")
593
+ sent = await send_pdf_report(
594
+ to_email=current_user.email,
595
+ report_type="propiedad",
596
+ identifier=identifier,
597
+ pdf_bytes=bytes(pdf_bytes),
598
+ user_name=current_user.full_name,
599
+ )
600
+ if sent:
601
+ return {"message": f"Informe enviado a {current_user.email}"}
602
+ raise HTTPException(status_code=500, detail="Error al enviar email")
603
+ except HTTPException:
604
+ raise
605
+ except Exception as e:
606
+ logger.error(f"Error emailing propiedad PDF: {e}")
607
+ raise HTTPException(status_code=500, detail="Error al generar o enviar PDF")
608
+
609
+
610
+ @router.get("/empresa/{cuit}/contrataciones", summary="Contrataciones pΓΊblicas de una empresa")
611
+ async def get_contrataciones(
612
+ cuit: str,
613
+ current_user: User = Depends(current_active_user),
614
+ ):
615
+ """Consulta contrataciones pΓΊblicas (CONTRATAR/COMPR.AR) por CUIT vΓ­a datos.gob.ar."""
616
+ cuit_clean = await validate_cuit(cuit)
617
+ from app.reports.service import contratar_scraper
618
+ try:
619
+ result = await contratar_scraper.safe_fetch(cuit_clean)
620
+ return {"cuit": cuit_clean, "contrataciones": result}
621
+ except Exception as e:
622
+ logger.error(f"Error fetching contrataciones for {cuit}: {e}")
623
+ raise HTTPException(status_code=500, detail="Error al consultar contrataciones")
624
+
625
+
626
+ @router.get("/persona/{dni}/deudores-alimentarios", summary="Registro deudores alimentarios (PBA)")
627
+ async def get_deudores_alimentarios(
628
+ dni: str,
629
+ sexo: str = "M",
630
+ current_user: User = Depends(current_active_user),
631
+ ):
632
+ """Consulta el RDAM de Provincia de Buenos Aires por DNI y sexo."""
633
+ dni_clean = dni.replace("-", "").replace(".", "").strip()
634
+ if not dni_clean.isdigit() or len(dni_clean) < 7:
635
+ raise HTTPException(status_code=400, detail="DNI invΓ‘lido")
636
+ if sexo.upper() not in ("M", "F", "NB", "NC"):
637
+ raise HTTPException(status_code=400, detail="Sexo invΓ‘lido. Use M, F, NB o NC")
638
+ from app.reports.service import deudores_alimentarios_scraper
639
+ try:
640
+ result = await deudores_alimentarios_scraper.safe_fetch(dni_clean, sexo=sexo.upper())
641
+ return {"dni": dni_clean, "deudores_alimentarios": result}
642
+ except Exception as e:
643
+ logger.error(f"Error fetching deudores alimentarios for DNI {dni}: {e}")
644
+ raise HTTPException(status_code=500, detail="Error al consultar deudores alimentarios")
645
+
646
+
647
+ @router.get("/user/history", response_model=list[HistoryItem], summary="Historial de bΓΊsquedas")
648
+ async def get_history(
649
+ current_user: User = Depends(current_active_user),
650
+ ):
651
+ """Retorna el historial de bΓΊsquedas del usuario actual."""
652
+ try:
653
+ return await service.get_user_history(str(current_user.id))
654
+ except Exception as e:
655
+ logger.error(f"Error fetching history: {e}")
656
+ raise HTTPException(status_code=500, detail="Error al obtener historial")
657
+
658
+
659
+ @router.post("/user/monitor/{report_type}/{identifier}", summary="Activar monitoreo para un CUIT")
660
+ async def start_monitoring(
661
+ report_type: str,
662
+ identifier: str,
663
+ current_user: User = Depends(current_active_user),
664
+ ):
665
+ try:
666
+ await service.add_monitor_task(str(current_user.id), identifier, report_type)
667
+ return {"message": f"Monitoreo activado para {identifier}"}
668
+ except Exception as e:
669
+ logger.error(f"Error starting monitor: {e}")
670
+ raise HTTPException(status_code=500, detail="Error al activar monitoreo")
671
+
672
+
673
+ @router.get("/user/monitor", response_model=list[MonitorItem], summary="Lista de CUITs monitoreados")
674
+ async def get_monitored_items(
675
+ current_user: User = Depends(current_active_user),
676
+ ):
677
+ try:
678
+ return await service.get_monitor_tasks(str(current_user.id))
679
+ except Exception as e:
680
+ logger.error(f"Error fetching monitor list: {e}")
681
+ raise HTTPException(status_code=500, detail="Error al obtener lista de monitoreo")
682
+
683
+
684
+ # ─── TelemetrΓ­a y Salud de Scrapers ───────────────────────────────────────────
685
+
686
+ from app.utils.telemetry import get_system_health_summary
687
+
688
+
689
+ @router.get("/scrapers/health", summary="Estado de salud de los scrapers", tags=["telemetry"])
690
+ async def get_scrapers_health(
691
+ current_user: User = Depends(current_active_user),
692
+ ):
693
+ """
694
+ Retorna el estado de salud de los 31 scrapers disponibles en CrowData.
695
+
696
+ Incluye para cada fuente:
697
+ - Estado del ΓΊltimo run (ok / empty / error / blocked)
698
+ - Timestamp del ΓΊltimo run
699
+ - Tasa de Γ©xito en las ΓΊltimas 24h
700
+ - Latencia promedio
701
+ - Cantidad de registros encontrados
702
+
703
+ Solo disponible para usuarios autenticados.
704
+ """
705
+ try:
706
+ summary = get_system_health_summary()
707
+ return summary
708
+ except Exception as e:
709
+ logger.error(f"Error obteniendo health de scrapers: {e}")
710
+ raise HTTPException(status_code=500, detail="Error al obtener estado de scrapers")
711
+
712
+
713
+ @router.get("/scrapers/health/public", summary="Estado pΓΊblico de scrapers (resumen)", tags=["telemetry"])
714
+ async def get_scrapers_health_public():
715
+ """
716
+ VersiΓ³n pΓΊblica del estado de salud: muestra cuΓ‘ntos scrapers estΓ‘n operativos
717
+ sin detalles individuales (para mostrar en la landing page).
718
+ """
719
+ try:
720
+ summary = get_system_health_summary()
721
+ return {
722
+ "total_scrapers": summary["total_scrapers"],
723
+ "operativos": summary["operativos"],
724
+ "health_pct": summary["health_pct"],
725
+ }
726
+ except Exception as e:
727
+ logger.error(f"Error obteniendo health pΓΊblico: {e}")
728
+ raise HTTPException(status_code=500, detail="Error al obtener estado")
729
+
730
+
731
+ @router.get("/scrapers/alerts", summary="Alertas de scrapers con problemas", tags=["telemetry"])
732
+ async def get_scrapers_alerts(
733
+ current_user: User = Depends(current_active_user),
734
+ ):
735
+ """
736
+ Detecta scrapers con problemas consistentes:
737
+ - Errores activos mΓΊltiples
738
+ - Bloqueos antibot
739
+ - Tasa de Γ©xito baja (< 30%)
740
+ - Scrapers que nunca corrieron
741
+ """
742
+ from app.utils.telemetry import get_scrapers_alerts as _get_alerts
743
+ try:
744
+ return {"alerts": _get_alerts()}
745
+ except Exception as e:
746
+ logger.error(f"Error obteniendo alertas de scrapers: {e}")
747
+ raise HTTPException(status_code=500, detail="Error al obtener alertas")
748
+
749
+
750
+ @router.get("/boletin", summary="Buscar publicaciones en el BoletΓ­n Oficial")
751
+ async def search_boletin(
752
+ q: str = Query(..., min_length=1, description="TΓ©rmino de bΓΊsqueda"),
753
+ ):
754
+ """BΓΊsqueda standalone del BoletΓ­n Oficial de la NaciΓ³n."""
755
+ from app.scrapers.boletin_oficial import BoletinOficialScraper
756
+ scraper = BoletinOficialScraper()
757
+ try:
758
+ result = await scraper.safe_fetch(q)
759
+ publicaciones = result.get("publicaciones", []) if isinstance(result, dict) else []
760
+ return {"publicaciones": publicaciones, "total": len(publicaciones)}
761
+ except Exception as e:
762
+ logger.error(f"Error buscando en BO: {e}")
763
+ raise HTTPException(status_code=502, detail="Error consultando el BoletΓ­n Oficial.")
app/reports/schema_validators.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Validadores Pydantic para campos numΓ©ricos y crΓ­ticos
3
+ CorrecciΓ³n de auditorΓ­a tΓ©cnica - 05/07/2026
4
+
5
+ Agregar estos validadores a schemas.py para prevenir datos malformados
6
+ """
7
+ from pydantic import field_validator
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ # ═══════════════════════════════════════════════════════════════════════
14
+ # VALIDADORES PARA BcraHistorial
15
+ # ═══════════════════════════════════════════════════════════════════════
16
+
17
+ class BcraHistorialValidators:
18
+ """
19
+ Agregar estos validadores a la clase BcraHistorial en schemas.py
20
+ """
21
+
22
+ @field_validator('monto_deuda', mode='before')
23
+ @classmethod
24
+ def coerce_monto_deuda(cls, v):
25
+ """Convierte string a float si es necesario"""
26
+ if v is None:
27
+ return None
28
+ if isinstance(v, (int, float)):
29
+ return float(v)
30
+ if isinstance(v, str):
31
+ # Limpiar formato de moneda
32
+ clean = v.replace('$', '').replace(',', '').replace('.', '').replace('k', '000').strip()
33
+ try:
34
+ return float(clean) if clean else None
35
+ except ValueError:
36
+ logger.warning(f"No se pudo convertir monto_deuda: {v}")
37
+ return None
38
+ return None
39
+
40
+ @field_validator('situacion', mode='before')
41
+ @classmethod
42
+ def validate_situacion(cls, v):
43
+ """Valida que situaciΓ³n estΓ© entre 1-6"""
44
+ if v is None:
45
+ return 1 # Default Normal
46
+ try:
47
+ sit = int(v)
48
+ if 1 <= sit <= 6:
49
+ return sit
50
+ logger.warning(f"SituaciΓ³n BCRA fuera de rango: {sit}, usando 1 (Normal)")
51
+ return 1
52
+ except (ValueError, TypeError):
53
+ logger.warning(f"SituaciΓ³n BCRA invΓ‘lida: {v}, usando 1 (Normal)")
54
+ return 1
55
+
56
+ @field_validator('dias_atraso', mode='before')
57
+ @classmethod
58
+ def coerce_dias_atraso(cls, v):
59
+ """Convierte a int de forma segura"""
60
+ if v is None:
61
+ return None
62
+ try:
63
+ return int(v)
64
+ except (ValueError, TypeError):
65
+ logger.warning(f"dias_atraso invΓ‘lido: {v}")
66
+ return None
67
+
68
+
69
+ # ═══════════════════════════════════════════════════════════════════════
70
+ # VALIDADORES PARA ChequeRechazado
71
+ # ═══════════════════════════════════════════════════════════════════════
72
+
73
+ class ChequeRechazadoValidators:
74
+ """
75
+ Agregar estos validadores a la clase ChequeRechazado en schemas.py
76
+ """
77
+
78
+ @field_validator('monto', mode='before')
79
+ @classmethod
80
+ def coerce_monto(cls, v):
81
+ """Convierte string a float si es necesario"""
82
+ if v is None:
83
+ return None
84
+ if isinstance(v, (int, float)):
85
+ return float(v)
86
+ if isinstance(v, str):
87
+ clean = v.replace('$', '').replace(',', '').replace('.', '').strip()
88
+ try:
89
+ return float(clean) if clean else None
90
+ except ValueError:
91
+ logger.warning(f"No se pudo convertir monto de cheque: {v}")
92
+ return None
93
+ return None
94
+
95
+
96
+ # ═══════════════════════════════════════════════════════════════════════
97
+ # VALIDADORES PARA Inmueble
98
+ # ═══════════════════════════════════════════════════════════════════════
99
+
100
+ class InmuebleValidators:
101
+ """
102
+ Agregar estos validadores a la clase Inmueble en schemas.py
103
+ """
104
+
105
+ @field_validator('valuacion_fiscal', mode='before')
106
+ @classmethod
107
+ def coerce_valuacion_fiscal(cls, v):
108
+ """Convierte string a float si es necesario"""
109
+ if v is None:
110
+ return None
111
+ if isinstance(v, (int, float)):
112
+ return float(v)
113
+ if isinstance(v, str):
114
+ clean = v.replace('$', '').replace(',', '').replace('.', '').strip()
115
+ try:
116
+ return float(clean) if clean else None
117
+ except ValueError:
118
+ logger.warning(f"No se pudo convertir valuacion_fiscal: {v}")
119
+ return None
120
+ return None
121
+
122
+
123
+ # ═══════════════════════════════════════════════════════════════════════
124
+ # VALIDADORES PARA InfraccionTransito
125
+ # ═══════════════════════════════════════════════════════════════════════
126
+
127
+ class InfraccionTransitoValidators:
128
+ """
129
+ Agregar estos validadores a la clase InfraccionTransito en schemas.py
130
+ """
131
+
132
+ @field_validator('monto', mode='before')
133
+ @classmethod
134
+ def coerce_monto(cls, v):
135
+ """Convierte string a float si es necesario"""
136
+ if v is None:
137
+ return None
138
+ if isinstance(v, (int, float)):
139
+ return float(v)
140
+ if isinstance(v, str):
141
+ clean = v.replace('$', '').replace(',', '').replace('.', '').strip()
142
+ try:
143
+ return float(clean) if clean else None
144
+ except ValueError:
145
+ logger.warning(f"No se pudo convertir monto de infracciΓ³n: {v}")
146
+ return None
147
+ return None
148
+
149
+
150
+ # ═══════════════════════════════════════════════════════════════════════
151
+ # EJEMPLO DE USO EN schemas.py
152
+ # ═══════════════════════════════════════════════════════════════════════
153
+
154
+ """
155
+ # EN EL ARCHIVO schemas.py, AGREGAR:
156
+
157
+ from pydantic import field_validator
158
+ import logging
159
+
160
+ logger = logging.getLogger(__name__)
161
+
162
+ class BcraHistorial(BaseModel):
163
+ periodo: Optional[str] = None
164
+ situacion: Optional[int] = None
165
+ entidad: Optional[str] = None
166
+ monto_deuda: Optional[float] = None
167
+ denominacion: Optional[str] = None
168
+ dias_atraso: Optional[int] = None
169
+ # ... resto de campos
170
+
171
+ # VALIDADORES
172
+ @field_validator('monto_deuda', mode='before')
173
+ @classmethod
174
+ def coerce_monto_deuda(cls, v):
175
+ if v is None:
176
+ return None
177
+ if isinstance(v, (int, float)):
178
+ return float(v)
179
+ if isinstance(v, str):
180
+ clean = v.replace('$', '').replace(',', '').replace('.', '').replace('k', '000').strip()
181
+ try:
182
+ return float(clean) if clean else None
183
+ except ValueError:
184
+ logger.warning(f"No se pudo convertir monto_deuda: {v}")
185
+ return None
186
+ return None
187
+
188
+ @field_validator('situacion', mode='before')
189
+ @classmethod
190
+ def validate_situacion(cls, v):
191
+ if v is None:
192
+ return 1
193
+ try:
194
+ sit = int(v)
195
+ if 1 <= sit <= 6:
196
+ return sit
197
+ logger.warning(f"SituaciΓ³n BCRA fuera de rango: {sit}, usando 1")
198
+ return 1
199
+ except (ValueError, TypeError):
200
+ return 1
201
+
202
+ # REPETIR PARA CADA CLASE QUE NECESITE VALIDACIΓ“N
203
+ """
app/reports/schemas.py ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from typing import Optional
3
+ from datetime import date, datetime
4
+
5
+
6
+ # ── IdentificaciΓ³n ────────────────────────────────────────────────────────────
7
+ class Domicilio(BaseModel):
8
+ tipo: Optional[str] = None # fiscal | real
9
+ calle: Optional[str] = None
10
+ numero: Optional[str] = None
11
+ piso: Optional[str] = None
12
+ dpto: Optional[str] = None
13
+ torre: Optional[str] = None
14
+ manzana: Optional[str] = None
15
+ sector: Optional[str] = None
16
+ localidad: Optional[str] = None
17
+ provincia: Optional[str] = None
18
+ id_provincia: Optional[int] = None
19
+ cp: Optional[str] = None
20
+ dato_adicional: Optional[str] = None
21
+ tipo_dato_adicional: Optional[str] = None
22
+ estado: Optional[str] = None
23
+
24
+
25
+ class Identificacion(BaseModel):
26
+ dni: Optional[str] = None
27
+ cuil: Optional[str] = None
28
+ apellido: Optional[str] = None
29
+ nombres: Optional[str] = None
30
+ fecha_nacimiento: Optional[str] = None
31
+ fecha_defuncion: Optional[str] = None
32
+ sexo: Optional[str] = None
33
+ nacionalidad: Optional[str] = None
34
+ foto_perfil: Optional[str] = None
35
+ foto_perfil_fuente: Optional[str] = None # "linkedin", "ui_avatars", None
36
+ validado_renaper: bool = False
37
+ # PadrΓ³n Electoral
38
+ seccion_electoral: Optional[str] = None # Distrito/secciΓ³n del padrΓ³n
39
+ lugar_votacion: Optional[str] = None # Escuela/establecimiento de votaciΓ³n
40
+ mesa_votacion: Optional[str] = None # NΓΊmero de mesa
41
+ orden_padron: Optional[str] = None # Orden en el padrΓ³n
42
+ cruzado_con_busqueda: bool = False
43
+ busqueda_tipo: Optional[str] = None
44
+ # Contacto adicional de IGJ
45
+ pagina_web: Optional[str] = None
46
+ email_contacto: Optional[str] = None
47
+ linkedin: Optional[str] = None
48
+
49
+
50
+ class Contacto(BaseModel):
51
+ domicilios: list[Domicilio] = []
52
+ telefonos: list[str] = []
53
+
54
+
55
+ # ── Fiscal ────────────────────────────────────────────────────────────────────
56
+ class ActividadFiscal(BaseModel):
57
+ codigo_clae: Optional[str] = None
58
+ descripcion: Optional[str] = None
59
+ es_principal: bool = False
60
+
61
+
62
+ class Monotributo(BaseModel):
63
+ categoria: Optional[str] = None
64
+ actividad_principal: Optional[str] = None
65
+ fecha_alta: Optional[str] = None
66
+
67
+
68
+ class DatosFiscales(BaseModel):
69
+ cuit: Optional[str] = None
70
+ tipo_clave: Optional[str] = None
71
+ tipo_documento: Optional[str] = None
72
+ numero_documento: Optional[str] = None
73
+ nombres: Optional[str] = None
74
+ apellido: Optional[str] = None
75
+ nombres_completo: Optional[str] = None
76
+ condicion_iva: Optional[str] = None
77
+ estado_afip: Optional[str] = None
78
+ fecha_inicio_actividad: Optional[str] = None
79
+ fecha_nacimiento: Optional[str] = None
80
+ fecha_contrato_social: Optional[str] = None
81
+ fecha_fallecimiento: Optional[str] = None
82
+ forma_juridica: Optional[str] = None
83
+ mes_cierre: Optional[int] = None
84
+ periodo_actividad_principal: Optional[int] = None
85
+ actividades: list[ActividadFiscal] = []
86
+ domicilios: list[Domicilio] = []
87
+ domicilio_fiscal: Optional[Domicilio] = None
88
+ monotributo: Optional[Monotributo] = None
89
+ riesgo_sircreb: Optional[str] = None
90
+ riesgo_sircupa: Optional[str] = None
91
+
92
+
93
+ # ── Financiero ────────────────────────────────────────────────────────────────
94
+ class BcraHistorial(BaseModel):
95
+ periodo: Optional[str] = None
96
+ situacion: Optional[int] = None
97
+ entidad: Optional[str] = None
98
+ monto_deuda: Optional[float] = None
99
+ denominacion: Optional[str] = None
100
+ dias_atraso: Optional[int] = None
101
+ fecha_sit1: Optional[str] = None
102
+ refinanciaciones: Optional[bool] = None
103
+ situacion_juridica: Optional[str] = None
104
+ recategorizacion: Optional[bool] = None
105
+ irrec_disp_tecnica: Optional[bool] = None
106
+ en_revision: Optional[bool] = None
107
+ proceso_judicial: Optional[bool] = None
108
+
109
+ @field_validator('situacion_juridica', mode='before')
110
+ @classmethod
111
+ def coerce_situacion_juridica(cls, v):
112
+ if v is None:
113
+ return None
114
+ if isinstance(v, bool):
115
+ return "SΓ­" if v else "No"
116
+ return str(v)
117
+
118
+
119
+ class ChequeRechazado(BaseModel):
120
+ causal: Optional[str] = None
121
+ entidad_codigo: Optional[str] = None
122
+ nro_cheque: Optional[str] = None
123
+ fecha_rechazo: Optional[str] = None
124
+ monto: Optional[float] = None
125
+ fecha: Optional[str] = None
126
+ banco: Optional[str] = None
127
+ estado: Optional[str] = None
128
+ fecha_pago: Optional[str] = None
129
+ fecha_pago_multa: Optional[str] = None
130
+ estado_multa: Optional[str] = None
131
+ cta_personal: Optional[bool] = None
132
+ denom_juridica: Optional[str] = None
133
+ en_revision: Optional[bool] = None
134
+ proceso_judicial: Optional[bool] = None
135
+
136
+
137
+ class BilleteraVirtual(BaseModel):
138
+ entidad_bcra: Optional[str] = None
139
+ marca: Optional[str] = None
140
+ tipo: Optional[str] = None
141
+ rubro: Optional[str] = None
142
+ situacion: Optional[int] = None
143
+ situacion_desc: Optional[str] = None
144
+ monto: Optional[float] = None
145
+ periodo: Optional[str] = None
146
+ es_fintech: bool = False
147
+
148
+
149
+ class DatosBilleterasVirtuales(BaseModel):
150
+ total_deuda_fintech: float = 0
151
+ cantidad_wallets_con_deuda: int = 0
152
+ cantidad_fintech_detectadas: int = 0
153
+ detalle: list[BilleteraVirtual] = []
154
+
155
+
156
+ class DatosFinancieros(BaseModel):
157
+ denominacion: Optional[str] = None
158
+ bcra_situacion_actual: Optional[int] = None
159
+ bcra_situacion_descripcion: Optional[str] = None
160
+ bcra_historial: list[BcraHistorial] = []
161
+ bcra_total_deuda_miles: Optional[float] = None
162
+ bcra_dias_atraso_max: Optional[int] = None
163
+ cheques_rechazados: list[ChequeRechazado] = []
164
+ tiene_deuda: bool = False
165
+ total_deuda_miles: Optional[float] = None
166
+ dias_atraso_max: Optional[int] = None
167
+
168
+
169
+ # ── Societario ────────────────────────────────────────────────────────────────
170
+ class ParticipacionSocietaria(BaseModel):
171
+ cuit_empresa: Optional[str] = None
172
+ razon_social: Optional[str] = None
173
+ rol: Optional[str] = None
174
+ fecha_desde: Optional[str] = None
175
+
176
+
177
+ class MatriculaProfesional(BaseModel):
178
+ consejo: Optional[str] = None
179
+ matricula: Optional[str] = None
180
+ estado: Optional[str] = None
181
+
182
+
183
+ class DatosSocietarios(BaseModel):
184
+ participaciones: list[ParticipacionSocietaria] = []
185
+ matriculas_profesionales: list[MatriculaProfesional] = []
186
+ cnv_registros: list[dict] = []
187
+
188
+ class TituloAcademico(BaseModel):
189
+ titulo: str
190
+ institucion: Optional[str] = None
191
+ anio_graduacion: Optional[str] = None
192
+ nivel: Optional[str] = None # Grado | Posgrado | TΓ©cnico
193
+
194
+ class DatosAcademicos(BaseModel):
195
+ titulos: list[TituloAcademico] = []
196
+ certificaciones: list[str] = []
197
+
198
+
199
+ # ── Patrimonial ───────────────────────────────────────────────────────────────
200
+ class Vehiculo(BaseModel):
201
+ dominio: Optional[str] = None
202
+ marca: Optional[str] = None
203
+ modelo: Optional[str] = None
204
+ anio: Optional[int] = None
205
+ tipo: Optional[str] = None
206
+ radicacion: Optional[str] = None
207
+ registro: Optional[str] = None
208
+ localidad: Optional[str] = None
209
+ provincia: Optional[str] = None
210
+ validez_vtv: Optional[str] = None
211
+ tiene_seguro: bool = True
212
+ estado: Optional[str] = None
213
+
214
+
215
+ class Inmueble(BaseModel):
216
+ partido: Optional[str] = None
217
+ matricula: Optional[str] = None
218
+ tipo: Optional[str] = None
219
+ descripcion: Optional[str] = None
220
+ provincia: Optional[str] = None
221
+ superficie: Optional[str] = None
222
+ nro_partida: Optional[str] = None
223
+ # Nuevos campos
224
+ valuacion_fiscal: Optional[float] = None
225
+ fuente: Optional[str] = None
226
+
227
+
228
+ class Inhibicion(BaseModel):
229
+ tipo: Optional[str] = None
230
+ organismo: Optional[str] = None
231
+ fecha: Optional[str] = None
232
+ monto: Optional[float] = None
233
+ descripcion: Optional[str] = None
234
+
235
+
236
+ class DatosPatrimoniales(BaseModel):
237
+ vehiculos: list[Vehiculo] = []
238
+ inmuebles: list[Inmueble] = []
239
+ inhibiciones: list[Inhibicion] = []
240
+ deuda_patentes: list[dict] = []
241
+
242
+
243
+ # ── Judicial ──────────────────────────────────────────────────────────────────
244
+ class CausaJudicial(BaseModel):
245
+ expediente: Optional[str] = None
246
+ fuero: Optional[str] = None
247
+ juzgado: Optional[str] = None
248
+ caratula: Optional[str] = None
249
+ fecha: Optional[str] = None
250
+ estado: Optional[str] = None
251
+ jurisdiccion: Optional[str] = None
252
+ caratula_completa: Optional[str] = None
253
+ voces: Optional[str] = None
254
+ sumario: Optional[str] = None
255
+ magistrados: Optional[str] = None
256
+ tipo_fallo: Optional[str] = None
257
+
258
+
259
+ class DatosJudiciales(BaseModel):
260
+ causas: list[CausaJudicial] = []
261
+ antecedentes_penales: bool = False
262
+ concursos: list[dict] = []
263
+ quiebras: list[dict] = []
264
+ inhibiciones_embargos: list[Inhibicion] = []
265
+
266
+ class DatosRegistroCivil(BaseModel):
267
+ fallecido: bool = False
268
+ fecha_defuncion: Optional[str] = None
269
+ lugar_defuncion: Optional[str] = None
270
+ actas_disponibles: list[str] = [] # Nacimiento, Matrimonio, DefunciΓ³n
271
+ numero_acta: Optional[str] = None
272
+ # Nuevos campos
273
+ fecha_nacimiento: Optional[str] = None
274
+ lugar_nacimiento: Optional[str] = None
275
+ estado_civil: Optional[str] = None # "Soltero/a", "Casado/a", "Divorciado/a", "Viudo/a"
276
+ fecha_matrimonio: Optional[str] = None
277
+ conyuge_nombre: Optional[str] = None
278
+
279
+
280
+ # ── BoletΓ­n Oficial ───────────────────────────────────────────────────────────
281
+ class PublicacionBO(BaseModel):
282
+ fecha: Optional[str] = None
283
+ seccion: Optional[str] = None
284
+ rubro: Optional[str] = None
285
+ texto: Optional[str] = None
286
+ numero_boletin: Optional[str] = None
287
+ url: Optional[str] = None
288
+ fuente: Optional[str] = None
289
+ tipo: Optional[str] = None
290
+
291
+
292
+ # ── Otros ─────────────────────────────────────────────────────────────────────
293
+ class MarcaINPI(BaseModel):
294
+ denominacion: Optional[str] = None
295
+ clase: Optional[str] = None
296
+ estado: Optional[str] = None
297
+ fecha_solicitud: Optional[str] = None
298
+ acta: Optional[str] = None
299
+ titulares: Optional[str] = None
300
+ tipo_marca: Optional[str] = None
301
+ numero_resolucion: Optional[str] = None
302
+ fecha_vencimiento: Optional[str] = None
303
+
304
+
305
+ class InfraccionTransito(BaseModel):
306
+ fecha: Optional[str] = None
307
+ tipo: Optional[str] = None
308
+ motivo: Optional[str] = None # alias legible del tipo
309
+ organismo: Optional[str] = None
310
+ jurisdiccion: Optional[str] = None # requerido por frontend
311
+ monto: Optional[float] = None
312
+ estado: Optional[str] = None
313
+ acta: Optional[str] = None
314
+ dominio: Optional[str] = None
315
+ nro_causa: Optional[str] = None
316
+ vencimiento: Optional[str] = None
317
+
318
+
319
+ class RedSocial(BaseModel):
320
+ plataforma: str # LinkedIn | Facebook | Instagram | Twitter
321
+ usuario: str
322
+ url: Optional[str] = None
323
+ seguidores: Optional[str] = None
324
+ snippet: Optional[str] = None # Extracto del resultado de bΓΊsqueda (Google/DDG)
325
+
326
+
327
+ class Vinculo(BaseModel):
328
+ cuit: Optional[str] = None
329
+ nombre: Optional[str] = None
330
+ tipo: Optional[str] = None # Familiar | Societario | Domiciliario
331
+ detalle: Optional[str] = None
332
+
333
+
334
+ class DatosPrevisionales(BaseModel):
335
+ tiene_aportes: bool = False
336
+ aportes_al_dia: Optional[bool] = None # None = no consultado
337
+ ultimo_empleador: Optional[str] = None
338
+ obra_social: Optional[str] = None
339
+ fecha_alta_obra_social: Optional[str] = None # Fecha de alta en la obra social
340
+ tipo_beneficiario: Optional[str] = None # Dependiente / AutΓ³nomo / Monotributista
341
+ estado_padron: Optional[str] = None
342
+ beneficios_sociales: list[str] = [] # AUH, Progresar, etc.
343
+ jubilaciones_pensiones: list[str] = []
344
+ fecha_proximo_cobro: Optional[str] = None
345
+ lugar_cobro: Optional[str] = None
346
+ historial_coberturas: list[dict] = []
347
+ diagnostics: dict = {}
348
+
349
+
350
+ class IndicadorRiesgo(BaseModel):
351
+ codigo: str
352
+ nivel: str # Bajo | Medio | Alto | Critico
353
+ descripcion: str
354
+ hallazgo: Optional[str] = None
355
+
356
+
357
+ class ScoreHistorial(BaseModel):
358
+ fecha: str
359
+ valor: int
360
+
361
+
362
+ class ScoreCrediticio(BaseModel):
363
+ valor: int # 1-100
364
+ nivel: str # Excelente / Bueno / Regular / Malo / CrΓ­tico
365
+ historial: list[ScoreHistorial] = []
366
+ factores: list[str] = [] # ["BCRA sit=5 (Irrecuperable) -40pts", ...]
367
+
368
+
369
+ # ── Meta del informe ──────────────────────────────────────────────────────────
370
+ class ReportMeta(BaseModel):
371
+ report_id: str
372
+ generated_at: str
373
+ sources: list[str] = []
374
+ failures: list[str] = []
375
+ empty_sources: list[str] = []
376
+ diagnostics: dict = {}
377
+ cached: bool = False
378
+ version: str = "1.0"
379
+ action: Optional[str] = None
380
+ message: Optional[str] = None
381
+ sample_sent_to: Optional[str] = None
382
+
383
+ class ViasSalud(BaseModel):
384
+ cuit_consultado: Optional[str] = None
385
+ cobertura_activa: bool = False
386
+ detalles: dict = {}
387
+
388
+ class ContratoEstado(BaseModel):
389
+ cuit_proveedor: Optional[str] = None
390
+ razon_social: Optional[str] = None
391
+ estado_inscripcion: Optional[str] = None
392
+ rubro_principal: Optional[str] = None
393
+ registro: Optional[str] = None
394
+ adjudicaciones: list[dict] = []
395
+ contratos: list[dict] = []
396
+
397
+
398
+
399
+ class TimelineEvent(BaseModel):
400
+ fecha: str # YYYY-MM-DD o YYYY-MM
401
+ titulo: str # "Monotributo Cat. C"
402
+ descripcion: Optional[str] = None # "Alta en monotributo categorΓ­a C"
403
+ categoria: str = "general" # fiscal | financiero | judicial | previsional | patrimonial | general
404
+ icono: Optional[str] = None # emoji override
405
+ color: Optional[str] = None # green | yellow | red | blue | gray
406
+ fuente: Optional[str] = None # ARCA, BCRA, BO, etc.
407
+
408
+
409
+ # ═══════════════════════════════════════════════════════════════════════════════
410
+ # DEUDORES ALIMENTARIOS
411
+ # ═══════════════════════════════════════════════════════════════════════════════
412
+ class DeudorAlimentario(BaseModel):
413
+ dni: Optional[str] = None
414
+ nombre_completo: Optional[str] = None
415
+ sexo: Optional[str] = None
416
+ resultado: Optional[str] = None # CERTIFICADO_GENERADO, REQUERIR_NOMBRE, NO_REGISTRADO
417
+ renaper_validado: bool = False
418
+ pdf_path: Optional[str] = None
419
+ mensaje: Optional[str] = None
420
+ fuente: Optional[str] = None
421
+ url: Optional[str] = None
422
+
423
+
424
+ # ═══════════════════════════════════════════════════════════════════════════════
425
+ # WEB HISTORIAL (ARCHIVE.ORG)
426
+ # ═══════════════════════════════════════════════════════════════════════════════
427
+ class WebHistorialItem(BaseModel):
428
+ timestamp: Optional[str] = None
429
+ fecha: Optional[str] = None
430
+ status: Optional[str] = None
431
+ tipo: Optional[str] = None
432
+ url_wayback: Optional[str] = None
433
+
434
+
435
+ class WebHistorial(BaseModel):
436
+ url_consultada: Optional[str] = None
437
+ snapshot_mas_cercano: Optional[dict] = None
438
+ historial: list[WebHistorialItem] = []
439
+ total_snapshots: int = 0
440
+
441
+
442
+ # ═══════════════════════════════════════════════════════════════════════════════
443
+ # PERSONA FÍSICA
444
+ # ═══════════════════════════════════════════════════════════════════════════════
445
+ class PersonReport(BaseModel):
446
+ meta: ReportMeta
447
+ score: Optional[ScoreCrediticio] = None
448
+ identificacion: Identificacion = Field(default_factory=Identificacion)
449
+ contacto: Contacto = Field(default_factory=Contacto)
450
+ fiscal: DatosFiscales = Field(default_factory=DatosFiscales)
451
+ financiero: DatosFinancieros = Field(default_factory=DatosFinancieros)
452
+ societario: DatosSocietarios = Field(default_factory=DatosSocietarios)
453
+ patrimonial: DatosPatrimoniales = Field(default_factory=DatosPatrimoniales)
454
+ judicial: DatosJudiciales = Field(default_factory=DatosJudiciales)
455
+ boletin_oficial: list[PublicacionBO] = []
456
+ infracciones_transito: list[InfraccionTransito] = []
457
+ previsional: DatosPrevisionales = Field(default_factory=DatosPrevisionales)
458
+ vinculos: list[Vinculo] = []
459
+ riesgo: list[IndicadorRiesgo] = []
460
+ billeteras_virtuales: DatosBilleterasVirtuales = Field(default_factory=DatosBilleterasVirtuales)
461
+ registro_civil: DatosRegistroCivil = Field(default_factory=DatosRegistroCivil)
462
+ academico: DatosAcademicos = Field(default_factory=DatosAcademicos)
463
+ historial_domicilios: list[Domicilio] = []
464
+ telefonos: list[str] = []
465
+ redes_sociales: list[RedSocial] = []
466
+ salud: Optional[ViasSalud] = None
467
+ compras_estatales: list[ContratoEstado] = []
468
+ peps: list[dict] = []
469
+ marcas_inpi: list[MarcaINPI] = []
470
+ timeline: list[TimelineEvent] = []
471
+ deudores_alimentarios: Optional[DeudorAlimentario] = None
472
+ web_historial: Optional[WebHistorial] = None
473
+ monotributo_historial: Optional[dict] = None
474
+ registro_conductores: Optional[dict] = None
475
+
476
+ @field_validator('timeline', mode='before')
477
+ @classmethod
478
+ def coerce_timeline(cls, v):
479
+ return v if v is not None else []
480
+
481
+
482
+ # ═══════════════════════════════════════════════════════════════════════════════
483
+ # PERSONA JURÍDICA / EMPRESA
484
+ # ═══════════════════════════════════════════════════════════════════════════════
485
+ class SocioDirectivo(BaseModel):
486
+ cuil: Optional[str] = None
487
+ nombre: Optional[str] = None
488
+ rol: Optional[str] = None
489
+ participacion_pct: Optional[float] = None
490
+ fecha_designacion: Optional[str] = None
491
+
492
+
493
+ class Balance(BaseModel):
494
+ periodo: Optional[str] = None
495
+ fecha_presentacion: Optional[str] = None
496
+ resultado: Optional[float] = None
497
+ tipo: Optional[str] = None
498
+
499
+
500
+ class DatosIGJ(BaseModel):
501
+ numero_inscripcion: Optional[str] = None
502
+ razon_social: Optional[str] = None
503
+ tipo_societario: Optional[str] = None
504
+ estado: Optional[str] = None
505
+ objeto_social: Optional[str] = None
506
+ socios_directivos: list[SocioDirectivo] = []
507
+ balances: list[Balance] = []
508
+ domicilio_legal: Optional[Domicilio] = None
509
+ pagina_web: Optional[str] = None
510
+ email_contacto: Optional[str] = None
511
+ linkedin: Optional[str] = None
512
+ contacto_raw: Optional[str] = None
513
+
514
+
515
+ class CompanyIdentificacion(BaseModel):
516
+ cuit: Optional[str] = None
517
+ razon_social: Optional[str] = None
518
+ nombre_fantasia: Optional[str] = None
519
+ tipo_societario: Optional[str] = None
520
+ fecha_constitucion: Optional[str] = None
521
+ pagina_web: Optional[str] = None
522
+ telefono: Optional[str] = None
523
+ email_contacto: Optional[str] = None
524
+ linkedin: Optional[str] = None
525
+ contacto_institucional: Optional[str] = None
526
+ fecha_constitucion: Optional[str] = None
527
+
528
+
529
+ class CompanyReport(BaseModel):
530
+ meta: ReportMeta
531
+ score: Optional[ScoreCrediticio] = None
532
+ identificacion: CompanyIdentificacion = Field(default_factory=CompanyIdentificacion)
533
+ igj: DatosIGJ = Field(default_factory=DatosIGJ)
534
+ fiscal: DatosFiscales = Field(default_factory=DatosFiscales)
535
+ financiero: DatosFinancieros = Field(default_factory=DatosFinancieros)
536
+ patrimonial: DatosPatrimoniales = Field(default_factory=DatosPatrimoniales)
537
+ judicial: DatosJudiciales = Field(default_factory=DatosJudiciales)
538
+ boletin_oficial: list[PublicacionBO] = []
539
+ marcas_inpi: list[MarcaINPI] = []
540
+ vinculos: list[Vinculo] = []
541
+ riesgo: list[IndicadorRiesgo] = []
542
+ compras_estatales: list[ContratoEstado] = []
543
+ peps: list[dict] = []
544
+ cnv_registros: list[dict] = []
545
+ web_historial: Optional[WebHistorial] = None
546
+ timeline: list[TimelineEvent] = []
547
+
548
+
549
+ # ═══════════════════════════════════════════════════════════════════════════════
550
+ # VEHÍCULO
551
+ # ═══════════════════════════════════════════════════════════════════════════════
552
+ class VehicleReport(BaseModel):
553
+ meta: ReportMeta
554
+ vehiculo: Vehiculo = Field(default_factory=Vehiculo)
555
+ titular: Optional[Identificacion] = None
556
+ prendas: list[dict] = []
557
+ denuncias_robo: list[dict] = []
558
+ infracciones: list[InfraccionTransito] = []
559
+ riesgo: list[IndicadorRiesgo] = []
560
+
561
+
562
+ class PropertyReport(BaseModel):
563
+ meta: ReportMeta
564
+ direccion: str
565
+ geolocalizacion: Optional[dict] = None
566
+ datos_catastrales: Optional[dict] = None
567
+ titulares_detectados: list[Identificacion] = []
568
+ historial_boletin: list[PublicacionBO] = []
569
+ riesgo: list[IndicadorRiesgo] = []
570
+ valor_estimado: Optional[float] = None
571
+ deudas_impositivas: Optional[dict] = None
572
+
573
+
574
+ # ── Inputs de bΓΊsqueda ────────────────────────────────────────────────────────
575
+ class SearchPersonaRequest(BaseModel):
576
+ cuit: Optional[str] = None
577
+ dni: Optional[str] = None
578
+ apellido: Optional[str] = None
579
+ nombres: Optional[str] = None
580
+ provincia: Optional[str] = None
581
+
582
+
583
+ class SearchEmpresaRequest(BaseModel):
584
+ cuit: Optional[str] = None
585
+ razon_social: Optional[str] = None
586
+ provincia: Optional[str] = None
587
+
588
+
589
+ class SearchVehiculoRequest(BaseModel):
590
+ dominio: str
591
+
592
+
593
+ class SearchPropertyRequest(BaseModel):
594
+ calle: str
595
+ numero: str
596
+ localidad: str
597
+ provincia: str = ""
598
+
599
+
600
+ class GroupReport(BaseModel):
601
+ meta: ReportMeta
602
+ score: Optional[ScoreCrediticio] = None
603
+ target: PersonReport
604
+ empresas_vinculadas: list[CompanyReport] = []
605
+ vehiculos_vinculados: list[VehicleReport] = []
606
+ total_patrimonio_estimado: Optional[float] = None
607
+ riesgo_consolidado: list[IndicadorRiesgo] = []
608
+
609
+
610
+ class HistoryItem(BaseModel):
611
+ id: int
612
+ identifier: str
613
+ type: str
614
+ name: Optional[str] = None
615
+ created_at: datetime
616
+ report_id: Optional[str] = None
617
+
618
+
619
+ class MonitorItem(BaseModel):
620
+ id: int
621
+ identifier: str
622
+ type: str
623
+ active: bool
624
+ alert_count: int
625
+ last_check: datetime
626
+
627
+
628
+
629
+