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,
]
}