diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b8982e..a362c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +v 1.45.4 [25-Ene-2022] +---------------------- + - Error: Al timbrar carta porte. + - Error: Al cancelar con Comercio Digital. + - Error: Issue #42 + - Mejora: Issue #44 + +* IMPORTANTE: Es necesario subir de nuevo tus certificados de sello, **solo** si timbras con Comercio Digital. + + v 1.45.3 [23-Ene-2022] ---------------------- - Error: El enviar por correo CFDI de pago. Ticket #40 diff --git a/VERSION b/VERSION index 1a5c268..bdcb000 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -1.45.3 +1.45.4 diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 7943af5..1b333ed 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -2,6 +2,7 @@ import falcon from middleware import get_template +from urllib.parse import unquote class AppEmpresas(object): @@ -111,6 +112,14 @@ class AppValues(object): def on_get(self, req, resp, table): values = req.params session = req.env['beaker.session'] + + if table == 'product': + original = values['name'] + try: + values['name'] = unquote(req.query_string.split('=')[1]) + except Exception as e: + values['name'] = original + if req.path in ('/values/titlelogin', '/values/empresas'): req.context['result'] = self._db.get_values(table, values, session) resp.status = falcon.HTTP_200 @@ -280,6 +289,7 @@ class AppProducts(object): def on_get(self, req, resp): values = req.params user = req.env['beaker.session']['userobj'] + if 'opt' in values: req.context['result'] = self._db.products_get(values, user) else: diff --git a/source/app/controllers/pacs/cfdi_cert.py b/source/app/controllers/pacs/cfdi_cert.py index eb5ef5f..2af68e0 100644 --- a/source/app/controllers/pacs/cfdi_cert.py +++ b/source/app/controllers/pacs/cfdi_cert.py @@ -28,18 +28,18 @@ class SATCertificate(object): self._init_values() self._get_data_cer(cer) self._get_data_key(key, password) - if not password: - self._test() + # ~ if not password: + # ~ self._test() - def _test(self): - key = self._get_key('') - self._p = TOKEN - self._key_der = key.private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.BestAvailableEncryption(self._p.encode()) - ) - return + # ~ def _test(self): + # ~ key = self._get_key('') + # ~ self._p = '' + # ~ self._key_der = key.private_bytes( + # ~ encoding=serialization.Encoding.DER, + # ~ format=serialization.PrivateFormat.PKCS8, + # ~ encryption_algorithm=serialization.BestAvailableEncryption(self._p.encode()) + # ~ ) + # ~ return def _init_values(self): self._rfc = '' @@ -52,6 +52,7 @@ class SATCertificate(object): self._is_fiel = False self._are_couple = False self._is_valid_time = False + self._key = b'' self._cer = b'' self._cer_pem = '' self._cer_txt = '' @@ -110,6 +111,8 @@ class SATCertificate(object): return def _get_data_key(self, key, password): + self._key = key + self._keyp = password self._key_enc = key if not key or not password: return @@ -175,17 +178,18 @@ class SATCertificate(object): node = xmlsec.tree.find_node(tree, 'X509Certificate') node.text = self.cer_txt - # ~ node = xmlsec.tree.find_node(tree, 'X509IssuerName') - # ~ node.text = self.issuer + node = xmlsec.tree.find_node(tree, 'X509IssuerName') + node.text = self.issuer node = xmlsec.tree.find_node(tree, 'X509SerialNumber') node.text = self.serial_number - # ~ node = xmlsec.tree.find_node(tree, 'SignatureValue') - # ~ node.text = node.text.replace('\n', '') - # ~ node = xmlsec.tree.find_node(tree, 'Modulus') - # ~ node.text = node.text.replace('\n', '') + node = xmlsec.tree.find_node(tree, 'SignatureValue') + node.text = node.text.replace('\n', '') + node = xmlsec.tree.find_node(tree, 'Modulus') + node.text = node.text.replace('\n', '') - xml_signed = ET.tostring(tree, - xml_declaration=True, encoding='UTF-8').decode() + # ~ xml_signed = ET.tostring(tree, + # ~ xml_declaration=True, encoding='UTF-8').decode() + xml_signed = ET.tostring(tree, encoding='UTF-8').decode().replace('\n', '') return xml_signed diff --git a/source/app/controllers/pacs/comerciodigital/comercio.py b/source/app/controllers/pacs/comerciodigital/comercio.py index b053c2e..e930278 100644 --- a/source/app/controllers/pacs/comerciodigital/comercio.py +++ b/source/app/controllers/pacs/comerciodigital/comercio.py @@ -42,6 +42,23 @@ logging.getLogger('requests').setLevel(logging.ERROR) TIMEOUT = 10 +def pretty_print_POST(req): + """ + At this point it is completely built and ready + to be fired; it is "prepared". + + However pay attention at the formatting used in + this function because it is programmed to be pretty + printed and may differ from the actual request. + """ + print('{}\n{}\r\n{}\r\n\r\n{}'.format( + '-----------START-----------', + req.method + ' ' + req.url, + '\r\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()), + req.body, + )) + + class PACComercioDigital(object): ws = 'https://{}.comercio-digital.mx/{}' api = 'https://app2.comercio-digital.mx/{}' @@ -91,6 +108,12 @@ class PACComercioDigital(object): headers['host'] = url.split('/')[2] headers['Content-type'] = 'text/plain' headers['Connection'] = 'Keep-Alive' + headers['Expect'] = '100-continue' + + if DEBUG: + req = requests.Request('POST', url, headers=headers, data=data) + prepared = req.prepare() + pretty_print_POST(prepared) try: result = requests.post(url, data=data, headers=headers, timeout=TIMEOUT) @@ -150,7 +173,7 @@ class PACComercioDigital(object): def _get_data_cancel(self, cfdi, info, auth): info['tipo'] = 'cfdi' info['key'] = base64.b64encode(info['key_enc']).decode() - info['cer'] = base64.b64encode(info['cer']).decode() + info['cer'] = base64.b64encode(info['cer_ori']).decode() NS_CFDI = { 'cfdi': 'http://www.sat.gob.mx/cfd/3', diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py index 72312ef..8e02fe3 100644 --- a/source/app/controllers/utils.py +++ b/source/app/controllers/utils.py @@ -765,9 +765,9 @@ def get_pac_by_rfc(cfdi): def _cancel_with_cert(invoice, args, auth, certificado): cert = SATCertificate(certificado.cer, certificado.key_enc.encode()) pac = PACS[auth['pac']]() - info = {'cer': cert.cer_pem, 'key': cert.key_pem, 'pass': '', 'args': args} - # ~ info = {'cer': cert.cer_pem, 'key': cert.key_pem, - # ~ 'key_enc': cert._key_der, 'pass': cert._p, 'args': args} + # ~ info = {'cer': cert.cer_pem, 'key': cert.key_pem, 'pass': '', 'args': args} + info = {'cer': cert.cer_pem, 'key': cert.key_pem, 'cer_ori': cert.cer, + 'key_enc': certificado.key, 'pass': decrypt(bytes(certificado.p12).decode(), certificado.serie), 'args': args} result = pac.cancel(invoice.xml, info, auth) if pac.error: @@ -781,8 +781,8 @@ def _cancel_with_cert(invoice, args, auth, certificado): def cancel_xml_sign(invoice, args, auth, certificado): - if auth['pac'] == 'finkok': - return _cancel_with_cert(invoice, args, auth, certificado) + # ~ if auth['pac'] == 'finkok': + return _cancel_with_cert(invoice, args, auth, certificado) cert = SATCertificate(certificado.cer, certificado.key_enc.encode()) pac = PACS[auth['pac']]() @@ -796,11 +796,11 @@ def cancel_xml_sign(invoice, args, auth, certificado): 'motivo': args['reason'], 'folio': folio_new, } + template = TEMPLATE_CANCEL.format(**data) tree = ET.fromstring(template.encode()) sign_xml = cert.sign_xml(tree) # ~ print(sign_xml) - result = pac.cancel_xml(sign_xml, auth, invoice.xml) if pac.error: diff --git a/source/app/models/main.py b/source/app/models/main.py index 1a4c8f4..3cf4462 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1167,9 +1167,12 @@ class Certificado(BaseModel): result['msg'] = 'El RFC del certificado no corresponde.' return result + obj.key = cert._key obj.key_enc = cert.key_enc obj.cer = cert.cer obj.serie = cert.serial_number + obj.cer_txt = cert.cer_txt + obj.p12 = utils.encrypt(cert._keyp, cert.serial_number) obj.desde = cert.not_before obj.hasta = cert.not_after obj.save() @@ -4158,6 +4161,12 @@ class Productos(BaseModel): msg = 'Ya existe un producto con esta clave' data = {'ok': False, 'row': {}, 'new': False, 'msg': msg} return data + except Exception as e: + msg = str(e) + if 'productos_clave' in msg: + msg = 'Ya existe un producto con esta clave' + data = {'ok': False, 'row': {}, 'new': False, 'msg': msg} + return data obj = Productos.get(Productos.id==id) obj.impuestos = obj_taxes @@ -5357,7 +5366,7 @@ class Facturas(BaseModel): ubicaciones = values['ubicaciones'] for ubicacion in ubicaciones: - if 'DistanciaRecorrida' in ubicacion: + if 'DistanciaRecorrida' in ubicacion and ubicacion['DistanciaRecorrida']: total_distance += float(ubicacion['DistanciaRecorrida']) if isinstance(ubicacion['DistanciaRecorrida'], (int, float)): ubicacion['DistanciaRecorrida'] = f"{ubicacion['DistanciaRecorrida']:.2f}" diff --git a/source/app/settings.py b/source/app/settings.py index eecb97a..8765bc4 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -42,7 +42,7 @@ except ImportError: DEBUG = DEBUG -VERSION = '1.45.3' +VERSION = '1.45.4' EMAIL_SUPPORT = ('soporte@empresalibre.mx',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) @@ -256,38 +256,41 @@ DEFAULT_GLOBAL = { 'clave_sat': '01010101', } -TEMPLATE_CANCEL = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -""" -# ~ TEMPLATE_CANCEL = """""" +# ~ TEMPLATE_CANCEL = """ +# ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ + # ~ +# ~ +# ~ """ + +TEMPLATE_CANCEL = """""" diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 8b2802c..6fd607f 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -1097,22 +1097,6 @@ function search_product_by_key(key){ } } }) - - //~ webix.ajax().get('/values/productokey', {'key': key}, { - //~ error: function(text, data, xhr) { - //~ msg_error('Error al consultar') - //~ }, - //~ success: function(text, data, xhr){ - //~ var values = data.json() - //~ if (values.ok){ - //~ set_product(values) - //~ } else { - //~ msg = 'No se encontró un producto con la clave: ' + key - //~ msg_error(msg) - //~ } - //~ } - //~ }) - } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 6a05e3c..7014d5d 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -746,7 +746,7 @@ var opt_clave_entidad = [ {id: 'GRO', value: 'Guerrero'}, {id: 'HID', value: 'Hidalgo'}, {id: 'JAL', value: 'Jalisco'}, - {id: 'MEX', value: 'México'}, + {id: 'MEX', value: 'Estado de México'}, {id: 'MIC', value: 'Michoacán'}, {id: 'MOR', value: 'Morelos'}, {id: 'NAC', value: 'Nacional'}, @@ -881,7 +881,7 @@ var opt_carta_estados = [ {id: 'GRO', value: 'Guerrero'}, {id: 'HID', value: 'Hidalgo'}, {id: 'JAL', value: 'Jalisco'}, - {id: 'MEX', value: 'México'}, + {id: 'MEX', value: 'Estado de México'}, {id: 'MIC', value: 'Michoacán'}, {id: 'MOR', value: 'Morelos'}, {id: 'NAC', value: 'Nacional'},