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] v 1.27.1 [23-ene-2019]
---------------------- ----------------------
- Error: Al cancelar nómina - Error: Al cancelar nómina

View File

@ -1 +1 @@
1.27.1 1.28.0

View File

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

View File

@ -84,6 +84,12 @@ SAT = {
'xmlns': 'http://www.sat.gob.mx/Pagos', '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', '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._edu = False
self._pagos = False self._pagos = False
self._is_nomina = False self._is_nomina = False
self._divisas = ''
self.error = '' self.error = ''
def _now(self): def _now(self):
@ -152,6 +159,8 @@ class CFDI(object):
self._ine = True self._ine = True
self._pagos = bool(datos['complementos'].get('pagos', False)) self._pagos = bool(datos['complementos'].get('pagos', False))
self._divisas = datos['comprobante'].pop('divisas', '')
if 'nomina' in datos: if 'nomina' in datos:
self._is_nomina = True self._is_nomina = True
return self._validate_nomina(datos) return self._validate_nomina(datos)
@ -190,6 +199,12 @@ class CFDI(object):
attributes[name] = SAT['edu']['xmlns'] attributes[name] = SAT['edu']['xmlns']
schema_edu = SAT['edu']['schema'] 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 = '' schema_nomina = ''
if self._is_nomina: if self._is_nomina:
name = 'xmlns:{}'.format(SAT['nomina']['prefix']) name = 'xmlns:{}'.format(SAT['nomina']['prefix'])
@ -204,7 +219,7 @@ class CFDI(object):
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \ attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
schema_locales + schema_donativo + schema_ine + schema_edu + \ schema_locales + schema_donativo + schema_ine + schema_edu + \
schema_nomina + schema_pagos schema_divisas + schema_nomina + schema_pagos
attributes.update(datos) attributes.update(datos)
if not 'Version' in attributes: if not 'Version' in attributes:
@ -426,6 +441,13 @@ class CFDI(object):
self._complemento = ET.SubElement( self._complemento = ET.SubElement(
self._cfdi, '{}:Complemento'.format(self._pre)) 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: if 'ine' in datos:
atributos = {'Version': SAT['ine']['version']} atributos = {'Version': SAT['ine']['version']}
atributos.update(datos['ine']) atributos.update(datos['ine'])

View File

@ -47,6 +47,7 @@ class AppLogin(object):
session.invalidate() session.invalidate()
values = req.params values = req.params
values['rfc'] = values['rfc'].upper() values['rfc'] = values['rfc'].upper()
values['ip'] = req.remote_addr
result, user = self._db.authenticate(values) result, user = self._db.authenticate(values)
if result['login']: if result['login']:
session.save() session.save()
@ -485,7 +486,14 @@ class AppNomina(object):
def on_get(self, req, resp): def on_get(self, req, resp):
values = req.params values = req.params
by = values.get('by', '')
req.context['result'] = self._db.get_nomina(values) 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 resp.status = falcon.HTTP_200
def on_post(self, req, resp): def on_post(self, req, resp):

View File

@ -872,7 +872,7 @@ class LIBO(object):
currency = self.CELL_STYLE.get(self._currency, 'peso') currency = self.CELL_STYLE.get(self._currency, 'peso')
return '{}{}'.format(currency, match.groups()[1]) return '{}{}'.format(currency, match.groups()[1])
def _conceptos(self, data): def _conceptos(self, data, pakings):
first = True first = True
col1 = [] col1 = []
col2 = [] col2 = []
@ -881,8 +881,9 @@ class LIBO(object):
col5 = [] col5 = []
col6 = [] col6 = []
col7 = [] col7 = []
col8 = []
count = len(data) - 1 count = len(data) - 1
for concepto in data: for i, concepto in enumerate(data):
key = concepto.get('noidentificacion', '') key = concepto.get('noidentificacion', '')
description = concepto['descripcion'] description = concepto['descripcion']
unidad = concepto['unidad'] unidad = concepto['unidad']
@ -899,6 +900,8 @@ class LIBO(object):
cell_5 = self._set_cell('{valorunitario}', valor_unitario, value=True) cell_5 = self._set_cell('{valorunitario}', valor_unitario, value=True)
cell_6 = self._set_cell('{importe}', importe, value=True) cell_6 = self._set_cell('{importe}', importe, value=True)
cell_7 = self._set_cell('{descuento}', descuento, 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: if len(data) > 1:
row = cell_1.getCellAddress().Row + 1 row = cell_1.getCellAddress().Row + 1
self._sheet.getRows().insertByIndex(row, count) self._sheet.getRows().insertByIndex(row, count)
@ -912,6 +915,8 @@ class LIBO(object):
col5.append((float(valor_unitario),)) col5.append((float(valor_unitario),))
col6.append((float(importe),)) col6.append((float(importe),))
col7.append((float(descuento),)) col7.append((float(descuento),))
if pakings:
col8.append((pakings[i],))
self._total_cantidades += float(cantidad) self._total_cantidades += float(cantidad)
if not count: if not count:
return return
@ -919,6 +924,9 @@ class LIBO(object):
style_5 = self._get_style(cell_5) style_5 = self._get_style(cell_5)
style_6 = self._get_style(cell_6) style_6 = self._get_style(cell_6)
style_7 = self._get_style(cell_7) style_7 = self._get_style(cell_7)
style_8 = ''
if pakings:
style_8 = self._get_style(cell_8)
col = cell_1.getCellAddress().Column col = cell_1.getCellAddress().Column
target1 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) target1 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
@ -933,9 +941,13 @@ class LIBO(object):
col = cell_6.getCellAddress().Column col = cell_6.getCellAddress().Column
target6 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) target6 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
target7 = None target7 = None
target8 = None
if not cell_7 is None: if not cell_7 is None:
col = cell_7.getCellAddress().Column col = cell_7.getCellAddress().Column
target7 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) 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)) target1.setFormulaArray(tuple(col1))
target2.setDataArray(tuple(col2)) target2.setDataArray(tuple(col2))
@ -945,6 +957,8 @@ class LIBO(object):
target6.setDataArray(tuple(col6)) target6.setDataArray(tuple(col6))
if not target7 is None: if not target7 is None:
target7.setDataArray(tuple(col7)) target7.setDataArray(tuple(col7))
if not target8 is None:
target8.setDataArray(tuple(col8))
if style_5: if style_5:
cell_5.CellStyle = style_5 cell_5.CellStyle = style_5
@ -955,6 +969,9 @@ class LIBO(object):
if style_7: if style_7:
cell_7.CellStyle = style_7 cell_7.CellStyle = style_7
target7.CellStyle = style_7 target7.CellStyle = style_7
if style_8:
cell_8.CellStyle = style_8
target8.CellStyle = style_8
return return
def _add_totales(self, data): def _add_totales(self, data):
@ -1067,6 +1084,12 @@ class LIBO(object):
self._set_cell('{ine.%s}' % k, v) self._set_cell('{ine.%s}' % k, v)
return 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): def _nomina(self, data):
if not data: if not data:
return return
@ -1275,10 +1298,12 @@ class LIBO(object):
self._currency = data['totales']['moneda'] self._currency = data['totales']['moneda']
self._pagos = data.pop('pagos', False) self._pagos = data.pop('pagos', False)
pakings = data.pop('pakings', [])
self._comprobante(data['comprobante']) self._comprobante(data['comprobante'])
self._emisor(data['emisor']) self._emisor(data['emisor'])
self._receptor(data['receptor']) self._receptor(data['receptor'])
self._conceptos(data['conceptos']) self._conceptos(data['conceptos'], pakings)
if self._pagos: if self._pagos:
self._cfdipays(data['pays']) self._cfdipays(data['pays'])
@ -1291,6 +1316,8 @@ class LIBO(object):
self._donataria(data['donataria']) self._donataria(data['donataria'])
self._ine(data['ine']) self._ine(data['ine'])
self._divisas(data.get('divisas', {}))
self._cancelado(data['cancelada']) self._cancelado(data['cancelada'])
self._clean() self._clean()
return return
@ -1298,6 +1325,7 @@ class LIBO(object):
def pdf(self, path, data, ods=False): def pdf(self, path, data, ods=False):
options = {'AsTemplate': True, 'Hidden': True} options = {'AsTemplate': True, 'Hidden': True}
log.debug('Abrir plantilla...') log.debug('Abrir plantilla...')
self._template = self._doc_open(path, options) self._template = self._doc_open(path, options)
if self._template is None: if self._template is None:
return b'' return b''
@ -1444,7 +1472,16 @@ class LIBO(object):
return {}, msg return {}, msg
data = tuple([r[2:] for r in rows[:count+2]]) 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): def _get_otros_pagos(self, doc, count):
rows, msg = self._get_data(doc, 'OtrosPagos') rows, msg = self._get_data(doc, 'OtrosPagos')
@ -1508,7 +1545,7 @@ class LIBO(object):
doc.close(True) doc.close(True)
return {}, msg return {}, msg
deducciones, msg = self._get_deducciones(doc, len(nomina)) deducciones, new_titles, msg = self._get_deducciones(doc, len(nomina))
if msg: if msg:
doc.close(True) doc.close(True)
return {}, msg return {}, msg
@ -1534,6 +1571,28 @@ class LIBO(object):
return {}, msg return {}, msg
doc.close(True) 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['nomina'] = nomina
data['percepciones'] = percepciones data['percepciones'] = percepciones
data['deducciones'] = deducciones data['deducciones'] = deducciones
@ -1541,6 +1600,7 @@ class LIBO(object):
data['separacion'] = separacion data['separacion'] = separacion
data['horas_extras'] = horas_extras data['horas_extras'] = horas_extras
data['incapacidades'] = incapacidades data['incapacidades'] = incapacidades
data['new_titles'] = new_titles
return data, '' return data, ''
@ -1563,11 +1623,13 @@ class LIBO(object):
def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'): def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'):
rfc = data['emisor']['rfc'] rfc = data['emisor']['rfc']
default = 'plantilla_factura.ods'
if DEBUG: if DEBUG:
rfc = emisor_rfc rfc = emisor_rfc
version = data['comprobante']['version'] version = data['comprobante']['version']
if 'nomina' in data and data['nomina']: if 'nomina' in data and data['nomina']:
version = '{}_{}'.format(data['nomina']['version'], version) version = '{}_{}'.format(data['nomina']['version'], version)
default = 'plantilla_nomina.ods'
pagos = '' pagos = ''
if data.get('pagos', False): if data.get('pagos', False):
@ -1584,7 +1646,7 @@ def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'):
if data['donativo']: if data['donativo']:
donativo = '_donativo' donativo = '_donativo'
name = '{}_{}{}{}.ods'.format(rfc.lower(), pagos, version, donativo) name = '{}_{}{}{}.ods'.format(rfc.lower(), pagos, version, donativo)
path = get_template_ods(name) path = get_template_ods(name, default)
if path: if path:
return app.pdf(path, data, ods) return app.pdf(path, data, ods)
@ -2105,7 +2167,7 @@ def get_data_from_xml(invoice, values):
data['pagos'] = values.get('pagos', False) data['pagos'] = values.get('pagos', False)
if data['pagos']: if data['pagos']:
data['pays'] = _cfdipays(doc, data, version) data['pays'] = _cfdipays(doc, data, version)
data['pakings'] = values.get('pakings', [])
return data return data
@ -3764,3 +3826,5 @@ def validate_rfc(value):
def parse_xml2(xml_str): def parse_xml2(xml_str):
return etree.fromstring(xml_str.encode('utf-8')) 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): def process_response(self, req, resp, resource):
if 'result' not in req.context: if 'result' not in req.context:
return return
if '/doc/' in req.path: if '/doc/' in req.path:
resp.body = req.context['result'] resp.body = req.context['result']
return return
if 'blob' in req.context:
resp.body = req.context['blob']
return
resp.body = util.dumps(req.context['result']) resp.body = util.dumps(req.context['result'])

View File

@ -384,8 +384,10 @@ class StorageEngine(object):
def get_tickets(self, values): def get_tickets(self, values):
return main.Tickets.get_by(values) return main.Tickets.get_by(values)
def get_invoices(self, values): def get_invoices(self, filters):
return main.Facturas.get_(values) if filters.get('by', ''):
return main.Facturas.get_by(filters)
return main.Facturas.get_(filters)
def get_preinvoices(self, values): def get_preinvoices(self, values):
return main.PreFacturas.get_(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__))) parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
# ~ v2
from controllers import utils
# ~ v1
from controllers import util from controllers import util
from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \ from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \
CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \
@ -286,6 +290,7 @@ def config_timbrar():
'cfdi_anticipo': Configuracion.get_('chk_config_anticipo'), 'cfdi_anticipo': Configuracion.get_('chk_config_anticipo'),
'cfdi_ine': Configuracion.get_bool('chk_config_ine'), 'cfdi_ine': Configuracion.get_bool('chk_config_ine'),
'cfdi_edu': Configuracion.get_bool('chk_config_edu'), '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_metodo_pago': Configuracion.get_bool('chk_config_ocultar_metodo_pago'),
'cfdi_condicion_pago': Configuracion.get_bool('chk_config_ocultar_condiciones_pago'), 'cfdi_condicion_pago': Configuracion.get_bool('chk_config_ocultar_condiciones_pago'),
'cfdi_open_pdf': Configuracion.get_bool('chk_config_open_pdf'), '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' msg = 'No se pudo guardar la configuración'
return {'ok': result, 'msg': msg} 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 @classmethod
def get_bool(cls, key): def get_bool(cls, key):
data = (Configuracion data = (Configuracion
@ -368,11 +384,45 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data} values = {r.clave: util.get_bool(r.valor) for r in data}
return values 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): def _get_complements(self):
fields = ( fields = (
'chk_config_ine', 'chk_config_ine',
'chk_config_edu', 'chk_config_edu',
'chk_config_pagos', 'chk_config_pagos',
'chk_config_divisas',
'chk_cfg_pays_data_bank', 'chk_cfg_pays_data_bank',
) )
data = (Configuracion data = (Configuracion
@ -401,6 +451,28 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data} values = {r.clave: util.get_bool(r.valor) for r in data}
return values 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 @classmethod
def get_(cls, keys): def get_(cls, keys):
if isinstance(keys, str): if isinstance(keys, str):
@ -412,27 +484,18 @@ class Configuracion(BaseModel):
return data[0].valor return data[0].valor
return '' return ''
options = ('partners', 'complements', 'folios') options = ('partners',
'admin_products',
'main_products',
'complements',
'folios',
'correo',
'admin_config_users',
)
opt = keys['fields'] opt = keys['fields']
if opt in options: if opt in options:
return getattr(cls, '_get_{}'.format(opt))(cls) 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': if keys['fields'] == 'configtemplates':
try: try:
emisor = Emisor.select()[0] emisor = Emisor.select()[0]
@ -465,10 +528,6 @@ class Configuracion(BaseModel):
'chk_config_tax_locales_truncate', 'chk_config_tax_locales_truncate',
'chk_config_decimales_precios', 'chk_config_decimales_precios',
'chk_config_anticipo', '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_usar_punto_de_venta',
'chk_ticket_pdf_show', 'chk_ticket_pdf_show',
'chk_ticket_direct_print', 'chk_ticket_direct_print',
@ -493,16 +552,7 @@ class Configuracion(BaseModel):
values[f] = Configuracion.get_(f) values[f] = Configuracion.get_(f)
return values return values
if keys['fields'] == 'correo': if keys['fields'] == 'path_cer':
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':
fields = ('path_key', 'path_cer') fields = ('path_key', 'path_cer')
data = (Configuracion data = (Configuracion
.select() .select()
@ -3204,6 +3254,8 @@ class Productos(BaseModel):
es_activo = BooleanField(default=True) es_activo = BooleanField(default=True)
impuestos = ManyToManyField(SATImpuestos, related_name='productos') impuestos = ManyToManyField(SATImpuestos, related_name='productos')
tags = ManyToManyField(Tags, related_name='productos_tags') tags = ManyToManyField(Tags, related_name='productos_tags')
cantidad_empaque = DecimalField(default=0.0, max_digits=14, decimal_places=4,
auto_round=True)
class Meta: class Meta:
order_by = ('descripcion',) order_by = ('descripcion',)
@ -3415,6 +3467,7 @@ class Productos(BaseModel):
Productos.inventario, Productos.inventario,
Productos.existencia, Productos.existencia,
Productos.minimo, Productos.minimo,
Productos.cantidad_empaque.alias('cant_by_packing'),
) )
.where(Productos.id==id).dicts()[0] .where(Productos.id==id).dicts()[0]
) )
@ -3442,6 +3495,7 @@ class Productos(BaseModel):
descripcion = util.spaces(values.pop('descripcion')) descripcion = util.spaces(values.pop('descripcion'))
fields = util.clean(values) fields = util.clean(values)
fields['cantidad_empaque'] = fields.pop('cant_by_packing', 0.0)
fields.pop('precio_con_impuestos', '') fields.pop('precio_con_impuestos', '')
fields['es_activo'] = fields.pop('es_activo_producto') fields['es_activo'] = fields.pop('es_activo_producto')
fields['descripcion'] = descripcion fields['descripcion'] = descripcion
@ -3484,6 +3538,7 @@ class Productos(BaseModel):
def actualizar(cls, values, id): def actualizar(cls, values, id):
values['cuenta_predial'] = values.get('cuenta_predial', '') values['cuenta_predial'] = values.get('cuenta_predial', '')
values['codigo_barras'] = values.get('codigo_barras', '') values['codigo_barras'] = values.get('codigo_barras', '')
fields, taxes = cls._clean(cls, values) fields, taxes = cls._clean(cls, values)
obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes)) obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes))
with database_proxy.transaction(): with database_proxy.transaction():
@ -3577,6 +3632,7 @@ class Facturas(BaseModel):
estatus = TextField(default='Guardada') estatus = TextField(default='Guardada')
estatus_sat = TextField(default='Vigente') estatus_sat = TextField(default='Vigente')
regimen_fiscal = TextField(default='') regimen_fiscal = TextField(default='')
divisas = TextField(default='')
notas = TextField(default='') notas = TextField(default='')
saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True) auto_round=True)
@ -3663,6 +3719,65 @@ class Facturas(BaseModel):
obj.save() obj.save()
return data 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 @classmethod
def filter_years(cls): def filter_years(cls):
data = [{'id': -1, 'value': 'Todos'}] data = [{'id': -1, 'value': 'Todos'}]
@ -3695,6 +3810,8 @@ class Facturas(BaseModel):
return {'ok': True, 'msg': 'Notas guardadas correctamente'} return {'ok': True, 'msg': 'Notas guardadas correctamente'}
def _get_not_in_xml(self, invoice, emisor): def _get_not_in_xml(self, invoice, emisor):
pdf_from = Configuracion.get_('make_pdf_from') or '1'
values = {} values = {}
values['notas'] = invoice.notas values['notas'] = invoice.notas
@ -3732,6 +3849,16 @@ class Facturas(BaseModel):
for k, v in receptor.items(): for k, v in receptor.items():
values['receptor'][k] = v 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 return values
@classmethod @classmethod
@ -3748,7 +3875,11 @@ class Facturas(BaseModel):
pdf_from = Configuracion.get_('make_pdf_from') or '1' pdf_from = Configuracion.get_('make_pdf_from') or '1'
values = cls._get_not_in_xml(cls, obj, emisor) values = cls._get_not_in_xml(cls, obj, emisor)
#Tmp to v2
data = util.get_data_from_xml(obj, values) 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) doc = util.to_pdf(data, emisor.rfc, pdf_from=pdf_from)
if sync: if sync:
@ -3980,6 +4111,7 @@ class Facturas(BaseModel):
@classmethod @classmethod
def send(cls, id, rfc): def send(cls, id, rfc):
values = Configuracion.get_({'fields': 'correo'}) values = Configuracion.get_({'fields': 'correo'})
contra = Configuracion.get_('correo_contra')
in_zip = Configuracion.get_bool('chk_config_send_zip') in_zip = Configuracion.get_bool('chk_config_send_zip')
if not values: if not values:
@ -4006,7 +4138,7 @@ class Facturas(BaseModel):
'puerto': values['correo_puerto'], 'puerto': values['correo_puerto'],
'ssl': bool(int(values['correo_ssl'])), 'ssl': bool(int(values['correo_ssl'])),
'usuario': values['correo_usuario'], 'usuario': values['correo_usuario'],
'contra': values['correo_contra'], 'contra': utils.decrypt(contra, rfc),
} }
options = { options = {
'para': obj.cliente.correo_facturas, 'para': obj.cliente.correo_facturas,
@ -4285,6 +4417,8 @@ class Facturas(BaseModel):
tax_locales = Configuracion.get_bool('chk_config_tax_locales') tax_locales = Configuracion.get_bool('chk_config_tax_locales')
tax_locales_truncate = Configuracion.get_bool('chk_config_tax_locales_truncate') tax_locales_truncate = Configuracion.get_bool('chk_config_tax_locales_truncate')
tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') tax_decimals = Configuracion.get_bool('chk_config_tax_decimals')
use_packing = Configuracion.get_bool('chk_use_packing')
subtotal = 0 subtotal = 0
descuento_cfdi = 0 descuento_cfdi = 0
totals_tax = {} totals_tax = {}
@ -4312,6 +4446,10 @@ class Facturas(BaseModel):
precio_final = valor_unitario - descuento precio_final = valor_unitario - descuento
importe = round(cantidad * precio_final, DECIMALES) 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['cantidad'] = cantidad
product['valor_unitario'] = valor_unitario product['valor_unitario'] = valor_unitario
product['descuento'] = round(descuento * cantidad, DECIMALES) product['descuento'] = round(descuento * cantidad, DECIMALES)
@ -4434,6 +4572,12 @@ class Facturas(BaseModel):
ine = values.pop('ine', {}) ine = values.pop('ine', {})
tipo_comprobante = values['tipo_comprobante'] tipo_comprobante = values['tipo_comprobante']
folio_custom = values.pop('folio_custom', '') 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] emisor = Emisor.select()[0]
values['serie'] = cls._get_serie(cls, user, values['serie']) values['serie'] = cls._get_serie(cls, user, values['serie'])
@ -4504,6 +4648,9 @@ class Facturas(BaseModel):
relacionados = {} relacionados = {}
donativo = {} donativo = {}
complementos = FacturasComplementos.get_(invoice) complementos = FacturasComplementos.get_(invoice)
comprobante['divisas'] = invoice.divisas
if invoice.divisas:
complementos['divisas'] = True
if invoice.donativo: if invoice.donativo:
donativo['noAutorizacion'] = emisor.autorizacion donativo['noAutorizacion'] = emisor.autorizacion
@ -4736,6 +4883,7 @@ class Facturas(BaseModel):
'edu': is_edu, 'edu': is_edu,
'complementos': complementos, 'complementos': complementos,
} }
return util.make_xml(data, certificado, auth) return util.make_xml(data, certificado, auth)
@classmethod @classmethod
@ -5616,6 +5764,8 @@ class FacturasDetalle(BaseModel):
nivel = TextField(default='') nivel = TextField(default='')
autorizacion = TextField(default='') autorizacion = TextField(default='')
cuenta_predial = TextField(default='') cuenta_predial = TextField(default='')
empaques = DecimalField(default=0.0, max_digits=14, decimal_places=4,
auto_round=True)
class Meta: class Meta:
order_by = ('factura',) order_by = ('factura',)
@ -7493,7 +7643,6 @@ class CfdiNomina(BaseModel):
data['fecha_pago'] = util.calc_to_date(row['fecha_pago']) data['fecha_pago'] = util.calc_to_date(row['fecha_pago'])
data['fecha_inicial_pago'] = util.calc_to_date(row['fecha_inicial_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['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 data['dias_pagados'] = days_pay
return data, '' return data, ''
@ -7546,7 +7695,7 @@ class CfdiNomina(BaseModel):
return data, totals, '' return data, totals, ''
def _validate_deducciones(self, headers, row): def _validate_deducciones(self, headers, row, new_titles):
total_retenciones = 0.0 total_retenciones = 0.0
total_otras_deducciones = 0.0 total_otras_deducciones = 0.0
@ -7572,6 +7721,7 @@ class CfdiNomina(BaseModel):
new = { new = {
'tipo_deduccion': td, 'tipo_deduccion': td,
'importe': importe, 'importe': importe,
'concepto': new_titles.get(i, ''),
} }
data.append(new) data.append(new)
@ -7735,6 +7885,7 @@ class CfdiNomina(BaseModel):
separacion = data['separacion'][2:] separacion = data['separacion'][2:]
horas_extras = data['horas_extras'][2:] horas_extras = data['horas_extras'][2:]
incapacidades = data['incapacidades'][2:] incapacidades = data['incapacidades'][2:]
new_titles = data['new_titles']
for i, row in enumerate(data['nomina']): for i, row in enumerate(data['nomina']):
row['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal row['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
@ -7756,7 +7907,7 @@ class CfdiNomina(BaseModel):
continue continue
new_deducciones, total_deducciones, msg = \ new_deducciones, total_deducciones, msg = \
self._validate_deducciones(self, hd, deducciones[i]) self._validate_deducciones(self, hd, deducciones[i], new_titles)
if msg: if msg:
util.log_file('nomina', msg) util.log_file('nomina', msg)
continue continue
@ -8190,11 +8341,34 @@ class CfdiNomina(BaseModel):
return {'ok': ok, 'msg_ok': msg, 'error': error, 'msg_error': msg_error} 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 @classmethod
def get_by(cls, values): def get_by(cls, values):
if not values: if not values:
return cls._get(cls) return cls._get(cls)
if values.get('by', ''):
return getattr(cls, f"_get_by_{values['by']}")(cls, values)
if values['opt'] == 'dates': if values['opt'] == 'dates':
dates = util.loads(values['range']) dates = util.loads(values['range'])
filters = CfdiNomina.fecha.between( filters = CfdiNomina.fecha.between(
@ -8508,6 +8682,50 @@ def _save_log(user, action, table):
return 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): def authenticate(args):
respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''} respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''}
values = util.get_con(args['rfc']) values = util.get_con(args['rfc'])
@ -8532,7 +8750,11 @@ def authenticate(args):
respuesta['login'] = True respuesta['login'] = True
respuesta['user'] = str(obj) respuesta['user'] = str(obj)
respuesta['super'] = obj.es_superusuario 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 return respuesta, obj
@ -8835,10 +9057,34 @@ def _migrate_tables(rfc=''):
activa = BooleanField(default=True) activa = BooleanField(default=True)
migrations.append(migrator.add_column(table, 'activa', activa)) 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: if migrations:
with database_proxy.atomic() as txn: with database_proxy.atomic() as txn:
migrate(*migrations) migrate(*migrations)
Configuracion.add({'version': VERSION})
log.info('Tablas migradas correctamente...') log.info('Tablas migradas correctamente...')
_importar_valores('', rfc) _importar_valores('', rfc)

View File

@ -47,8 +47,8 @@ except ImportError:
DEBUG = DEBUG DEBUG = DEBUG
VERSION = '1.27.1' VERSION = '1.28.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',) EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) BASE_DIR = os.path.abspath(os.path.dirname(__file__))

View File

@ -47,6 +47,13 @@
font-size: 125%; font-size: 125%;
} }
.link_default {
font-weight: bold;
color: #610B0B;
text-decoration: none;
}
.link_default:hover {text-decoration:underline;}
.link_forum { .link_forum {
font-weight: bold; font-weight: bold;
color: #610B0B; color: #610B0B;

View File

@ -59,11 +59,13 @@ var controllers = {
$$('grid_admin_unidades').attachEvent('onItemClick', grid_admin_unidades_click) $$('grid_admin_unidades').attachEvent('onItemClick', grid_admin_unidades_click)
$$('grid_moneda_found').attachEvent('onValueSuggest', grid_moneda_found_click) $$('grid_moneda_found').attachEvent('onValueSuggest', grid_moneda_found_click)
$$('cmd_agregar_impuesto').attachEvent('onItemClick', cmd_agregar_impuesto_click) $$('cmd_agregar_impuesto').attachEvent('onItemClick', cmd_agregar_impuesto_click)
//~ Usuarios //~ Usuarios
$$('cmd_usuario_agregar').attachEvent('onItemClick', cmd_usuario_agregar_click) $$('cmd_usuario_agregar').attachEvent('onItemClick', cmd_usuario_agregar_click)
$$('grid_usuarios').attachEvent('onItemClick', grid_usuarios_click) $$('grid_usuarios').attachEvent('onItemClick', grid_usuarios_click)
$$('grid_usuarios').attachEvent('onCheck', grid_usuarios_on_check) $$('grid_usuarios').attachEvent('onCheck', grid_usuarios_on_check)
$$('grid_usuarios').attachEvent('onItemDblClick', grid_usuarios_double_click) $$('grid_usuarios').attachEvent('onItemDblClick', grid_usuarios_double_click)
$$('chk_users_notify_access').attachEvent('onItemClick', chk_config_item_click)
admin_ui_windows.init() admin_ui_windows.init()
//~ Opciones //~ Opciones
@ -83,6 +85,13 @@ var controllers = {
//~ Partners //~ Partners
$$('chk_config_change_balance_partner').attachEvent('onItemClick', chk_config_item_click) $$('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_metodo_pago').attachEvent('onItemClick', chk_config_item_click)
$$('chk_config_ocultar_condiciones_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) $$('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_ine').attachEvent('onItemClick', chk_config_item_click)
$$('chk_config_edu').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_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_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_usar_punto_de_venta').attachEvent('onItemClick', chk_config_item_click)
$$('chk_ticket_pdf_show').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) $$('chk_ticket_direct_print').attachEvent('onItemClick', chk_config_item_click)
@ -405,9 +411,22 @@ function get_admin_usos_cfdi(){
function get_admin_usuarios(){ function get_admin_usuarios(){
webix.ajax().sync().get('/values/allusuarios', function(text, data){ webix.ajax().sync().get('/values/allusuarios', function(text, data){
var values = data.json() var rows = data.json()
$$('grid_usuarios').clearAll() $$('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) { success: function(text, data, xhr) {
var values = data.json() var values = data.json()
//~ showvar(values)
Object.keys(values).forEach(function(key){ Object.keys(values).forEach(function(key){
$$(key).setValue(values[key]) $$(key).setValue(values[key])
}) })
@ -814,6 +832,7 @@ function cmd_probar_correo_click(){
function save_config_mail(values){ function save_config_mail(values){
values['opt'] = 'save_mail'
webix.ajax().sync().post('/config', values, { webix.ajax().sync().post('/config', values, {
error: function(text, data, xhr) { error: function(text, data, xhr) {
msg = 'Error al guardar la configuración' msg = 'Error al guardar la configuración'
@ -1277,6 +1296,7 @@ function tab_options_change(nv, ov){
var cv = { var cv = {
tab_admin_templates: 'templates', tab_admin_templates: 'templates',
tab_admin_partners: 'partners', tab_admin_partners: 'partners',
tab_admin_products: 'admin_products',
tab_admin_complements: 'complements', tab_admin_complements: 'complements',
tab_admin_otros: 'configotros', 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('onKeyPress', txt_folio_custom_key_press);
$$('txt_folio_custom').attachEvent('onBlur', txt_folio_custom_lost_focus); $$('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) webix.extend($$('grid_invoices'), webix.ProgressBar)
init_config_invoices() init_config_invoices()
@ -217,6 +220,7 @@ function default_config(){
$$('grid_details').showColumn('student') $$('grid_details').showColumn('student')
} }
show('fs_students', values.cfdi_edu) show('fs_students', values.cfdi_edu)
show('fs_divisas', values.cfdi_divisas)
show('txt_folio_custom', values.cfdi_folio_custom) show('txt_folio_custom', values.cfdi_folio_custom)
}) })
} }
@ -638,6 +642,7 @@ function guardar_y_timbrar(values){
data['donativo'] = donativo data['donativo'] = donativo
data['notas'] = values.notas data['notas'] = values.notas
data['folio_custom'] = $$('txt_folio_custom').getValue() data['folio_custom'] = $$('txt_folio_custom').getValue()
data['divisas'] = $$('opt_divisas').getValue()
var usar_ine = $$('chk_cfdi_usar_ine').getValue() var usar_ine = $$('chk_cfdi_usar_ine').getValue()
if(usar_ine){ if(usar_ine){
@ -1376,20 +1381,23 @@ function cmd_invoice_cancelar_click(){
} }
function get_invoices(rango){ function get_filters_invoices(){
if(rango == undefined){ var filters = $$('filter_dates').getValue()
var fy = $$('filter_year') filters['year'] = $$('filter_year').getValue()
var fm = $$('filter_month') filters['month'] = $$('filter_month').getValue()
filters['client'] = $$('grid_invoices').getFilter('cliente').value
return filters
}
var y = fy.getValue()
var m = fm.getValue() function get_invoices(){
rango = {'year': y, 'month': m} var filters = get_filters_invoices()
} filters['by'] = 'dates'
var grid = $$('grid_invoices') var grid = $$('grid_invoices')
grid.showProgress({type: 'icon'}) grid.showProgress({type: 'icon'})
webix.ajax().get('/invoices', rango, { webix.ajax().get('/invoices', filters, {
error: function(text, data, xhr) { error: function(text, data, xhr) {
msg_error('Error al consultar') msg_error('Error al consultar')
}, },
@ -1416,7 +1424,7 @@ function filter_month_change(nv, ov){
function filter_dates_change(range){ function filter_dates_change(range){
if(range.start != null && range.end != null){ 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()) 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_delete').attachEvent('onItemClick', cmd_nomina_delete_click)
$$('cmd_nomina_timbrar').attachEvent('onItemClick', cmd_nomina_timbrar_click) $$('cmd_nomina_timbrar').attachEvent('onItemClick', cmd_nomina_timbrar_click)
$$('cmd_nomina_log').attachEvent('onItemClick', cmd_nomina_log_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) $$('cmd_nomina_cancel').attachEvent('onItemClick', cmd_nomina_cancel_click)
$$('grid_nomina').attachEvent('onItemClick', grid_nomina_click) $$('grid_nomina').attachEvent('onItemClick', grid_nomina_click)
$$('filter_year_nomina').attachEvent('onChange', filter_year_nomina_change) $$('filter_year_nomina').attachEvent('onChange', filter_year_nomina_change)
@ -491,3 +492,28 @@ function cancel_nomina(id){
function cmd_nomina_log_click(){ function cmd_nomina_log_click(){
location = '/doc/nomlog/0' 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(){ function products_default_config(){
webix.ajax().get('/config', {'fields': 'productos'}, { webix.ajax().get('/config', {'fields': 'main_products'}, {
error: function(text, data, xhr) { error: function(text, data, xhr) {
msg = 'Error al consultar' msg = 'Error al consultar'
msg_error(msg) msg_error(msg)
@ -18,6 +18,7 @@ function products_default_config(){
if(cfg_products['inventario']){ if(cfg_products['inventario']){
$$('grid_products').showColumn('existencia') $$('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(); 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)){ if(!validate_sat_key_product(values.clave_sat, false)){
msg_error('La clave SAT no existe') msg_error('La clave SAT no existe')
return return

View File

@ -681,18 +681,6 @@ var options_admin_otros = [
labelRight: 'Ayuda para generar anticipos'}, labelRight: 'Ayuda para generar anticipos'},
{}]}, {}]},
{maxHeight: 20}, {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'}, {template: 'Punto de venta', type: 'section'},
{cols: [{maxWidth: 15}, {cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_usar_punto_de_venta', labelWidth: 0, {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 = [ var options_admin_complements = [
{maxHeight: 20}, {maxHeight: 20},
{cols: [{maxWidth: 15}, {cols: [{maxWidth: 15},
@ -753,6 +756,12 @@ var options_admin_complements = [
{view: 'text', id: 'txt_config_cfdipay_folio', name: 'txt_config_cfdipay_serie', {view: 'text', id: 'txt_config_cfdipay_folio', name: 'txt_config_cfdipay_serie',
label: 'Folio', labelWidth: 50, labelAlign: 'right'}, label: 'Folio', labelWidth: 50, labelAlign: 'right'},
{maxWidth: 15}]}, {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}}, rows: options_templates}},
{header: 'Clientes y Proveedores', body: {id: 'tab_admin_partners', {header: 'Clientes y Proveedores', body: {id: 'tab_admin_partners',
view: 'scrollview', body: {rows: options_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', {header: 'Complementos', body: {id: 'tab_admin_complements',
view: 'scrollview', body: {rows: options_admin_complements}}}, view: 'scrollview', body: {rows: options_admin_complements}}},
{header: 'Otros', body: {id: 'tab_admin_otros', view: 'scrollview', {header: 'Otros', body: {id: 'tab_admin_otros', view: 'scrollview',
@ -1214,6 +1225,10 @@ var usuarios_admin = [
{maxHeight: 20}, {maxHeight: 20},
{template: 'Usuarios Registrados', type: 'section'}, {template: 'Usuarios Registrados', type: 'section'},
{cols: [{maxWidth: 10}, grid_usuarios, {maxWidth: 10}]}, {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}, labelWidth: 50, width: 200, options: months},
{view: 'daterangepicker', id: 'filter_dates', label: 'Fechas', {view: 'daterangepicker', id: 'filter_dates', label: 'Fechas',
labelAlign: 'right', width: 300}, 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 = { var body_regimen_fiscal = {
view: 'richselect', view: 'richselect',
id: 'lst_regimen_fiscal', id: 'lst_regimen_fiscal',
@ -593,6 +602,7 @@ var controls_generate = [
{view: 'fieldset', label: 'Comprobante', body: body_comprobante}, {view: 'fieldset', label: 'Comprobante', body: body_comprobante},
{view: 'fieldset', label: 'Opciones de Pago', body: body_opciones}, {view: 'fieldset', label: 'Opciones de Pago', body: body_opciones},
{view: 'fieldset', id: 'fs_moneda', label: 'Moneda', body: body_moneda}, {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', {view: 'fieldset', id: 'fs_regimen_fiscal', label: 'Regimen Fiscal',
body: body_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_blog = "<a class='link_default' target='_blank' href='https://blog.empresalibre.mx'>Blog</a>";
var link_doc = "<a class='link_doc' target='_blank' href='https://doc.empresalibre.net'><b>?</b> </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 = { 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_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, menu_user,
{view: 'button', id: 'cmd_update_timbres', type: 'icon', width: 45, {view: 'button', id: 'cmd_update_timbres', type: 'icon', width: 45,
css: 'app_button', icon: 'bell-o', badge: 0}, 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'}, type: 'iconButton', autowidth: true, icon: 'check-circle'},
{view: 'button', id: 'cmd_nomina_log', label: 'Log', {view: 'button', id: 'cmd_nomina_log', label: 'Log',
type: 'iconButton', autowidth: true, icon: 'download'}, 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', {view: 'button', id: 'cmd_nomina_cancel', label: 'Cancelar',
type: 'iconButton', autowidth: true, icon: 'ban'}, type: 'iconButton', autowidth: true, icon: 'ban'},

View File

@ -135,9 +135,13 @@ var controls_generals = [
{view: "richselect", id: "unidad", name: "unidad", label: "Unidad", {view: "richselect", id: "unidad", name: "unidad", label: "Unidad",
width: 300, labelWidth: 130, labelAlign: "right", required: true, width: 300, labelWidth: 130, labelAlign: "right", required: true,
invalidMessage: "La Unidad es requerida", options: []}, invalidMessage: "La Unidad es requerida", options: []},
{view: 'text', id: 'tags_producto', name: 'tags_producto', {view: 'text', id: 'cant_by_packing', name: 'cant_by_packing',
labelAlign: 'right', label: 'Etiquetas', labelAlign: 'right', labelWidth: 150, inputAlign: "right",
placeholder: 'Separadas por comas'} label: 'Cantidad por empaque:'},
{},
//~ {view: 'text', id: 'tags_producto', name: 'tags_producto',
//~ labelAlign: 'right', label: 'Etiquetas',
//~ placeholder: 'Separadas por comas'}
]}, ]},
{cols: [ {cols: [
{view: "currency", type: "text", id: "valor_unitario", {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="ine11.xslt"/>
<xsl:include href="iedu.xslt"/> <xsl:include href="iedu.xslt"/>
<xsl:include href="pagos10.xslt"/> <xsl:include href="pagos10.xslt"/>
<xsl:include href="divisas.xslt"/>
<!-- <!--
<xsl:include href="ecc11.xslt"/> <xsl:include href="ecc11.xslt"/>
<xsl:include href="Divisas.xslt"/>
<xsl:include href="pfic.xslt"/> <xsl:include href="pfic.xslt"/>
<xsl:include href="TuristaPasajeroExtranjero.xslt"/> <xsl:include href="TuristaPasajeroExtranjero.xslt"/>
<xsl:include href="cfdiregistrofiscal.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>