From 8ff857ec73d637b44457f536a5edc80871e2a61e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 16 Oct 2017 00:02:51 -0500 Subject: [PATCH] Enviar correo --- source/app/controllers/helper.py | 84 ++++++++++++++ source/app/controllers/main.py | 23 +++- source/app/controllers/util.py | 34 +++++- source/app/main.py | 3 +- source/app/models/db.py | 12 ++ source/app/models/main.py | 90 ++++++++++++++- source/static/js/controller/admin.js | 141 +++++++++++++++++++++++- source/static/js/controller/invoices.js | 37 ++++++- source/static/js/ui/admin.js | 92 ++++++++++++++++ 9 files changed, 508 insertions(+), 8 deletions(-) diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 6020d66..32fe513 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -14,8 +14,15 @@ #~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 import re +import smtplib import collections + from collections import OrderedDict +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email import encoders +from email.utils import formatdate class CaseInsensitiveDict(collections.MutableMapping): @@ -223,3 +230,80 @@ class NumLet(object): return re.sub('$', 's', palabra) else: return palabra + 'es' + + +class SendMail(object): + + def __init__(self, config): + self._config = config + self._server = None + self._error = '' + self._is_connect = self._login() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self): + try: + if self._config['ssl']: + self._server = smtplib.SMTP_SSL( + self._config['servidor'], + self._config['puerto'], timeout=10) + else: + self._server = smtplib.SMTP( + self._config['servidor'], + self._config['puerto'], timeout=10) + self._server.login(self._config['usuario'], self._config['contra']) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = 'Nombre de usuario o contraseña inválidos' + return False + if '534' in str(e) and 'gmail' in self._config['servidor']: + self._error = 'Necesitas activar el acceso a otras ' \ + 'aplicaciones en tu cuenta de GMail' + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return + + def send(self, options): + try: + message = MIMEMultipart() + message['From'] = self._config['usuario'] + message['To'] = options['para'] + message['CC'] = options['copia'] + message['Subject'] = options['asunto'] + message['Date'] = formatdate(localtime=True) + message.attach(MIMEText(options['mensaje'], 'html')) + for f in options['files']: + part = MIMEBase('application', 'octet-stream') + part.set_payload(f[0]) + encoders.encode_base64(part) + part.add_header( + 'Content-Disposition', + "attachment; filename={}".format(f[1])) + message.attach(part) + + receivers = options['para'].split(',') + options['copia'].split(',') + self._server.sendmail( + self._config['usuario'], receivers, message.as_string()) + return '' + except Exception as e: + return str(e) + + def close(self): + try: + self._server.quit() + except: + pass + return diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 0c1ebfb..1939e44 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -76,12 +76,33 @@ class AppValues(object): if file_object is None: session = req.env['beaker.session'] values = req.params - req.context['result'] = self._db.validate_cert(values, session) + if table == 'correo': + req.context['result'] = self._db.validate_email(values) + elif table == 'sendmail': + req.context['result'] = self._db.send_email(values, session) + else: + req.context['result'] = self._db.validate_cert(values, session) else: req.context['result'] = self._db.add_cert(file_object) resp.status = falcon.HTTP_200 +class AppConfig(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_config(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.add_config(values) + resp.status = falcon.HTTP_200 + + class AppPartners(object): diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index e673eb9..6cba2be 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -15,7 +15,9 @@ import time import unicodedata import uuid import zipfile + from io import BytesIO +from smtplib import SMTPException, SMTPAuthenticationError from xml.etree import ElementTree as ET import uno @@ -25,7 +27,7 @@ from com.sun.star.awt import Size import pyqrcode from dateutil import parser -from .helper import CaseInsensitiveDict, NumLet +from .helper import CaseInsensitiveDict, NumLet, SendMail from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE @@ -926,3 +928,33 @@ def to_zip(*files): return zip_buffer.getvalue() + +def make_fields(xml): + doc = ET.fromstring(xml) + data = CaseInsensitiveDict(doc.attrib.copy()) + data.pop('certificado') + data.pop('sello') + version = data['version'] + receptor = doc.find('{}Receptor'.format(PRE[version])) + receptor = CaseInsensitiveDict(receptor.attrib.copy()) + data['receptor_nombre'] = receptor['nombre'] + data['receptor_rfc'] = receptor['rfc'] + data = {k.lower(): v for k, v in data.items()} + return data + + +def make_info_mail(data, fields): + return data.format(**fields).replace('\n', '
') + + +def send_mail(data): + msg = '' + server = SendMail(data['server']) + is_connect = server.is_connect + if is_connect: + msg = server.send(data['options']) + else: + msg = server.error + server.close() + return {'ok': is_connect, 'msg': msg} + diff --git a/source/app/main.py b/source/app/main.py index b423f09..a35833f 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -13,7 +13,7 @@ from middleware import ( ) from models.db import StorageEngine from controllers.main import ( - AppLogin, AppLogout, AppAdmin, AppEmisor, + AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, AppDocumentos ) @@ -38,6 +38,7 @@ api.add_route('/emisor', AppEmisor(db)) api.add_route('/folios', AppFolios(db)) api.add_route('/main', AppMain(db)) api.add_route('/values/{table}', AppValues(db)) +api.add_route('/config', AppConfig(db)) api.add_route('/doc/{type_doc}/{id_doc}', AppDocumentos(db)) api.add_route('/partners', AppPartners(db)) api.add_route('/products', AppProducts(db)) diff --git a/source/app/models/db.py b/source/app/models/db.py index 7488143..f5a460d 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -14,12 +14,24 @@ class StorageEngine(object): def get_values(self, table, values=None): return getattr(self, '_get_{}'.format(table))(values) + def get_config(self, values): + return main.Configuracion.get_(values) + + def add_config(self, values): + return main.Configuracion.add(values) + def add_cert(self, file_object): return main.Certificado.add(file_object) def validate_cert(self, values, session): return main.Certificado.validate(values, session) + def validate_email(self, values): + return main.test_correo(values) + + def send_email(self, values, session): + return main.Facturas.send(values['id'], session['rfc']) + def _get_cert(self, values): return main.Certificado.get_data() diff --git a/source/app/models/main.py b/source/app/models/main.py index 5d69c8c..24f9dcd 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -59,9 +59,34 @@ def desconectar(): class Configuracion(BaseModel): - clave = TextField() + clave = TextField(unique=True) valor = TextField(default='') + @classmethod + def get_(cls, keys): + if keys['fields'] == 'correo': + fields = ('correo_servidor', 'correo_puerto', 'correo_ssl', + 'correo_usuario', 'correo_contra', 'correo_copia', + 'correo_asunto', 'correo_mensaje', 'correo_directo') + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + values = {r.clave: r.valor for r in data} + return values + + @classmethod + def add(cls, values): + try: + for k, v in values.items(): + obj, created = Configuracion.get_or_create(clave=k) + obj.valor = v + obj.save() + return {'ok': True} + except Exception as e: + log.error(str(e)) + return {'ok': False, 'msg': str(e)} + class Meta: order_by = ('clave',) indexes = ( @@ -1059,6 +1084,47 @@ class Facturas(BaseModel): return file_zip, name_zip + @classmethod + def send(cls, id, rfc): + values = Configuracion.get_({'fields': 'correo'}) + #~ print (server) + obj = Facturas.get(Facturas.id==id) + if obj.uuid is None: + msg = 'La factura no esta timbrada' + return {'ok': False, 'msg': msg} + + if not obj.cliente.correo_facturas: + msg = 'El cliente no tiene configurado el correo para facturas' + return {'ok': False, 'msg': msg} + + files = (cls.get_zip(id, rfc),) + + fields = util.make_fields(obj.xml) + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(int(values['correo_ssl'])), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': obj.cliente.correo_facturas, + 'copia': values['correo_copia'], + 'asunto': util.make_info_mail(values['correo_asunto'], fields), + 'mensaje': util.make_info_mail(values['correo_mensaje'], fields), + 'files': files, + } + data= { + 'server': server, + 'options': options, + } + result = util.send_mail(data) + if not result['ok'] or result['msg']: + return {'ok': False, 'msg': result['msg']} + + msg = 'Factura enviada correctamente' + return {'ok': True, 'msg': msg} + @classmethod def get_(cls, values): rows = tuple(Facturas @@ -1450,6 +1516,28 @@ def get_sat_key(key): return util.get_sat_key('products', key) +def test_correo(values): + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(values['correo_ssl'].replace('0', '')), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': values['correo_usuario'], + 'copia': values['correo_copia'], + 'asunto': values['correo_asunto'], + 'mensaje': values['correo_mensaje'].replace('\n', '
'), + 'files': [], + } + data= { + 'server': server, + 'options': options, + } + return util.send_mail(data) + + def _init_values(): data = ( {'key': 'version', 'value': VERSION}, diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 47c90b8..52b3bcf 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -16,6 +16,8 @@ var controllers = { $$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete) $$('cmd_agregar_serie').attachEvent('onItemClick', cmd_agregar_serie_click) $$('grid_folios').attachEvent('onItemClick', grid_folios_click) + $$('cmd_probar_correo').attachEvent('onItemClick', cmd_probar_correo_click) + $$('cmd_guardar_correo').attachEvent('onItemClick', cmd_guardar_correo_click) } } @@ -175,6 +177,24 @@ function get_table_folios(){ } +function get_config_correo(){ + var form = $$('form_correo') + var fields = form.getValues() + + webix.ajax().get('/config', {'fields': 'correo'}, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json() + form.setValues(values) + } + }) + +} + + function multi_admin_change(prevID, nextID){ //~ webix.message(nextID) if(nextID == 'app_emisor'){ @@ -188,6 +208,11 @@ function multi_admin_change(prevID, nextID){ get_table_folios() return } + + if(nextID == 'app_correo'){ + get_config_correo() + return + } } @@ -442,10 +467,120 @@ function grid_folios_click(id, e, node){ msg_error(msg) } }) - - - } } }) } + + +function validar_correo(values){ + + if(!values.correo_servidor.trim()){ + msg = 'El servidor de salida no puede estar vacío' + msg_error(msg) + return false + } + if(!values.correo_puerto){ + msg = 'El puerto no puede ser cero' + msg_error(msg) + return false + } + if(!values.correo_usuario.trim()){ + msg = 'El nombre de usuario no puede estar vacío' + msg_error(msg) + return false + } + if(!values.correo_contra.trim()){ + msg = 'La contraseña no puede estar vacía' + msg_error(msg) + return false + } + if(!values.correo_asunto.trim()){ + msg = 'El asunto del correo no puede estar vacío' + msg_error(msg) + return false + } + if(!values.correo_mensaje.trim()){ + msg = 'El mensaje del correo no puede estar vacío' + msg_error(msg) + return false + } + + return true +} + + +function cmd_probar_correo_click(){ + var form = $$('form_correo') + var values = form.getValues() + + if(!validar_correo(values)){ + return + } + + webix.ajax().sync().post('/values/correo', values, { + error: function(text, data, xhr) { + msg = 'Error al probar el correo' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg = 'Correo de prueba enviado correctamente. Ya puedes \ + guardar esta configuración' + msg_sucess(msg) + }else{ + msg_error(values.msg) + } + } + }) + +} + + +function save_config_mail(values){ + + webix.ajax().sync().post('/config', values, { + error: function(text, data, xhr) { + msg = 'Error al guardar la configuración' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg = 'Configuración guardada correctamente' + msg_sucess(msg) + }else{ + msg_error(values.msg) + } + } + }) + +} + + +function cmd_guardar_correo_click(){ + var form = $$('form_correo') + var values = form.getValues() + + if(!validar_correo(values)){ + return + } + + msg = 'Asegurate de haber probado la configuración

\ + ¿Estás seguro de guardar estos datos?' + webix.confirm({ + title: 'Configuración de correo', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + save_config_mail(values) + } + } + }) +} + + diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index af77be7..3adc7e3 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -642,6 +642,41 @@ function cmd_invoice_timbrar_click(){ } +function enviar_correo(row){ + if(!row.uuid){ + msg_error('La factura no esta timbrada') + return + } + + msg = '¿Estás seguro de enviar por correo esta factura?' + webix.confirm({ + title: 'Enviar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + webix.ajax().post('/values/sendmail', {'id': row.id}, { + error:function(text, data, XmlHttpRequest){ + msg = 'Ocurrio un error, consulta a soporte técnico' + msg_error(msg) + }, + success:function(text, data, XmlHttpRequest){ + values = data.json(); + if(values.ok){ + msg_sucess(values.msg) + }else{ + msg_error(values.msg) + } + } + }) + } + } + }) +} + + function grid_invoices_click(id, e, node){ var row = this.getItem(id) @@ -652,7 +687,7 @@ function grid_invoices_click(id, e, node){ }else if(id.column == 'zip'){ location = '/doc/zip/' + row.id }else if(id.column == 'email'){ - show('Correo') + enviar_correo(row) } } diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 9ca919d..e38e58c 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -3,6 +3,7 @@ var menu_data = [ {id: 'app_home', icon: 'dashboard', value: 'Inicio'}, {id: 'app_emisor', icon: 'user-circle', value: 'Emisor'}, {id: 'app_folios', icon: 'sort-numeric-asc', value: 'Folios'}, + {id: 'app_correo', icon: 'envelope-o', value: 'Correo'}, ] @@ -205,6 +206,55 @@ var emisor_folios = [ ] +var emisor_correo = [ + {template: 'Servidor de Salida', type: 'section'}, + {cols: [ + {view: 'text', id: 'correo_servidor', name: 'correo_servidor', + label: 'Servidor SMTP: '}, + {}]}, + {cols: [ + {view: 'counter', id: 'correo_puerto', name: 'correo_puerto', + label: 'Puerto: ', value: 26, step: 1}, + {}]}, + {cols: [ + {view: 'checkbox', id: 'correo_ssl', name: 'correo_ssl', + label: 'Usar TLS/SSL: '}, + {}]}, + {cols: [ + {view: 'text', id: 'correo_usuario', name: 'correo_usuario', + label: 'Usuario: '}, + {}]}, + {cols: [ + {view: 'text', id: 'correo_contra', name: 'correo_contra', + label: 'Contraseña: ', type: 'password'}, + {}]}, + {cols: [ + {view: 'text', id: 'correo_copia', name: 'correo_copia', + label: 'Con copia a: '} + ]}, + {cols: [ + {view: 'text', id: 'correo_asunto', name: 'correo_asunto', + label: 'Asunto del correo: '} + ]}, + {cols: [ + {view: 'textarea', id: 'correo_mensaje', name: 'correo_mensaje', + label: 'Mensaje del correo: ', height: 200} + ]}, + {cols: [ + {view: 'checkbox', id: 'correo_directo', name: 'correo_directo', + label: 'Enviar directamente: '}, + {}]}, + {minHeight: 25}, + {cols: [{}, + {view: 'button', id: 'cmd_probar_correo', label: 'Probar Configuración', + autowidth: true, type: 'form'}, + {maxWidth: 100}, + {view: 'button', id: 'cmd_guardar_correo', label: 'Guardar Configuración', + autowidth: true, type: 'form'}, + {}]} +] + + var controls_folios = [ { view: 'tabview', @@ -219,6 +269,20 @@ var controls_folios = [ ] +var controls_correo = [ + { + view: 'tabview', + id: 'tab_correo', + tabbar: {options: ['Correo Electrónico']}, + animate: true, + cells: [ + {id: 'Correo Electrónico', rows: emisor_correo}, + {}, + ] + } +] + + var form_folios = { type: 'space', cols: [{ @@ -239,6 +303,22 @@ var form_folios = { } +var form_correo = { + type: 'space', + cols: [{ + view: 'form', + id: 'form_correo', + complexData: true, + elements: controls_correo, + elementsConfig: { + labelWidth: 150, + labelAlign: 'right' + }, + autoheight: true + }] +} + + var app_emisor = { id: 'app_emisor', rows:[ @@ -267,6 +347,17 @@ var app_folios = { } +var app_correo = { + id: 'app_correo', + rows:[ + {view: 'template', id: 'th_correo', type: 'header', + template: 'Configuración de correo'}, + form_correo, + {}, + ] +} + + var multi_admin = { id: 'multi_admin', animate: true, @@ -278,6 +369,7 @@ var multi_admin = { }, app_emisor, app_folios, + app_correo, ] }