Merge branch 'develop'

Soporte para complemento de divisas
This commit is contained in:
Mauricio Baeza 2019-02-17 22:12:39 -06:00
commit 79cea5b092
25 changed files with 921 additions and 89 deletions

View File

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

View File

@ -1 +1 @@
1.27.1
1.28.0

View File

@ -13,3 +13,4 @@ pyqrcode
pypng
reportlab
psycopg2-binary
cryptography

View File

@ -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'])

View File

@ -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):

View File

@ -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'))

View File

@ -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 <http://www.gnu.org/licenses/>.
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()

View File

@ -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'])

View File

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

View File

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

View File

@ -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__))

View File

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

View File

@ -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',
}

View File

@ -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)
}
}

View File

@ -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'
}
}
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');
});
}

View File

@ -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){
}
}
})
}
}

View File

@ -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)'}]},
{},
]

View File

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

View File

@ -59,8 +59,9 @@ var menu_user = {
}
var link_forum = "<a class='link_forum' target='_blank' href='https://gitlab.com/mauriciobaeza/empresa-libre/issues'>Foro de Soporte</a>";
var link_doc = "<a class='link_doc' target='_blank' href='https://doc.empresalibre.net'><b>?</b> </a>";
var link_blog = "<a class='link_default' target='_blank' href='https://blog.empresalibre.mx'>Blog</a>";
var link_forum = "<a class='link_default' target='_blank' href='https://gitlab.com/mauriciobaeza/empresa-libre/issues'>Foro</a>";
var link_doc = "<a class='link_default' target='_blank' href='https://doc.empresalibre.mx'>Doc</a>";
var ui_main = {
@ -72,8 +73,10 @@ var ui_main = {
}
},
{view: 'label', id: 'lbl_title_main', label: '<b>Empresa Libre</b>'},
{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},

View File

@ -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'},

View File

@ -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",

Binary file not shown.

View File

@ -13,9 +13,10 @@
<xsl:include href="ine11.xslt"/>
<xsl:include href="iedu.xslt"/>
<xsl:include href="pagos10.xslt"/>
<xsl:include href="divisas.xslt"/>
<!--
<xsl:include href="ecc11.xslt"/>
<xsl:include href="Divisas.xslt"/>
<xsl:include href="pfic.xslt"/>
<xsl:include href="TuristaPasajeroExtranjero.xslt"/>
<xsl:include href="cfdiregistrofiscal.xslt"/>

13
source/xslt/divisas.xslt Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions" xmlns:divisas="http://www.sat.gob.mx/divisas">
<!-- Manejador de nodos tipo divisas:Divisas -->
<xsl:template match="divisas:Divisas">
<!-- Iniciamos el tratamiento de los atributos de divisas:Divisas -->
<xsl:call-template name="Requerido">
<xsl:with-param name="valor" select="./@version"/>
</xsl:call-template>
<xsl:call-template name="Requerido">
<xsl:with-param name="valor" select="./@tipoOperacion"/>
</xsl:call-template>
</xsl:template>
</xsl:stylesheet>