diff --git a/CHANGELOG.md b/CHANGELOG.md
index feeab18..d942214 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,35 @@
+v 1.28.0 [17-feb-2019]
+----------------------
+ - Mejora: Manejo de empaques para mensajeria
+ - Mejora: Usar concepto personalizado en deducciones de nómina 004 Otros
+ - Mejora: Búsqueda en notas
+ - Mejora: Soporte para el complemento de Divisas
+ - Mejora: Descarga de nómina en lote
+
+* IMPORTANTE:
+Es necesario realizar una migración, despues de actualizar.
+
+```
+git pull origin master
+
+cd source/app/models
+
+python main.py -bk
+
+python main.py -m
+```
+
+Es necesario agregar un nuevo requerimiento.
+
+```
+sudo pip install cryptography
+```
+
+**IMPORTANTE** Si envias tus facturas por correo directamente, es necesario
+volver a capturar la contraseña de tu servidor de correo y guardar los datos
+nuevamente.
+
+
v 1.27.1 [23-ene-2019]
----------------------
- Error: Al cancelar nómina
diff --git a/VERSION b/VERSION
index 08002f8..cfc7307 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.27.1
+1.28.0
diff --git a/requirements.txt b/requirements.txt
index 35ebb91..21de129 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,3 +13,4 @@ pyqrcode
pypng
reportlab
psycopg2-binary
+cryptography
diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py
index ec369ee..7054bbe 100644
--- a/source/app/controllers/cfdi_xml.py
+++ b/source/app/controllers/cfdi_xml.py
@@ -84,6 +84,12 @@ SAT = {
'xmlns': 'http://www.sat.gob.mx/Pagos',
'schema': ' http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd',
},
+ 'divisas': {
+ 'version': '1.0',
+ 'prefix': 'divisas',
+ 'xmlns': 'http://www.sat.gob.mx/divisas',
+ 'schema': ' http://www.sat.gob.mx/divisas http://www.sat.gob.mx/sitio_internet/cfd/divisas/divisas.xsd',
+ },
}
@@ -101,6 +107,7 @@ class CFDI(object):
self._edu = False
self._pagos = False
self._is_nomina = False
+ self._divisas = ''
self.error = ''
def _now(self):
@@ -152,6 +159,8 @@ class CFDI(object):
self._ine = True
self._pagos = bool(datos['complementos'].get('pagos', False))
+ self._divisas = datos['comprobante'].pop('divisas', '')
+
if 'nomina' in datos:
self._is_nomina = True
return self._validate_nomina(datos)
@@ -190,6 +199,12 @@ class CFDI(object):
attributes[name] = SAT['edu']['xmlns']
schema_edu = SAT['edu']['schema']
+ schema_divisas = ''
+ if self._divisas:
+ name = 'xmlns:{}'.format(SAT['divisas']['prefix'])
+ attributes[name] = SAT['divisas']['xmlns']
+ schema_divisas = SAT['divisas']['schema']
+
schema_nomina = ''
if self._is_nomina:
name = 'xmlns:{}'.format(SAT['nomina']['prefix'])
@@ -204,7 +219,7 @@ class CFDI(object):
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
schema_locales + schema_donativo + schema_ine + schema_edu + \
- schema_nomina + schema_pagos
+ schema_divisas + schema_nomina + schema_pagos
attributes.update(datos)
if not 'Version' in attributes:
@@ -426,6 +441,13 @@ class CFDI(object):
self._complemento = ET.SubElement(
self._cfdi, '{}:Complemento'.format(self._pre))
+ if self._divisas:
+ atributos = {
+ 'version': SAT['divisas']['version'],
+ 'tipoOperacion': self._divisas,
+ }
+ ET.SubElement(self._complemento, 'divisas:Divisas', atributos)
+
if 'ine' in datos:
atributos = {'Version': SAT['ine']['version']}
atributos.update(datos['ine'])
diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py
index 5952af9..9919aca 100644
--- a/source/app/controllers/main.py
+++ b/source/app/controllers/main.py
@@ -47,6 +47,7 @@ class AppLogin(object):
session.invalidate()
values = req.params
values['rfc'] = values['rfc'].upper()
+ values['ip'] = req.remote_addr
result, user = self._db.authenticate(values)
if result['login']:
session.save()
@@ -485,7 +486,14 @@ class AppNomina(object):
def on_get(self, req, resp):
values = req.params
+ by = values.get('by', '')
req.context['result'] = self._db.get_nomina(values)
+ if 'download' in by:
+ name = req.context['result']['name']
+ req.context['blob'] = req.context['result']['data']
+ resp.content_type = 'application/octet-stream'
+ resp.append_header(
+ 'Content-Disposition', f'attachment; filename={name}')
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index 6b1a6e8..f4c2e2b 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -872,7 +872,7 @@ class LIBO(object):
currency = self.CELL_STYLE.get(self._currency, 'peso')
return '{}{}'.format(currency, match.groups()[1])
- def _conceptos(self, data):
+ def _conceptos(self, data, pakings):
first = True
col1 = []
col2 = []
@@ -881,8 +881,9 @@ class LIBO(object):
col5 = []
col6 = []
col7 = []
+ col8 = []
count = len(data) - 1
- for concepto in data:
+ for i, concepto in enumerate(data):
key = concepto.get('noidentificacion', '')
description = concepto['descripcion']
unidad = concepto['unidad']
@@ -899,6 +900,8 @@ class LIBO(object):
cell_5 = self._set_cell('{valorunitario}', valor_unitario, value=True)
cell_6 = self._set_cell('{importe}', importe, value=True)
cell_7 = self._set_cell('{descuento}', descuento, value=True)
+ if pakings:
+ cell_8 = self._set_cell('{empaque}', pakings[i], value=True)
if len(data) > 1:
row = cell_1.getCellAddress().Row + 1
self._sheet.getRows().insertByIndex(row, count)
@@ -912,6 +915,8 @@ class LIBO(object):
col5.append((float(valor_unitario),))
col6.append((float(importe),))
col7.append((float(descuento),))
+ if pakings:
+ col8.append((pakings[i],))
self._total_cantidades += float(cantidad)
if not count:
return
@@ -919,6 +924,9 @@ class LIBO(object):
style_5 = self._get_style(cell_5)
style_6 = self._get_style(cell_6)
style_7 = self._get_style(cell_7)
+ style_8 = ''
+ if pakings:
+ style_8 = self._get_style(cell_8)
col = cell_1.getCellAddress().Column
target1 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
@@ -933,9 +941,13 @@ class LIBO(object):
col = cell_6.getCellAddress().Column
target6 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
target7 = None
+ target8 = None
if not cell_7 is None:
col = cell_7.getCellAddress().Column
target7 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
+ if pakings:
+ col = cell_8.getCellAddress().Column
+ target8 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
target1.setFormulaArray(tuple(col1))
target2.setDataArray(tuple(col2))
@@ -945,6 +957,8 @@ class LIBO(object):
target6.setDataArray(tuple(col6))
if not target7 is None:
target7.setDataArray(tuple(col7))
+ if not target8 is None:
+ target8.setDataArray(tuple(col8))
if style_5:
cell_5.CellStyle = style_5
@@ -955,6 +969,9 @@ class LIBO(object):
if style_7:
cell_7.CellStyle = style_7
target7.CellStyle = style_7
+ if style_8:
+ cell_8.CellStyle = style_8
+ target8.CellStyle = style_8
return
def _add_totales(self, data):
@@ -1067,6 +1084,12 @@ class LIBO(object):
self._set_cell('{ine.%s}' % k, v)
return
+ def _divisas(self, data):
+ if data:
+ for k, v in data.items():
+ self._set_cell(f'{{divisas.{k}}}', v)
+ return
+
def _nomina(self, data):
if not data:
return
@@ -1275,10 +1298,12 @@ class LIBO(object):
self._currency = data['totales']['moneda']
self._pagos = data.pop('pagos', False)
+ pakings = data.pop('pakings', [])
+
self._comprobante(data['comprobante'])
self._emisor(data['emisor'])
self._receptor(data['receptor'])
- self._conceptos(data['conceptos'])
+ self._conceptos(data['conceptos'], pakings)
if self._pagos:
self._cfdipays(data['pays'])
@@ -1291,6 +1316,8 @@ class LIBO(object):
self._donataria(data['donataria'])
self._ine(data['ine'])
+ self._divisas(data.get('divisas', {}))
+
self._cancelado(data['cancelada'])
self._clean()
return
@@ -1298,6 +1325,7 @@ class LIBO(object):
def pdf(self, path, data, ods=False):
options = {'AsTemplate': True, 'Hidden': True}
log.debug('Abrir plantilla...')
+
self._template = self._doc_open(path, options)
if self._template is None:
return b''
@@ -1444,7 +1472,16 @@ class LIBO(object):
return {}, msg
data = tuple([r[2:] for r in rows[:count+2]])
- return data, ''
+
+ sheet = doc.Sheets['Deducciones']
+ notes = sheet.getAnnotations()
+ new_titles = {}
+ for n in notes:
+ col = n.getPosition().Column - 2
+ if data[0][col] == '004':
+ new_titles[col] = n.getString()
+
+ return data, new_titles, ''
def _get_otros_pagos(self, doc, count):
rows, msg = self._get_data(doc, 'OtrosPagos')
@@ -1508,7 +1545,7 @@ class LIBO(object):
doc.close(True)
return {}, msg
- deducciones, msg = self._get_deducciones(doc, len(nomina))
+ deducciones, new_titles, msg = self._get_deducciones(doc, len(nomina))
if msg:
doc.close(True)
return {}, msg
@@ -1534,6 +1571,28 @@ class LIBO(object):
return {}, msg
doc.close(True)
+
+ rows = len(nomina) + 2
+
+ if rows != len(percepciones):
+ msg = 'Cantidad de filas incorrecta en: Percepciones'
+ return {}, msg
+ if rows != len(deducciones):
+ msg = 'Cantidad de filas incorrecta en: Deducciones'
+ return {}, msg
+ if rows != len(otros_pagos):
+ msg = 'Cantidad de filas incorrecta en: Otros Pagos'
+ return {}, msg
+ if rows != len(separacion):
+ msg = 'Cantidad de filas incorrecta en: Separación'
+ return {}, msg
+ if rows != len(horas_extras):
+ msg = 'Cantidad de filas incorrecta en: Horas Extras'
+ return {}, msg
+ if rows != len(incapacidades):
+ msg = 'Cantidad de filas incorrecta en: Incapacidades'
+ return {}, msg
+
data['nomina'] = nomina
data['percepciones'] = percepciones
data['deducciones'] = deducciones
@@ -1541,6 +1600,7 @@ class LIBO(object):
data['separacion'] = separacion
data['horas_extras'] = horas_extras
data['incapacidades'] = incapacidades
+ data['new_titles'] = new_titles
return data, ''
@@ -1563,11 +1623,13 @@ class LIBO(object):
def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'):
rfc = data['emisor']['rfc']
+ default = 'plantilla_factura.ods'
if DEBUG:
rfc = emisor_rfc
version = data['comprobante']['version']
if 'nomina' in data and data['nomina']:
version = '{}_{}'.format(data['nomina']['version'], version)
+ default = 'plantilla_nomina.ods'
pagos = ''
if data.get('pagos', False):
@@ -1584,7 +1646,7 @@ def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'):
if data['donativo']:
donativo = '_donativo'
name = '{}_{}{}{}.ods'.format(rfc.lower(), pagos, version, donativo)
- path = get_template_ods(name)
+ path = get_template_ods(name, default)
if path:
return app.pdf(path, data, ods)
@@ -2105,7 +2167,7 @@ def get_data_from_xml(invoice, values):
data['pagos'] = values.get('pagos', False)
if data['pagos']:
data['pays'] = _cfdipays(doc, data, version)
-
+ data['pakings'] = values.get('pakings', [])
return data
@@ -3764,3 +3826,5 @@ def validate_rfc(value):
def parse_xml2(xml_str):
return etree.fromstring(xml_str.encode('utf-8'))
+
+
diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py
new file mode 100644
index 0000000..192ff6f
--- /dev/null
+++ b/source/app/controllers/utils.py
@@ -0,0 +1,295 @@
+#!/usr/bin/env python3
+
+# ~ Empresa Libre
+# ~ Copyright (C) 2016-2019 Mauricio Baeza Servin (public@elmau.net)
+# ~
+# ~ This program is free software: you can redistribute it and/or modify
+# ~ it under the terms of the GNU General Public License as published by
+# ~ the Free Software Foundation, either version 3 of the License, or
+# ~ (at your option) any later version.
+# ~
+# ~ This program is distributed in the hope that it will be useful,
+# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# ~ GNU General Public License for more details.
+# ~
+# ~ You should have received a copy of the GNU General Public License
+# ~ along with this program. If not, see .
+
+import base64
+import collections
+import datetime
+import math
+import smtplib
+import zipfile
+
+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
+from io import BytesIO
+
+import lxml.etree as ET
+import requests
+
+from cryptography.fernet import Fernet
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from dateutil import parser
+
+
+TIMEOUT = 10
+
+
+#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
+class CaseInsensitiveDict(collections.MutableMapping):
+ """
+ A case-insensitive ``dict``-like object.
+ Implements all methods and operations of
+ ``collections.MutableMapping`` as well as dict's ``copy``. Also
+ provides ``lower_items``.
+ All keys are expected to be strings. The structure remembers the
+ case of the last key to be set, and ``iter(instance)``,
+ ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
+ will contain case-sensitive keys. However, querying and contains
+ testing is case insensitive:
+ cid = CaseInsensitiveDict()
+ cid['Accept'] = 'application/json'
+ cid['aCCEPT'] == 'application/json' # True
+ list(cid) == ['Accept'] # True
+ For example, ``headers['content-encoding']`` will return the
+ value of a ``'Content-Encoding'`` response header, regardless
+ of how the header name was originally stored.
+ If the constructor, ``.update``, or equality comparison
+ operations are given keys that have equal ``.lower()``s, the
+ behavior is undefined.
+ """
+ def __init__(self, data=None, **kwargs):
+ self._store = dict()
+ if data is None:
+ data = {}
+ self.update(data, **kwargs)
+
+ def __setitem__(self, key, value):
+ # Use the lowercased key for lookups, but store the actual
+ # key alongside the value.
+ self._store[key.lower()] = (key, value)
+
+ def __getitem__(self, key):
+ return self._store[key.lower()][1]
+
+ def __delitem__(self, key):
+ del self._store[key.lower()]
+
+ def __iter__(self):
+ return (casedkey for casedkey, mappedvalue in self._store.values())
+
+ def __len__(self):
+ return len(self._store)
+
+ def lower_items(self):
+ """Like iteritems(), but with all lowercase keys."""
+ return (
+ (lowerkey, keyval[1])
+ for (lowerkey, keyval)
+ in self._store.items()
+ )
+
+ def __eq__(self, other):
+ if isinstance(other, collections.Mapping):
+ other = CaseInsensitiveDict(other)
+ else:
+ return NotImplemented
+ # Compare insensitively
+ return dict(self.lower_items()) == dict(other.lower_items())
+
+ # Copy is required
+ def copy(self):
+ return CaseInsensitiveDict(self._store.values())
+
+ def __repr__(self):
+ return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
+
+
+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):
+ hosts = ('gmail' in self._config['server'] or
+ 'outlook' in self._config['server'])
+ try:
+ if self._config['ssl'] and hosts:
+ self._server = smtplib.SMTP(
+ self._config['server'],
+ self._config['port'], timeout=TIMEOUT)
+ self._server.ehlo()
+ self._server.starttls()
+ self._server.ehlo()
+ elif self._config['ssl']:
+ self._server = smtplib.SMTP_SSL(
+ self._config['server'],
+ self._config['port'], timeout=TIMEOUT)
+ self._server.ehlo()
+ else:
+ self._server = smtplib.SMTP(
+ self._config['server'],
+ self._config['port'], timeout=TIMEOUT)
+ self._server.login(self._config['user'], self._config['pass'])
+ 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['server']:
+ 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['user']
+ message['To'] = options['to']
+ message['CC'] = options.get('copy', '')
+ message['Subject'] = options['subject']
+ message['Date'] = formatdate(localtime=True)
+ if options.get('confirm', False):
+ message['Disposition-Notification-To'] = message['From']
+ message.attach(MIMEText(options['message'], 'html'))
+ for f in options.get('files', ()):
+ part = MIMEBase('application', 'octet-stream')
+ if isinstance(f[0], str):
+ part.set_payload(f[0].encode('utf-8'))
+ else:
+ part.set_payload(f[0])
+ encoders.encode_base64(part)
+ part.add_header(
+ 'Content-Disposition',
+ "attachment; filename={}".format(f[1]))
+ message.attach(part)
+
+ receivers = options['to'].split(',') + message['CC'].split(',')
+ self._server.sendmail(
+ self._config['user'], receivers, message.as_string())
+ return ''
+ except Exception as e:
+ return str(e)
+
+ def close(self):
+ try:
+ self._server.quit()
+ except:
+ pass
+ return
+
+
+class CfdiToDict(object):
+ NS = {
+ 'cfdi': 'http://www.sat.gob.mx/cfd/3',
+ 'divisas': 'http://www.sat.gob.mx/divisas',
+ }
+
+ def __init__(self, xml):
+ self._values = {}
+ self._root = ET.parse(BytesIO(xml.encode())).getroot()
+ self._get_values()
+
+ @property
+ def values(self):
+ return self._values
+
+ def _get_values(self):
+ self._complementos()
+ return
+
+ def _complementos(self):
+ complemento = self._root.xpath('//cfdi:Complemento', namespaces=self.NS)[0]
+
+ divisas = complemento.xpath('//divisas:Divisas', namespaces=self.NS)
+ if divisas:
+ d = CaseInsensitiveDict(divisas[0].attrib)
+ d.pop('version', '')
+ self._values.update({'divisas': d})
+ return
+
+def send_mail(data):
+ msg = ''
+ ok = True
+ server = SendMail(data['server'])
+ if server.is_connect:
+ msg = server.send(data['mail'])
+ else:
+ msg = server.error
+ ok = False
+ server.close()
+
+ return {'ok': ok, 'msg': msg}
+
+
+def round_up(value):
+ return int(math.ceil(value))
+
+
+def _get_key(password):
+ digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
+ digest.update(password.encode())
+ key = base64.urlsafe_b64encode(digest.finalize())
+ return key
+
+
+def encrypt(data, password):
+ f = Fernet(_get_key(password))
+ return f.encrypt(data.encode()).decode()
+
+
+def decrypt(data, password):
+ f = Fernet(_get_key(password))
+ return f.decrypt(data.encode()).decode()
+
+
+def to_bool(value):
+ return bool(int(value))
+
+
+def get_url(url):
+ r = requests.get(url).text
+ return r
+
+
+def parse_date(value, next_day=False):
+ d = parser.parse(value)
+ if next_day:
+ return d + datetime.timedelta(days=1)
+ return d
+
+
+def to_zip(files):
+ zip_buffer = BytesIO()
+
+ with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:
+ for file_name, data in files.items():
+ zip_file.writestr(file_name, data)
+
+ return zip_buffer.getvalue()
+
diff --git a/source/app/middleware.py b/source/app/middleware.py
index 9c873c1..24da94a 100644
--- a/source/app/middleware.py
+++ b/source/app/middleware.py
@@ -65,9 +65,15 @@ class JSONTranslator(object):
def process_response(self, req, resp, resource):
if 'result' not in req.context:
return
+
if '/doc/' in req.path:
resp.body = req.context['result']
return
+
+ if 'blob' in req.context:
+ resp.body = req.context['blob']
+ return
+
resp.body = util.dumps(req.context['result'])
diff --git a/source/app/models/db.py b/source/app/models/db.py
index a3de267..ca73fd0 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -384,8 +384,10 @@ class StorageEngine(object):
def get_tickets(self, values):
return main.Tickets.get_by(values)
- def get_invoices(self, values):
- return main.Facturas.get_(values)
+ def get_invoices(self, filters):
+ if filters.get('by', ''):
+ return main.Facturas.get_by(filters)
+ return main.Facturas.get_(filters)
def get_preinvoices(self, values):
return main.PreFacturas.get_(values)
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 615c0de..c9502cb 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -29,8 +29,12 @@ if __name__ == '__main__':
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
+# ~ v2
+from controllers import utils
+# ~ v1
from controllers import util
+
from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \
CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \
@@ -286,6 +290,7 @@ def config_timbrar():
'cfdi_anticipo': Configuracion.get_('chk_config_anticipo'),
'cfdi_ine': Configuracion.get_bool('chk_config_ine'),
'cfdi_edu': Configuracion.get_bool('chk_config_edu'),
+ 'cfdi_divisas': Configuracion.get_bool('chk_config_divisas'),
'cfdi_metodo_pago': Configuracion.get_bool('chk_config_ocultar_metodo_pago'),
'cfdi_condicion_pago': Configuracion.get_bool('chk_config_ocultar_condiciones_pago'),
'cfdi_open_pdf': Configuracion.get_bool('chk_config_open_pdf'),
@@ -347,6 +352,17 @@ class Configuracion(BaseModel):
msg = 'No se pudo guardar la configuración'
return {'ok': result, 'msg': msg}
+ def _save_mail(self, values):
+ rfc = Emisor.select()[0].rfc
+ values['correo_contra'] = utils.encrypt(values['correo_contra'], rfc)
+
+ for k, v in values.items():
+ obj, created = Configuracion.get_or_create(clave=k)
+ obj.valor = v
+ obj.save()
+
+ return {'ok': True}
+
@classmethod
def get_bool(cls, key):
data = (Configuracion
@@ -368,11 +384,45 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data}
return values
+
+ def _get_admin_products(self):
+ fields = (
+ 'chk_config_cuenta_predial',
+ 'chk_config_codigo_barras',
+ 'chk_config_precio_con_impuestos',
+ 'chk_llevar_inventario',
+ 'chk_use_packing',
+ )
+ data = (Configuracion
+ .select()
+ .where(Configuracion.clave.in_(fields))
+ )
+ values = {r.clave: util.get_bool(r.valor) for r in data}
+ return values
+
+ def _get_main_products(self):
+ fields = (
+ 'chk_config_cuenta_predial',
+ 'chk_config_codigo_barras',
+ 'chk_config_precio_con_impuestos',
+ 'chk_llevar_inventario',
+ 'chk_use_packing',
+ )
+ data = (Configuracion
+ .select()
+ .where(Configuracion.clave.in_(fields))
+ )
+ values = {r.clave: r.valor for r in data}
+ values['default_tax'] = SATImpuestos.select()[0].id
+ values['default_unidad'] = SATUnidades.get_default()
+ return values
+
def _get_complements(self):
fields = (
'chk_config_ine',
'chk_config_edu',
'chk_config_pagos',
+ 'chk_config_divisas',
'chk_cfg_pays_data_bank',
)
data = (Configuracion
@@ -401,6 +451,28 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data}
return values
+ def _get_correo(self):
+ fields = ('correo_servidor', 'correo_puerto', 'correo_ssl',
+ 'correo_usuario', 'correo_copia', 'correo_asunto',
+ 'correo_mensaje', 'correo_directo', 'correo_confirmacion')
+ data = (Configuracion
+ .select()
+ .where(Configuracion.clave.in_(fields))
+ )
+ values = {r.clave: r.valor for r in data}
+ return values
+
+ def _get_admin_config_users(self):
+ fields = (
+ 'chk_users_notify_access',
+ )
+ data = (Configuracion
+ .select()
+ .where(Configuracion.clave.in_(fields))
+ )
+ values = {r.clave: util.get_bool(r.valor) for r in data}
+ return values
+
@classmethod
def get_(cls, keys):
if isinstance(keys, str):
@@ -412,27 +484,18 @@ class Configuracion(BaseModel):
return data[0].valor
return ''
- options = ('partners', 'complements', 'folios')
+ options = ('partners',
+ 'admin_products',
+ 'main_products',
+ 'complements',
+ 'folios',
+ 'correo',
+ 'admin_config_users',
+ )
opt = keys['fields']
if opt in options:
return getattr(cls, '_get_{}'.format(opt))(cls)
- if keys['fields'] == 'productos':
- fields = (
- 'chk_config_cuenta_predial',
- 'chk_config_codigo_barras',
- 'chk_config_precio_con_impuestos',
- 'chk_llevar_inventario',
- )
- data = (Configuracion
- .select()
- .where(Configuracion.clave.in_(fields))
- )
- values = {r.clave: r.valor for r in data}
- values['default_tax'] = SATImpuestos.select()[0].id
- values['default_unidad'] = SATUnidades.get_default()
- return values
-
if keys['fields'] == 'configtemplates':
try:
emisor = Emisor.select()[0]
@@ -465,10 +528,6 @@ class Configuracion(BaseModel):
'chk_config_tax_locales_truncate',
'chk_config_decimales_precios',
'chk_config_anticipo',
- 'chk_config_cuenta_predial',
- 'chk_config_codigo_barras',
- 'chk_config_precio_con_impuestos',
- 'chk_llevar_inventario',
'chk_usar_punto_de_venta',
'chk_ticket_pdf_show',
'chk_ticket_direct_print',
@@ -493,16 +552,7 @@ class Configuracion(BaseModel):
values[f] = Configuracion.get_(f)
return values
- if keys['fields'] == 'correo':
- fields = ('correo_servidor', 'correo_puerto', 'correo_ssl',
- 'correo_usuario', 'correo_contra', 'correo_copia',
- 'correo_asunto', 'correo_mensaje', 'correo_directo',
- 'correo_confirmacion')
- data = (Configuracion
- .select()
- .where(Configuracion.clave.in_(fields))
- )
- elif keys['fields'] == 'path_cer':
+ if keys['fields'] == 'path_cer':
fields = ('path_key', 'path_cer')
data = (Configuracion
.select()
@@ -3204,6 +3254,8 @@ class Productos(BaseModel):
es_activo = BooleanField(default=True)
impuestos = ManyToManyField(SATImpuestos, related_name='productos')
tags = ManyToManyField(Tags, related_name='productos_tags')
+ cantidad_empaque = DecimalField(default=0.0, max_digits=14, decimal_places=4,
+ auto_round=True)
class Meta:
order_by = ('descripcion',)
@@ -3415,6 +3467,7 @@ class Productos(BaseModel):
Productos.inventario,
Productos.existencia,
Productos.minimo,
+ Productos.cantidad_empaque.alias('cant_by_packing'),
)
.where(Productos.id==id).dicts()[0]
)
@@ -3442,6 +3495,7 @@ class Productos(BaseModel):
descripcion = util.spaces(values.pop('descripcion'))
fields = util.clean(values)
+ fields['cantidad_empaque'] = fields.pop('cant_by_packing', 0.0)
fields.pop('precio_con_impuestos', '')
fields['es_activo'] = fields.pop('es_activo_producto')
fields['descripcion'] = descripcion
@@ -3484,6 +3538,7 @@ class Productos(BaseModel):
def actualizar(cls, values, id):
values['cuenta_predial'] = values.get('cuenta_predial', '')
values['codigo_barras'] = values.get('codigo_barras', '')
+
fields, taxes = cls._clean(cls, values)
obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes))
with database_proxy.transaction():
@@ -3577,6 +3632,7 @@ class Facturas(BaseModel):
estatus = TextField(default='Guardada')
estatus_sat = TextField(default='Vigente')
regimen_fiscal = TextField(default='')
+ divisas = TextField(default='')
notas = TextField(default='')
saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
@@ -3663,6 +3719,65 @@ class Facturas(BaseModel):
obj.save()
return data
+ def _get_filters(self, values):
+ if 'start' in values:
+ filters = Facturas.fecha.between(
+ utils.parse_date(values['start']),
+ utils.parse_date(values['end'], True)
+ )
+ else:
+ if values['year'] == '-1':
+ fy = (Facturas.fecha.year > 0)
+ else:
+ fy = (Facturas.fecha.year == int(values['year']))
+ if values['month'] == '-1':
+ fm = (Facturas.fecha.month > 0)
+ else:
+ fm = (Facturas.fecha.month == int(values['month']))
+ filters = (fy & fm)
+
+ if 'client' in values:
+ filters &= (Socios.nombre==values['client'])
+
+ return filters
+
+ def _get_invoices(self, filters):
+ rows = tuple(Facturas.select(
+ Facturas.id,
+ Facturas.serie,
+ Facturas.folio,
+ Facturas.uuid,
+ Facturas.fecha,
+ Facturas.tipo_comprobante,
+ Facturas.estatus,
+ case(Facturas.pagada, (
+ (True, 'Si'),
+ (False, 'No'),
+ )).alias('paid'),
+ Facturas.total,
+ Facturas.moneda.alias('currency'),
+ Facturas.total_mn,
+ Socios.nombre.alias('cliente'))
+ .where(filters)
+ .join(Socios)
+ .switch(Facturas).dicts()
+ )
+ return {'ok': True, 'rows': rows}
+
+ def _get_by_dates(self, filters):
+ filters = self._get_filters(self, filters)
+ return self._get_invoices(self, filters)
+
+ def _get_by_notes(self, filters):
+ notes = filters['notes']
+ filters = self._get_filters(self, filters)
+ filters &= (Facturas.notas.contains(notes))
+ return self._get_invoices(self, filters)
+
+ @classmethod
+ def get_by(cls, filters):
+ return getattr(cls, f"_get_by_{filters['by']}")(cls, filters)
+
@classmethod
def filter_years(cls):
data = [{'id': -1, 'value': 'Todos'}]
@@ -3695,6 +3810,8 @@ class Facturas(BaseModel):
return {'ok': True, 'msg': 'Notas guardadas correctamente'}
def _get_not_in_xml(self, invoice, emisor):
+ pdf_from = Configuracion.get_('make_pdf_from') or '1'
+
values = {}
values['notas'] = invoice.notas
@@ -3732,6 +3849,16 @@ class Facturas(BaseModel):
for k, v in receptor.items():
values['receptor'][k] = v
+ use_packing = Configuracion.get_bool('chk_use_packing')
+ if use_packing:
+ w = FacturasDetalle.factura == invoice
+ q = (FacturasDetalle
+ .select(FacturasDetalle.empaques)
+ .where(w)
+ .order_by(FacturasDetalle.id.asc())
+ .tuples())
+ values['pakings'] = [str(int(r[0])) for r in q]
+
return values
@classmethod
@@ -3748,7 +3875,11 @@ class Facturas(BaseModel):
pdf_from = Configuracion.get_('make_pdf_from') or '1'
values = cls._get_not_in_xml(cls, obj, emisor)
+
+ #Tmp to v2
data = util.get_data_from_xml(obj, values)
+ data.update(utils.CfdiToDict(obj.xml).values)
+
doc = util.to_pdf(data, emisor.rfc, pdf_from=pdf_from)
if sync:
@@ -3980,6 +4111,7 @@ class Facturas(BaseModel):
@classmethod
def send(cls, id, rfc):
values = Configuracion.get_({'fields': 'correo'})
+ contra = Configuracion.get_('correo_contra')
in_zip = Configuracion.get_bool('chk_config_send_zip')
if not values:
@@ -4006,7 +4138,7 @@ class Facturas(BaseModel):
'puerto': values['correo_puerto'],
'ssl': bool(int(values['correo_ssl'])),
'usuario': values['correo_usuario'],
- 'contra': values['correo_contra'],
+ 'contra': utils.decrypt(contra, rfc),
}
options = {
'para': obj.cliente.correo_facturas,
@@ -4285,6 +4417,8 @@ class Facturas(BaseModel):
tax_locales = Configuracion.get_bool('chk_config_tax_locales')
tax_locales_truncate = Configuracion.get_bool('chk_config_tax_locales_truncate')
tax_decimals = Configuracion.get_bool('chk_config_tax_decimals')
+ use_packing = Configuracion.get_bool('chk_use_packing')
+
subtotal = 0
descuento_cfdi = 0
totals_tax = {}
@@ -4312,6 +4446,10 @@ class Facturas(BaseModel):
precio_final = valor_unitario - descuento
importe = round(cantidad * precio_final, DECIMALES)
+ if use_packing and p.cantidad_empaque:
+ product['empaques'] = utils.round_up(
+ cantidad / float(p.cantidad_empaque))
+
product['cantidad'] = cantidad
product['valor_unitario'] = valor_unitario
product['descuento'] = round(descuento * cantidad, DECIMALES)
@@ -4434,6 +4572,12 @@ class Facturas(BaseModel):
ine = values.pop('ine', {})
tipo_comprobante = values['tipo_comprobante']
folio_custom = values.pop('folio_custom', '')
+ divisas = values.pop('divisas', '')
+ if Configuracion.get_bool('chk_config_divisas'):
+ divisas = divisas.lower()
+ if divisas == 'ninguna':
+ divisas = ''
+ values['divisas'] = divisas
emisor = Emisor.select()[0]
values['serie'] = cls._get_serie(cls, user, values['serie'])
@@ -4504,6 +4648,9 @@ class Facturas(BaseModel):
relacionados = {}
donativo = {}
complementos = FacturasComplementos.get_(invoice)
+ comprobante['divisas'] = invoice.divisas
+ if invoice.divisas:
+ complementos['divisas'] = True
if invoice.donativo:
donativo['noAutorizacion'] = emisor.autorizacion
@@ -4736,6 +4883,7 @@ class Facturas(BaseModel):
'edu': is_edu,
'complementos': complementos,
}
+
return util.make_xml(data, certificado, auth)
@classmethod
@@ -5616,6 +5764,8 @@ class FacturasDetalle(BaseModel):
nivel = TextField(default='')
autorizacion = TextField(default='')
cuenta_predial = TextField(default='')
+ empaques = DecimalField(default=0.0, max_digits=14, decimal_places=4,
+ auto_round=True)
class Meta:
order_by = ('factura',)
@@ -7493,7 +7643,6 @@ class CfdiNomina(BaseModel):
data['fecha_pago'] = util.calc_to_date(row['fecha_pago'])
data['fecha_inicial_pago'] = util.calc_to_date(row['fecha_inicial_pago'])
data['fecha_final_pago'] = util.calc_to_date(row['fecha_final_pago'])
- # ~ data['dias_pagados'] = util.get_days(data['fecha_inicial_pago'], data['fecha_final_pago'])
data['dias_pagados'] = days_pay
return data, ''
@@ -7546,7 +7695,7 @@ class CfdiNomina(BaseModel):
return data, totals, ''
- def _validate_deducciones(self, headers, row):
+ def _validate_deducciones(self, headers, row, new_titles):
total_retenciones = 0.0
total_otras_deducciones = 0.0
@@ -7572,6 +7721,7 @@ class CfdiNomina(BaseModel):
new = {
'tipo_deduccion': td,
'importe': importe,
+ 'concepto': new_titles.get(i, ''),
}
data.append(new)
@@ -7735,6 +7885,7 @@ class CfdiNomina(BaseModel):
separacion = data['separacion'][2:]
horas_extras = data['horas_extras'][2:]
incapacidades = data['incapacidades'][2:]
+ new_titles = data['new_titles']
for i, row in enumerate(data['nomina']):
row['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
@@ -7756,7 +7907,7 @@ class CfdiNomina(BaseModel):
continue
new_deducciones, total_deducciones, msg = \
- self._validate_deducciones(self, hd, deducciones[i])
+ self._validate_deducciones(self, hd, deducciones[i], new_titles)
if msg:
util.log_file('nomina', msg)
continue
@@ -8190,11 +8341,34 @@ class CfdiNomina(BaseModel):
return {'ok': ok, 'msg_ok': msg, 'error': error, 'msg_error': msg_error}
+ def _get_by_download(self, filters):
+ emisor = Emisor.select()[0]
+ ids = util.loads(filters['ids'])
+ w = CfdiNomina.id.in_(ids)
+ rows = CfdiNomina.select().where(w)
+
+ files = {}
+ for row in rows:
+ name = '{}{}_{}'.format(row.serie, row.folio, row.empleado.rfc)
+ files[f'{name}.xml'] = row.xml
+
+ values = self._get_not_in_xml(self, row, emisor)
+ data = util.get_data_from_xml(row, values)
+ doc = util.to_pdf(data, emisor.rfc)
+ files[f'{name}.pdf'] = doc
+
+ fz = utils.to_zip(files)
+
+ return {'data': fz, 'name': name + 'zip'}
+
@classmethod
def get_by(cls, values):
if not values:
return cls._get(cls)
+ if values.get('by', ''):
+ return getattr(cls, f"_get_by_{values['by']}")(cls, values)
+
if values['opt'] == 'dates':
dates = util.loads(values['range'])
filters = CfdiNomina.fecha.between(
@@ -8508,6 +8682,50 @@ def _save_log(user, action, table):
return
+@util.run_in_thread
+def _send_notify_access(args):
+ admins = (Usuarios
+ .select(Usuarios.correo)
+ .where(Usuarios.es_admin==True)
+ .scalar(as_tuple=True))
+
+ if not admins:
+ return
+
+ config = Configuracion.get_({'fields': 'correo'})
+ contra = Configuracion.get_('correo_contra')
+ if not config:
+ return
+
+ user = args['usuario']
+ rfc = args['rfc']
+ ip = args['ip']
+
+ url = f"http://ip-api.com/line/{ip}?fields=city"
+ city = utils.get_url(url)
+ message = f"Desde la IP: {ip} en: {city}"
+
+ server = {
+ 'server': config['correo_servidor'],
+ 'port': config['correo_puerto'],
+ 'ssl': utils.to_bool(config['correo_ssl']),
+ 'user': config['correo_usuario'],
+ 'pass': utils.decrypt(contra, rfc),
+ }
+ mail = {
+ 'to': ','.join(admins),
+ 'subject': f"Usuario {user} identificado",
+ 'message': message,
+ }
+ data= {
+ 'server': server,
+ 'mail': mail,
+ }
+ result = utils.send_mail(data)
+
+ return
+
+
def authenticate(args):
respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''}
values = util.get_con(args['rfc'])
@@ -8532,7 +8750,11 @@ def authenticate(args):
respuesta['login'] = True
respuesta['user'] = str(obj)
respuesta['super'] = obj.es_superusuario
- #~ respuesta['admin'] = obj.es_superusuario or obj.es_admin
+
+ notify_access = Configuracion.get_bool('chk_users_notify_access')
+ if notify_access:
+ _send_notify_access(args)
+
return respuesta, obj
@@ -8835,10 +9057,34 @@ def _migrate_tables(rfc=''):
activa = BooleanField(default=True)
migrations.append(migrator.add_column(table, 'activa', activa))
+ table = 'productos'
+ columns = [c.name for c in database_proxy.get_columns(table)]
+ if not 'cantidad_empaque' in columns:
+ cantidad_empaque = DecimalField(default=0.0, max_digits=14,
+ decimal_places=4, auto_round=True)
+ migrations.append(migrator.add_column(
+ table, 'cantidad_empaque', cantidad_empaque))
+
+ table = 'facturasdetalle'
+ columns = [c.name for c in database_proxy.get_columns(table)]
+ if not 'empaques' in columns:
+ empaques = DecimalField(default=0.0, max_digits=14,
+ decimal_places=4, auto_round=True)
+ migrations.append(migrator.add_column(
+ table, 'empaques', empaques))
+
+ table = 'facturas'
+ columns = [c.name for c in database_proxy.get_columns(table)]
+ if not 'divisas' in columns:
+ divisas = TextField(default='')
+ migrations.append(migrator.add_column(table, 'divisas', divisas))
+
if migrations:
with database_proxy.atomic() as txn:
migrate(*migrations)
+ Configuracion.add({'version': VERSION})
+
log.info('Tablas migradas correctamente...')
_importar_valores('', rfc)
diff --git a/source/app/settings.py b/source/app/settings.py
index 1f3bedc..74da420 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -47,8 +47,8 @@ except ImportError:
DEBUG = DEBUG
-VERSION = '1.27.1'
-EMAIL_SUPPORT = ('soporte@empresalibre.net',)
+VERSION = '1.28.0'
+EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
diff --git a/source/static/css/app.css b/source/static/css/app.css
index 624d382..873f688 100644
--- a/source/static/css/app.css
+++ b/source/static/css/app.css
@@ -47,6 +47,13 @@
font-size: 125%;
}
+.link_default {
+ font-weight: bold;
+ color: #610B0B;
+ text-decoration: none;
+}
+.link_default:hover {text-decoration:underline;}
+
.link_forum {
font-weight: bold;
color: #610B0B;
diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js
index f8920ea..e577f4d 100644
--- a/source/static/js/controller/admin.js
+++ b/source/static/js/controller/admin.js
@@ -59,11 +59,13 @@ var controllers = {
$$('grid_admin_unidades').attachEvent('onItemClick', grid_admin_unidades_click)
$$('grid_moneda_found').attachEvent('onValueSuggest', grid_moneda_found_click)
$$('cmd_agregar_impuesto').attachEvent('onItemClick', cmd_agregar_impuesto_click)
+
//~ Usuarios
$$('cmd_usuario_agregar').attachEvent('onItemClick', cmd_usuario_agregar_click)
$$('grid_usuarios').attachEvent('onItemClick', grid_usuarios_click)
$$('grid_usuarios').attachEvent('onCheck', grid_usuarios_on_check)
$$('grid_usuarios').attachEvent('onItemDblClick', grid_usuarios_double_click)
+ $$('chk_users_notify_access').attachEvent('onItemClick', chk_config_item_click)
admin_ui_windows.init()
//~ Opciones
@@ -83,6 +85,13 @@ var controllers = {
//~ Partners
$$('chk_config_change_balance_partner').attachEvent('onItemClick', chk_config_item_click)
+ //~ Products
+ $$('chk_config_cuenta_predial').attachEvent('onItemClick', chk_config_item_click)
+ $$('chk_config_codigo_barras').attachEvent('onItemClick', chk_config_item_click)
+ $$('chk_config_precio_con_impuestos').attachEvent('onItemClick', chk_config_item_click)
+ $$('chk_llevar_inventario').attachEvent('onItemClick', chk_config_item_click)
+ $$('chk_use_packing').attachEvent('onItemClick', chk_config_item_click)
+
$$('chk_config_ocultar_metodo_pago').attachEvent('onItemClick', chk_config_item_click)
$$('chk_config_ocultar_condiciones_pago').attachEvent('onItemClick', chk_config_item_click)
$$('chk_config_send_zip').attachEvent('onItemClick', chk_config_item_click)
@@ -98,11 +107,8 @@ var controllers = {
$$('chk_config_ine').attachEvent('onItemClick', chk_config_item_click)
$$('chk_config_edu').attachEvent('onItemClick', chk_config_item_click)
$$('chk_config_pagos').attachEvent('onItemClick', chk_config_item_click)
+ $$('chk_config_divisas').attachEvent('onItemClick', chk_config_item_click)
$$('chk_cfg_pays_data_bank').attachEvent('onItemClick', chk_config_item_click)
- $$('chk_config_cuenta_predial').attachEvent('onItemClick', chk_config_item_click)
- $$('chk_config_codigo_barras').attachEvent('onItemClick', chk_config_item_click)
- $$('chk_config_precio_con_impuestos').attachEvent('onItemClick', chk_config_item_click)
- $$('chk_llevar_inventario').attachEvent('onItemClick', chk_config_item_click)
$$('chk_usar_punto_de_venta').attachEvent('onItemClick', chk_config_item_click)
$$('chk_ticket_pdf_show').attachEvent('onItemClick', chk_config_item_click)
$$('chk_ticket_direct_print').attachEvent('onItemClick', chk_config_item_click)
@@ -405,9 +411,22 @@ function get_admin_usos_cfdi(){
function get_admin_usuarios(){
webix.ajax().sync().get('/values/allusuarios', function(text, data){
- var values = data.json()
+ var rows = data.json()
$$('grid_usuarios').clearAll()
- $$('grid_usuarios').parse(values, 'json')
+ $$('grid_usuarios').parse(rows)
+ })
+
+ webix.ajax().get('/config', {'fields': 'admin_config_users'}, {
+ error: function(text, data, xhr) {
+ msg = 'Error al consultar'
+ msg_error(msg)
+ },
+ success: function(text, data, xhr) {
+ var values = data.json()
+ Object.keys(values).forEach(function(key){
+ $$(key).setValue(values[key])
+ })
+ }
})
}
@@ -444,7 +463,6 @@ function get_config_values(opt){
},
success: function(text, data, xhr) {
var values = data.json()
- //~ showvar(values)
Object.keys(values).forEach(function(key){
$$(key).setValue(values[key])
})
@@ -814,6 +832,7 @@ function cmd_probar_correo_click(){
function save_config_mail(values){
+ values['opt'] = 'save_mail'
webix.ajax().sync().post('/config', values, {
error: function(text, data, xhr) {
msg = 'Error al guardar la configuración'
@@ -1277,6 +1296,7 @@ function tab_options_change(nv, ov){
var cv = {
tab_admin_templates: 'templates',
tab_admin_partners: 'partners',
+ tab_admin_products: 'admin_products',
tab_admin_complements: 'complements',
tab_admin_otros: 'configotros',
}
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index 0d607d1..6e8cd2c 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -82,6 +82,9 @@ var invoices_controllers = {
$$('txt_folio_custom').attachEvent('onKeyPress', txt_folio_custom_key_press);
$$('txt_folio_custom').attachEvent('onBlur', txt_folio_custom_lost_focus);
+ $$('search_by').attachEvent('onKeyPress', search_by_key_press)
+ $$('search_by').attachEvent('onItemClick', search_by_click)
+
webix.extend($$('grid_invoices'), webix.ProgressBar)
init_config_invoices()
@@ -217,6 +220,7 @@ function default_config(){
$$('grid_details').showColumn('student')
}
show('fs_students', values.cfdi_edu)
+ show('fs_divisas', values.cfdi_divisas)
show('txt_folio_custom', values.cfdi_folio_custom)
})
}
@@ -638,6 +642,7 @@ function guardar_y_timbrar(values){
data['donativo'] = donativo
data['notas'] = values.notas
data['folio_custom'] = $$('txt_folio_custom').getValue()
+ data['divisas'] = $$('opt_divisas').getValue()
var usar_ine = $$('chk_cfdi_usar_ine').getValue()
if(usar_ine){
@@ -1376,20 +1381,23 @@ function cmd_invoice_cancelar_click(){
}
-function get_invoices(rango){
- if(rango == undefined){
- var fy = $$('filter_year')
- var fm = $$('filter_month')
+function get_filters_invoices(){
+ var filters = $$('filter_dates').getValue()
+ filters['year'] = $$('filter_year').getValue()
+ filters['month'] = $$('filter_month').getValue()
+ filters['client'] = $$('grid_invoices').getFilter('cliente').value
+ return filters
+}
- var y = fy.getValue()
- var m = fm.getValue()
- rango = {'year': y, 'month': m}
- }
+
+function get_invoices(){
+ var filters = get_filters_invoices()
+ filters['by'] = 'dates'
var grid = $$('grid_invoices')
grid.showProgress({type: 'icon'})
- webix.ajax().get('/invoices', rango, {
+ webix.ajax().get('/invoices', filters, {
error: function(text, data, xhr) {
msg_error('Error al consultar')
},
@@ -1416,7 +1424,7 @@ function filter_month_change(nv, ov){
function filter_dates_change(range){
if(range.start != null && range.end != null){
- get_invoices(range)
+ get_invoices()
}
}
@@ -2265,3 +2273,44 @@ function txt_folio_custom_lost_focus(prev){
validate_folio_exists(prev.getValue())
}
}
+
+
+function search_by(value){
+ var filters = get_filters_invoices()
+ filters['by'] = 'notes'
+ filters['notes'] = value
+
+
+ var grid = $$('grid_invoices')
+ grid.showProgress({type: 'icon'})
+
+ webix.ajax().get('/invoices', filters, {
+ error: function(text, data, xhr) {
+ msg_error('Error al consultar')
+ },
+ success: function(text, data, xhr) {
+ var values = data.json();
+ grid.clearAll();
+ if (values.ok){
+ grid.parse(values.rows, 'json');
+ };
+ }
+ });
+
+}
+
+
+function search_by_key_press(code, e){
+ var value = this.getValue().trim()
+ if(code == 13 && value.length > 3){
+ search_by(value)
+ }
+}
+
+
+function search_by_click(){
+ var value = this.getValue().trim()
+ if(value.length > 3){
+ search_by(value)
+ }
+}
diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js
index 98b7244..fc74676 100644
--- a/source/static/js/controller/nomina.js
+++ b/source/static/js/controller/nomina.js
@@ -13,6 +13,7 @@ var nomina_controllers = {
$$('cmd_nomina_delete').attachEvent('onItemClick', cmd_nomina_delete_click)
$$('cmd_nomina_timbrar').attachEvent('onItemClick', cmd_nomina_timbrar_click)
$$('cmd_nomina_log').attachEvent('onItemClick', cmd_nomina_log_click)
+ $$('cmd_nomina_download').attachEvent('onItemClick', cmd_nomina_download_click)
$$('cmd_nomina_cancel').attachEvent('onItemClick', cmd_nomina_cancel_click)
$$('grid_nomina').attachEvent('onItemClick', grid_nomina_click)
$$('filter_year_nomina').attachEvent('onChange', filter_year_nomina_change)
@@ -490,4 +491,29 @@ function cancel_nomina(id){
function cmd_nomina_log_click(){
location = '/doc/nomlog/0'
-}
\ No newline at end of file
+}
+
+
+function cmd_nomina_download_click(){
+ var grid = $$('grid_nomina')
+
+ if(!grid.count()){
+ msg = 'Sin documentos a descargar'
+ msg_error(msg)
+ return
+ }
+
+ var ids = []
+ grid.eachRow(function(row){
+ var r = grid.getItem(row)
+ ids.push(r.id)
+ })
+
+ var filters = {'by': 'download', 'ids': ids}
+
+ webix.ajax().response('blob').get('/nomina', filters, function(text, data){
+ webix.html.download(data, 'nomina.zip');
+ });
+
+}
+
diff --git a/source/static/js/controller/products.js b/source/static/js/controller/products.js
index a21973a..f13731f 100644
--- a/source/static/js/controller/products.js
+++ b/source/static/js/controller/products.js
@@ -2,7 +2,7 @@ var cfg_products = new Object()
function products_default_config(){
- webix.ajax().get('/config', {'fields': 'productos'}, {
+ webix.ajax().get('/config', {'fields': 'main_products'}, {
error: function(text, data, xhr) {
msg = 'Error al consultar'
msg_error(msg)
@@ -18,6 +18,7 @@ function products_default_config(){
if(cfg_products['inventario']){
$$('grid_products').showColumn('existencia')
}
+ show('cant_by_packing', values.chk_use_packing)
}
})
}
@@ -212,6 +213,11 @@ function cmd_save_product_click(id, e, node){
var values = form.getValues();
+ if(!isFinite(values.cant_by_packing)){
+ msg_error('La cantidad por empaque debe ser un número')
+ return
+ }
+
if(!validate_sat_key_product(values.clave_sat, false)){
msg_error('La clave SAT no existe')
return
@@ -422,4 +428,4 @@ function up_products_upload_complete(response){
}
}
})
-}
\ No newline at end of file
+}
diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js
index 617d011..d419f15 100644
--- a/source/static/js/ui/admin.js
+++ b/source/static/js/ui/admin.js
@@ -681,18 +681,6 @@ var options_admin_otros = [
labelRight: 'Ayuda para generar anticipos'},
{}]},
{maxHeight: 20},
- {template: 'Productos y Servicios', type: 'section'},
- {cols: [{maxWidth: 15},
- {view: 'checkbox', id: 'chk_config_cuenta_predial', labelWidth: 0,
- labelRight: 'Mostrar cuenta predial'},
- {view: 'checkbox', id: 'chk_config_codigo_barras', labelWidth: 0,
- labelRight: 'Mostrar código de barras'},
- {view: 'checkbox', id: 'chk_config_precio_con_impuestos', labelWidth: 0,
- labelRight: 'Mostrar precio con impuestos'},
- {view: 'checkbox', id: 'chk_llevar_inventario', labelWidth: 0,
- labelRight: 'Mostrar inventario'},
- ]},
- {maxHeight: 20},
{template: 'Punto de venta', type: 'section'},
{cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_usar_punto_de_venta', labelWidth: 0,
@@ -733,6 +721,21 @@ var options_admin_partners = [
]
+var options_admin_products = [
+ {maxHeight: 20},
+ {cols: [{view: 'checkbox', id: 'chk_config_cuenta_predial', labelWidth: 15,
+ labelRight: 'Mostrar cuenta predial'}]},
+ {cols: [{view: 'checkbox', id: 'chk_config_codigo_barras', labelWidth: 15,
+ labelRight: 'Mostrar código de barras'}]},
+ {cols: [{view: 'checkbox', id: 'chk_config_precio_con_impuestos', labelWidth: 15,
+ labelRight: 'Mostrar precio con impuestos'}]},
+ {cols: [{view: 'checkbox', id: 'chk_llevar_inventario', labelWidth: 15,
+ labelRight: 'Mostrar inventario'}]},
+ {cols: [{view: 'checkbox', id: 'chk_use_packing', labelWidth: 15,
+ labelRight: 'Usar empaques'}]},
+]
+
+
var options_admin_complements = [
{maxHeight: 20},
{cols: [{maxWidth: 15},
@@ -753,6 +756,12 @@ var options_admin_complements = [
{view: 'text', id: 'txt_config_cfdipay_folio', name: 'txt_config_cfdipay_serie',
label: 'Folio', labelWidth: 50, labelAlign: 'right'},
{maxWidth: 15}]},
+ {maxHeight: 20},
+ {template: 'Complemento de Divisas', type: 'section'},
+ {cols: [{maxWidth: 15},
+ {view: 'checkbox', id: 'chk_config_divisas', labelWidth: 0,
+ labelRight: 'Usar complemento de divisas'},
+ {maxWidth: 15}]},
]
@@ -765,6 +774,8 @@ var tab_options = {
rows: options_templates}},
{header: 'Clientes y Proveedores', body: {id: 'tab_admin_partners',
view: 'scrollview', body: {rows: options_admin_partners}}},
+ {header: 'Productos y Servicios', body: {id: 'tab_admin_products',
+ view: 'scrollview', body: {rows: options_admin_products}}},
{header: 'Complementos', body: {id: 'tab_admin_complements',
view: 'scrollview', body: {rows: options_admin_complements}}},
{header: 'Otros', body: {id: 'tab_admin_otros', view: 'scrollview',
@@ -1214,6 +1225,10 @@ var usuarios_admin = [
{maxHeight: 20},
{template: 'Usuarios Registrados', type: 'section'},
{cols: [{maxWidth: 10}, grid_usuarios, {maxWidth: 10}]},
+ {maxHeight: 20},
+ {template: 'Opciones', type: 'section'},
+ {cols: [{view: 'checkbox', id: 'chk_users_notify_access', labelWidth: 15,
+ labelRight: 'Notificar accesos al sistema (solo a administradores)'}]},
{},
]
diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js
index 0370bb1..0aa72c1 100644
--- a/source/static/js/ui/invoices.js
+++ b/source/static/js/ui/invoices.js
@@ -234,6 +234,9 @@ var toolbar_invoices_filter = [
labelWidth: 50, width: 200, options: months},
{view: 'daterangepicker', id: 'filter_dates', label: 'Fechas',
labelAlign: 'right', width: 300},
+ {},
+ {view: 'search', id: 'search_by', name: 'search_by', width: 200,
+ placeholder: 'Captura al menos cuatro letras'},
]
@@ -535,6 +538,12 @@ var body_moneda = {cols: [
]}
+var body_divisas = {cols: [
+ {view: 'radio', id: 'opt_divisas', name: 'opt_divisas',
+ options: ['Ninguna', 'Compra', 'Venta']},
+]}
+
+
var body_regimen_fiscal = {
view: 'richselect',
id: 'lst_regimen_fiscal',
@@ -593,6 +602,7 @@ var controls_generate = [
{view: 'fieldset', label: 'Comprobante', body: body_comprobante},
{view: 'fieldset', label: 'Opciones de Pago', body: body_opciones},
{view: 'fieldset', id: 'fs_moneda', label: 'Moneda', body: body_moneda},
+ {view: 'fieldset', id: 'fs_divisas', label: 'Divisas - Tipo de Operación', body: body_divisas},
{view: 'fieldset', id: 'fs_regimen_fiscal', label: 'Regimen Fiscal',
body: body_regimen_fiscal},
]}
diff --git a/source/static/js/ui/main.js b/source/static/js/ui/main.js
index d0f600e..469eb90 100644
--- a/source/static/js/ui/main.js
+++ b/source/static/js/ui/main.js
@@ -59,8 +59,9 @@ var menu_user = {
}
-var link_forum = "Foro de Soporte";
-var link_doc = "? ";
+var link_blog = "Blog";
+var link_forum = "Foro";
+var link_doc = "Doc";
var ui_main = {
@@ -72,8 +73,10 @@ var ui_main = {
}
},
{view: 'label', id: 'lbl_title_main', label: 'Empresa Libre'},
- {view: 'label', id: 'lbl_forum', label: link_forum, align: 'right'},
- {view: 'label', id: 'lbl_doc', label: link_doc, align: 'left', width: 25},
+ {},
+ {view: 'label', id: 'lbl_blog', label: link_blog, align: 'right', width: 30},
+ {view: 'label', id: 'lbl_forum', label: link_forum, align: 'right', width: 30},
+ {view: 'label', id: 'lbl_doc', label: link_doc, align: 'right', width: 25},
menu_user,
{view: 'button', id: 'cmd_update_timbres', type: 'icon', width: 45,
css: 'app_button', icon: 'bell-o', badge: 0},
diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js
index 69dcc65..5a84e74 100644
--- a/source/static/js/ui/nomina.js
+++ b/source/static/js/ui/nomina.js
@@ -20,6 +20,8 @@ var toolbar_nomina_util = [
type: 'iconButton', autowidth: true, icon: 'check-circle'},
{view: 'button', id: 'cmd_nomina_log', label: 'Log',
type: 'iconButton', autowidth: true, icon: 'download'},
+ {view: 'button', id: 'cmd_nomina_download', label: 'Descargar',
+ type: 'iconButton', autowidth: true, icon: 'download'},
{},
{view: 'button', id: 'cmd_nomina_cancel', label: 'Cancelar',
type: 'iconButton', autowidth: true, icon: 'ban'},
diff --git a/source/static/js/ui/products.js b/source/static/js/ui/products.js
index 3fdf5f1..232eef0 100644
--- a/source/static/js/ui/products.js
+++ b/source/static/js/ui/products.js
@@ -135,9 +135,13 @@ var controls_generals = [
{view: "richselect", id: "unidad", name: "unidad", label: "Unidad",
width: 300, labelWidth: 130, labelAlign: "right", required: true,
invalidMessage: "La Unidad es requerida", options: []},
- {view: 'text', id: 'tags_producto', name: 'tags_producto',
- labelAlign: 'right', label: 'Etiquetas',
- placeholder: 'Separadas por comas'}
+ {view: 'text', id: 'cant_by_packing', name: 'cant_by_packing',
+ labelAlign: 'right', labelWidth: 150, inputAlign: "right",
+ label: 'Cantidad por empaque:'},
+ {},
+ //~ {view: 'text', id: 'tags_producto', name: 'tags_producto',
+ //~ labelAlign: 'right', label: 'Etiquetas',
+ //~ placeholder: 'Separadas por comas'}
]},
{cols: [
{view: "currency", type: "text", id: "valor_unitario",
diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura.ods
index cab2f95..a35d18a 100644
Binary files a/source/templates/plantilla_factura.ods and b/source/templates/plantilla_factura.ods differ
diff --git a/source/templates/plantilla_nomina.ods b/source/templates/plantilla_nomina.ods
new file mode 100644
index 0000000..3f344fd
Binary files /dev/null and b/source/templates/plantilla_nomina.ods differ
diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt
index 81d45ac..4c6e2d5 100644
--- a/source/xslt/cadena.xslt
+++ b/source/xslt/cadena.xslt
@@ -13,9 +13,10 @@
+
+
+
+
+
+
+
+
+
+
+
+