l10n-ar-api 2.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. l10n_ar_api/__init__.py +7 -0
  2. l10n_ar_api/afip_webservices/__init__.py +8 -0
  3. l10n_ar_api/afip_webservices/config.py +19 -0
  4. l10n_ar_api/afip_webservices/ws_sr_padron/__init__.py +1 -0
  5. l10n_ar_api/afip_webservices/ws_sr_padron/ws_sr_padron.py +53 -0
  6. l10n_ar_api/afip_webservices/wsaa/__init__.py +3 -0
  7. l10n_ar_api/afip_webservices/wsaa/certificate.py +54 -0
  8. l10n_ar_api/afip_webservices/wsaa/tokens.py +145 -0
  9. l10n_ar_api/afip_webservices/wsaa/wsaa.py +32 -0
  10. l10n_ar_api/afip_webservices/wsbfe/__init__.py +3 -0
  11. l10n_ar_api/afip_webservices/wsbfe/error.py +9 -0
  12. l10n_ar_api/afip_webservices/wsbfe/invoice.py +118 -0
  13. l10n_ar_api/afip_webservices/wsbfe/wsbfe.py +328 -0
  14. l10n_ar_api/afip_webservices/wsct/__init__.py +3 -0
  15. l10n_ar_api/afip_webservices/wsct/error.py +9 -0
  16. l10n_ar_api/afip_webservices/wsct/invoice.py +172 -0
  17. l10n_ar_api/afip_webservices/wsct/wsct.py +465 -0
  18. l10n_ar_api/afip_webservices/wsfe/__init__.py +4 -0
  19. l10n_ar_api/afip_webservices/wsfe/error.py +9 -0
  20. l10n_ar_api/afip_webservices/wsfe/invoice.py +156 -0
  21. l10n_ar_api/afip_webservices/wsfe/qr_generator.py +30 -0
  22. l10n_ar_api/afip_webservices/wsfe/wsfe.py +395 -0
  23. l10n_ar_api/afip_webservices/wsfex/__init__.py +3 -0
  24. l10n_ar_api/afip_webservices/wsfex/error.py +9 -0
  25. l10n_ar_api/afip_webservices/wsfex/invoice.py +140 -0
  26. l10n_ar_api/afip_webservices/wsfex/wsfex.py +343 -0
  27. l10n_ar_api/afip_webservices/wstesimbrefiscal/__init__.py +2 -0
  28. l10n_ar_api/afip_webservices/wstesimbrefiscal/error.py +9 -0
  29. l10n_ar_api/afip_webservices/wstesimbrefiscal/wstesimbrefiscal.py +145 -0
  30. l10n_ar_api/anmat_webservices/__init__.py +2 -0
  31. l10n_ar_api/anmat_webservices/anmat.py +129 -0
  32. l10n_ar_api/anmat_webservices/config.py +4 -0
  33. l10n_ar_api/arba_webservices/__init__.py +21 -0
  34. l10n_ar_api/arba_webservices/config.py +4 -0
  35. l10n_ar_api/arba_webservices/cot/__init__.py +1 -0
  36. l10n_ar_api/arba_webservices/cot/config.py +38 -0
  37. l10n_ar_api/arba_webservices/cot/cot.py +635 -0
  38. l10n_ar_api/arba_webservices/iibb.py +56 -0
  39. l10n_ar_api/documents/__init__.py +4 -0
  40. l10n_ar_api/documents/invoice.py +22 -0
  41. l10n_ar_api/documents/tax.py +22 -0
  42. l10n_ar_api/documents/tests/__init__.py +3 -0
  43. l10n_ar_api/documents/tests/test_gross_income.py +41 -0
  44. l10n_ar_api/documents/tests/test_profit.py +61 -0
  45. l10n_ar_api/documents/tests/test_tribute.py +11 -0
  46. l10n_ar_api/documents/tribute.py +111 -0
  47. l10n_ar_api/padron/__init__.py +2 -0
  48. l10n_ar_api/padron/banks.py +164 -0
  49. l10n_ar_api/padron/config.py +1 -0
  50. l10n_ar_api/padron/contributor.py +35 -0
  51. l10n_ar_api/padron/test/__init__.py +2 -0
  52. l10n_ar_api/padron/test/test_banks.py +30 -0
  53. l10n_ar_api/padron/test/test_contributors.py +30 -0
  54. l10n_ar_api/presentations/__init__.py +2 -0
  55. l10n_ar_api/presentations/presentation.py +79 -0
  56. l10n_ar_api/presentations/presentation_line.py +4396 -0
  57. l10n_ar_api/presentations/test/__init__.py +4 -0
  58. l10n_ar_api/presentations/test/test_purchases_sales_presentation.py +196 -0
  59. l10n_ar_api/presentations/test/test_purchases_sales_presentation_line.py +183 -0
  60. l10n_ar_api/presentations/test/test_purchases_sales_vat_digital_book.py +173 -0
  61. l10n_ar_api/presentations/test/test_purchases_sales_vat_digital_book_line.py +161 -0
  62. l10n_ar_api-2.8.3.dist-info/METADATA +27 -0
  63. l10n_ar_api-2.8.3.dist-info/RECORD +65 -0
  64. l10n_ar_api-2.8.3.dist-info/WHEEL +5 -0
  65. l10n_ar_api-2.8.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,7 @@
1
+ from __future__ import absolute_import
2
+ from . import documents
3
+ from . import afip_webservices
4
+ from . import anmat_webservices
5
+ from . import arba_webservices
6
+ from . import presentations
7
+ from . import padron
@@ -0,0 +1,8 @@
1
+ from __future__ import absolute_import
2
+ from . import wsaa
3
+ from . import wsbfe
4
+ from . import wsfe
5
+ from . import wsfex
6
+ from . import config
7
+ from . import wsct
8
+ from . import wstesimbrefiscal
@@ -0,0 +1,19 @@
1
+ authorization_urls = dict(
2
+ homologation='https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl',
3
+ production='https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl',
4
+ )
5
+
6
+ service_urls = dict(
7
+ wstesimbrefiscal_homologation='https://wsaduhomoext.afip.gob.ar/diav2/wgestimbrefiscalelectronico/wgestimbrefiscalelectronico.asmx?WSDL',
8
+ wstesimbrefiscal_production='https://wswadu.afip.gob.ar/DIAV2/wgestimbrefiscalelectronico/wgestimbrefiscalelectronico.asmx?WSDL',
9
+ wsct_homologation='https://fwshomo.afip.gov.ar/wsct/CTService?WSDL',
10
+ wsct_production='https://serviciosjava.afip.gob.ar/wsct/CTService?WSDL',
11
+ wsbfev1_homologation='https://wswhomo.afip.gov.ar/wsbfev1/service.asmx?WSDL',
12
+ wsbfev1_production='https://servicios1.afip.gov.ar/wsbfev1/service.asmx?WSDL',
13
+ wsfev1_homologation='https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl',
14
+ wsfev1_production='https://servicios1.afip.gov.ar/wsfev1/service.asmx?wsdl',
15
+ wsfexv1_homologation='https://wswhomo.afip.gov.ar/wsfexv1/service.asmx?wsdl',
16
+ wsfexv1_production='https://servicios1.afip.gov.ar/wsfexv1/service.asmx?wsdl',
17
+ ws_sr_padron_a5_homologation='https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL',
18
+ ws_sr_padron_a5_production='https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL',
19
+ )
@@ -0,0 +1 @@
1
+ from . import ws_sr_padron
@@ -0,0 +1,53 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Segun RG 2485 – Proyecto FE v2.8 - 12/09/2016
3
+ import sys
4
+ sys.path.append("..")
5
+ from zeep import Client
6
+ from .. import config
7
+
8
+
9
+ class InvalidIdException(Exception):
10
+ def __init__(self, msg):
11
+ super(Exception, self).__init__(msg)
12
+
13
+
14
+ class WsSrPadron(object):
15
+ """
16
+ Consulta Padron A5.
17
+
18
+ :param access_token: AccessToken - Token de acceso
19
+ :param cuit: Cuit de la empresa
20
+ :param homologation: Homologacion si es True
21
+ :param url: Url de servicios para WsSrPadron
22
+ """
23
+
24
+ def __init__(self, access_token, cuit, homologation=True, url=None):
25
+ if not url:
26
+ self.url = config.service_urls.get('ws_sr_padron_a5_homologation') if homologation\
27
+ else config.service_urls.get('ws_sr_padron_a5_production')
28
+ else:
29
+ self.url = url
30
+
31
+ self.accessToken = access_token
32
+ self.cuit = cuit
33
+
34
+ def get_partner_data(self, vat):
35
+ """
36
+ :param vat: Numero de documentos a consultar
37
+ :returns str: Respuesta de AFIP sobre el numero de documento
38
+ """
39
+
40
+ try:
41
+ response = Client(self.url).service.getPersona(
42
+ token=self.accessToken.token,
43
+ sign=self.accessToken.sign,
44
+ cuitRepresentada=self.cuit,
45
+ idPersona=vat
46
+ )
47
+ except Exception as e:
48
+ if e.args[0] == 'No existe persona con ese Id':
49
+ raise InvalidIdException("El numero de documento {} es inexistente.".format(vat))
50
+ if e.args[0] == 'El Id de la persona no es valido':
51
+ raise InvalidIdException("El numero de documento {} no es valido.".format(vat))
52
+ raise Exception(e.args[0])
53
+ return response
@@ -0,0 +1,3 @@
1
+ from . import certificate
2
+ from . import tokens
3
+ from . import wsaa
@@ -0,0 +1,54 @@
1
+ from OpenSSL import crypto
2
+
3
+
4
+ class WsaaCertificate(object):
5
+
6
+ def __init__(self, key):
7
+ self.key = key
8
+ self.country_code = None
9
+ self.state_name = None
10
+ self.company_name = None
11
+ self.company_vat = None
12
+
13
+ def validate_values(self):
14
+ """ Validamos que esten todos los campos necesarios seteados """
15
+ values = vars(self)
16
+ for value in values:
17
+ if not values.get(value):
18
+ raise AttributeError('Falta configurar alguno de los siguientes campos:\n'
19
+ 'Codigo de pais, Provincia, Nombre de la empresa o CUIT')
20
+
21
+ def generate_certificate_request(self, hash_sign='sha256', ou='odoo'):
22
+
23
+ self.validate_values()
24
+
25
+ # Utilizamos la libreria de crypto para generar el pedido de certificado
26
+ req = crypto.X509Req()
27
+ req.get_subject().C = self.country_code
28
+ req.get_subject().ST = self.state_name
29
+ req.get_subject().O = self.company_name
30
+ req.get_subject().OU = ou
31
+ req.get_subject().CN = self.company_name
32
+ req.get_subject().serialNumber = 'CUIT {}'.format(self.company_vat)
33
+
34
+ # Validamos el formato de la key
35
+ key = crypto.load_privatekey(crypto.FILETYPE_PEM, self.key)
36
+ self.key = crypto.dump_privatekey(crypto.FILETYPE_PEM, key)
37
+
38
+ # Firmamos con la key y el hash el certificado
39
+ req.set_pubkey(key)
40
+ req.sign(key, hash_sign)
41
+
42
+ return crypto.dump_certificate_request(crypto.FILETYPE_PEM, req)
43
+
44
+
45
+ class WsaaPrivateKey(object):
46
+
47
+ def __init__(self, length=2048):
48
+ self.length = length
49
+ self.key = None
50
+
51
+ def generate_rsa_key(self):
52
+ pkey = crypto.PKey()
53
+ pkey.generate_key(crypto.TYPE_RSA, self.length)
54
+ self.key = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
@@ -0,0 +1,145 @@
1
+ # -*- coding: utf-8 -*-
2
+ import pytz
3
+ from datetime import datetime
4
+ import time
5
+ from lxml import etree
6
+ from M2Crypto import BIO, SMIME
7
+ import sys
8
+
9
+
10
+ class AccessRequerimentToken(object):
11
+
12
+ def __init__(self, service, tz=pytz.utc):
13
+ self._service = service
14
+ self._timezone = tz
15
+
16
+ def _create_tra(self):
17
+ """
18
+ uniqueId: Entero de 32 bits sin signo que junto con generationTime identifica el
19
+ requerimiento.
20
+
21
+ generationTime: Momento en que fue generado el requerimiento. La tolerancia de aceptacion
22
+ sera de hasta 24 horas previas al requerimiento de acceso
23
+
24
+ expirationTime: Momento en el que expira la solicitud. La tolerancia de aceptacion sera de
25
+ hasta 24 horas posteriores al requerimiento de acceso.
26
+
27
+ service: Identificacion del WSN para el cual se solicita el TA.
28
+
29
+ :return: Ticket de Requerimiento de Acceso
30
+ """
31
+
32
+ ticket_request = etree.Element('loginTicketRequest')
33
+ ticket_request.set('version', '1.0')
34
+ header = etree.SubElement(ticket_request, 'header')
35
+
36
+ # uniqueId
37
+ uniqueid = etree.SubElement(header, 'uniqueId')
38
+ # Traemos la fecha actual
39
+ timestamp = int(time.mktime(datetime.now().timetuple()))
40
+ uniqueid.text = str(timestamp)
41
+
42
+ # generationTime
43
+ # Generamos el token con media hora atrasada
44
+ tsgen = datetime.fromtimestamp(timestamp-1800)
45
+ tsgen = pytz.utc.localize(tsgen).astimezone(self.timezone)
46
+ gentime = etree.SubElement(header, 'generationTime')
47
+ gentime.text = tsgen.isoformat()
48
+
49
+ # expirationTime
50
+ # El token vence 4.5 horas despues
51
+ tsexp = datetime.fromtimestamp(timestamp+14400)
52
+ tsexp = pytz.utc.localize(tsexp).astimezone(self.timezone)
53
+ exptime = etree.SubElement(header, 'expirationTime')
54
+ exptime.text = tsexp.isoformat()
55
+ exptime.tail = None
56
+
57
+ # service
58
+ serv = etree.SubElement(ticket_request, 'service')
59
+ serv.text = self.service
60
+
61
+ return etree.tostring(ticket_request)
62
+
63
+ def sign_tra(self, private_key, certificate):
64
+
65
+ smime = SMIME.SMIME()
66
+ ks = BIO.MemoryBuffer(private_key.encode('ascii'))
67
+ cs = BIO.MemoryBuffer(certificate.encode('ascii'))
68
+ bf = BIO.MemoryBuffer(self._create_tra().decode('ascii').encode('ascii'))
69
+ out = BIO.MemoryBuffer()
70
+
71
+ try:
72
+ smime.load_key_bio(ks, cs)
73
+ except Exception:
74
+ raise Exception('Error en el formato del certificado o clave privada')
75
+
76
+ sbf = smime.sign(bf)
77
+ smime.write(out, sbf)
78
+
79
+ head, body, end = out.read().split(b'\n\n')
80
+ return body
81
+
82
+ @property
83
+ def service(self):
84
+ return self._service
85
+
86
+ @property
87
+ def timezone(self):
88
+ return self._timezone
89
+
90
+
91
+ class AccessToken(object):
92
+
93
+ """ Ejemplo de token:
94
+
95
+ <?xml version="1.0" encoding="UTF8"?>
96
+ <loginTicketResponse version="1.0">
97
+ <header>
98
+ <source>cn=wsaa,o=afip,c=ar,serialNumber=CUIT 33693450239</source>
99
+ <destination>cn=srv1,ou=facturacion,o=empresa s.a.,c=ar,serialNumber=CUIT 30123456789</destination>
100
+ <uniqueId>383953094</uniqueId>
101
+ <generationTime>20011231T12:00:0203:00</generationTime>
102
+ <expirationTime>20020101T00:00:0203:00</expirationTime>
103
+ </header>
104
+ <credentials>
105
+ <token>cES0SSuWIIPlfe5/dLtb0Qeg2jQuvYuuSEDOrz+w2EnAQiEeS86gzYf7ehiU3UaYit5FRb9z/3zq</token>
106
+ <sign>a6QSSZBgLf0TTcktSNteeSg3qXsMVjo/F5py/Gtw7xucTrUWbsrVCdIoGE8Cm1bixpuVPlr58k6n</sign>
107
+ </credentials>
108
+ </loginTicketResponse>
109
+ """
110
+
111
+ def __init__(self):
112
+ # Header
113
+ self.source = None
114
+ self.destination = None
115
+ self.uniqueId = None
116
+ self.generation_time = None
117
+ self.expiration_time = None
118
+
119
+ # Credentials
120
+ self.token = None
121
+ self.sign = None
122
+
123
+ def create_token_from_login(self, login_fault):
124
+ """
125
+ Asigna los atributos al token desde el login
126
+ :param login_fault: XML con el login autorizado
127
+ """
128
+
129
+ if sys.version_info[0] >= 3:
130
+ login_fault_tree = etree.fromstring(login_fault.encode('utf-8'))
131
+ else:
132
+ login_fault_tree = etree.fromstring(str(login_fault))
133
+
134
+ try:
135
+
136
+ self.source = login_fault_tree.find('header/source').text
137
+ self.destination = login_fault_tree.find('header/destination').text
138
+ self.generation_time = login_fault_tree.find('header/generationTime').text
139
+ self.expiration_time = login_fault_tree.find('header/expirationTime').text
140
+
141
+ self.token = login_fault_tree.find('credentials/token').text
142
+ self.sign = login_fault_tree.find('credentials/sign').text
143
+
144
+ except AttributeError:
145
+ raise AttributeError("Error al generar el token de acceso")
@@ -0,0 +1,32 @@
1
+ from zeep import Client
2
+ from requests import Session
3
+ from zeep.transports import Transport
4
+
5
+ from l10n_ar_api.afip_webservices import config
6
+
7
+
8
+ class Wsaa(object):
9
+
10
+ def __init__(self, homologation=True, url=None):
11
+
12
+ if not url:
13
+ self.url = config.authorization_urls.get('homologation') if homologation\
14
+ else config.authorization_urls.get('production')
15
+ else:
16
+ self.url = url
17
+
18
+ def login(self, tra):
19
+ """
20
+ :param tra: TRA que se usara para el logeo
21
+ :return: XML con el login autorizado
22
+ """
23
+
24
+ try:
25
+ session = Session()
26
+ session.verify = False
27
+ transport = Transport(session=session)
28
+ login_fault = Client(self.url, transport=transport).service.loginCms(tra)
29
+ except Exception as e:
30
+ raise Exception("Error al autenticarse\n{}".format(e))
31
+
32
+ return login_fault
@@ -0,0 +1,3 @@
1
+ from . import wsbfe
2
+ from . import error
3
+ from . import invoice
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ class AfipError:
3
+
4
+ @classmethod
5
+ def parse_error(cls, error):
6
+ return Exception("Error {}: {}".format(
7
+ error.BFEErr.ErrCode,
8
+ error.BFEErr.ErrMsg),
9
+ )
@@ -0,0 +1,118 @@
1
+ # -*- coding: utf-8 -*-
2
+ from l10n_ar_api.documents import invoice
3
+ from datetime import date, datetime
4
+
5
+
6
+ class FiscalElectronicBondValidator:
7
+
8
+ @classmethod
9
+ def validate_invoice(cls, invoice):
10
+ '''
11
+ Valida que los campos de la factura cumplan con los requisitos de la AFIP
12
+ :param invoices: Factura a validar, objeto FiscalElectronicBond
13
+ '''
14
+
15
+ cls._validate_document_date(invoice)
16
+ cls._validate_amount(invoice)
17
+
18
+ @staticmethod
19
+ def _validate_document_date(invoice):
20
+ assert invoice.document_date, "El documento no tiene fecha"
21
+
22
+ invoice_date = datetime.strptime(invoice.document_date, '%Y%m%d').date()
23
+
24
+ if abs(date.today() - invoice_date).days > 5:
25
+ raise AttributeError("La fecha del documento debe ser\
26
+ mayor o menor a 5 dias de la fecha de generacion")
27
+
28
+ date_today = date.today().replace(day=1)
29
+ new_invoice_date = invoice_date.replace(day=1)
30
+ if date_today != new_invoice_date:
31
+ raise AttributeError("La fecha del documento no debe exceder el mes de la fecha de\
32
+ envío del pedido de autorización")
33
+
34
+ @staticmethod
35
+ def _validate_amount(invoice):
36
+ if invoice.get_total_items_amount() > invoice.get_total_amount():
37
+ raise AttributeError("La Suma de los items de la factura\
38
+ debe ser menor o igual al total de la misma")
39
+
40
+ class FiscalElectronicBondItem(object):
41
+
42
+ def __init__(self, codigo_prod_ncm, description, quantity = None, measurement_unit = None, unit_price = None, bonification = None, iva_id = None):
43
+ self.codigo_prod_ncm = codigo_prod_ncm
44
+ self.description = description
45
+ self.quantity = quantity
46
+ self.measurement_unit = measurement_unit
47
+ self.unit_price = unit_price
48
+ self.bonification = bonification
49
+ self.iva_id = iva_id
50
+
51
+ @property
52
+ def total_price(self):
53
+ assert self.quantity, "No existe cantidad para el item"
54
+ assert self.unit_price, "No existe precio para el item"
55
+
56
+ return (self.quantity * self.unit_price) - self.bonification
57
+
58
+
59
+ class FiscalElectronicBond(invoice.Invoice):
60
+
61
+ def __init__(self, document_code):
62
+ self.zone_id = 0
63
+ self.reception_amount = None
64
+ self.municipal_reception_amount = None
65
+ self.array_items = []
66
+
67
+ # Cliente
68
+ self.customer_name = None
69
+ self.customer_address = None
70
+ self.customer_document_type = None
71
+ self.customer_document_number = None
72
+
73
+ # Tributos
74
+ self.total_amount = None
75
+ self.pay_off_tax_amount = None
76
+ self.rni_pay_off_tax_amount = None
77
+ self.internal_tax_amount = None
78
+ self.iibb_amount = None
79
+
80
+ # Moneda
81
+ self.mon_id = None
82
+ self.mon_cotiz = None
83
+
84
+ # Opcionales
85
+ self.array_optionals = []
86
+
87
+ # Comprobantes asociados
88
+ self.associated_documents = []
89
+
90
+ self._payment_due_date = None
91
+
92
+ super(FiscalElectronicBond, self).__init__(document_code)
93
+
94
+ # Override
95
+ def get_total_amount(self):
96
+ return self.total_amount
97
+
98
+ def get_total_items_amount(self):
99
+ return sum(item.total_price for item in self.array_items)
100
+
101
+ def add_item(self, value):
102
+ self.array_items.append(value)
103
+
104
+ @property
105
+ def document_date(self):
106
+ return self._document_date
107
+
108
+ @document_date.setter
109
+ def document_date(self, value):
110
+ self._document_date = value.strftime('%Y%m%d')
111
+
112
+ @property
113
+ def payment_due_date(self):
114
+ return self._payment_due_date
115
+
116
+ @payment_due_date.setter
117
+ def payment_due_date(self, value):
118
+ self._payment_due_date = value.strftime('%Y%m%d')