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 @@ + + + + + + + + + + + +