diff --git a/README.md b/README.md
index 49ffdad..fe9bc4f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Este proyecto está en continuo desarrollo, contratar un esquema de soporte,
nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para
-contratar.
+contratar: administracion@empresalibre.net
### Requerimientos:
@@ -16,6 +16,9 @@ contratar.
* Servidor web, recomendado Nginx
* uwsgi
* python3
+* xsltproc
+* openssl
+* xmlsec
Debería de funcionar con cualquier combinación servidor-wsgi que soporte
aplicaciones Python.
diff --git a/source/app/conf.py.example b/source/app/conf.py.example
index 1f8e28d..70d42ef 100644
--- a/source/app/conf.py.example
+++ b/source/app/conf.py.example
@@ -1,10 +1,4 @@
#!/usr/bin/env python
-from peewee import SqliteDatabase
DEBUG = True
-ID_SUPPORT = ''
-
-DATABASE = None
-if DEBUG:
- DATABASE = SqliteDatabase('empresalibre.sqlite')
diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py
index 25b7fa4..b99fb87 100644
--- a/source/app/controllers/pac.py
+++ b/source/app/controllers/pac.py
@@ -371,7 +371,7 @@ class Finkok(object):
if os.path.isfile(file_xml):
root = etree.parse(file_xml).getroot()
else:
- root = etree.fromstring(file_xml)
+ root = etree.fromstring(file_xml.encode())
xml = etree.tostring(root)
@@ -385,8 +385,12 @@ class Finkok(object):
'store_pending': True,
}
- result = client.service.cancel_signature(**args)
- return result
+ try:
+ result = client.service.cancel_signature(**args)
+ return result
+ except Fault as e:
+ self.error = str(e)
+ return ''
def get_acuse(self, rfc, uuids, type_acuse='C'):
for u in uuids:
@@ -443,21 +447,41 @@ class Finkok(object):
return result
def add_token(self, rfc, email):
- """
+ """Agrega un nuevo token al cliente para timbrado.
Se requiere cuenta de reseller para usar este método
+
+ Args:
+ rfc (str): El RFC del cliente, ya debe existir
+ email (str): El correo del cliente, funciona como USER al timbrar
+
+ Returns:
+ dict
+ 'username': 'username',
+ 'status': True or False
+ 'name': 'name',
+ 'success': True or False
+ 'token': 'Token de timbrado',
+ 'message': None
"""
+ auth = AUTH['RESELLER']
+
method = 'util'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
- 'username': AUTH['USER'],
- 'password': AUTH['PASS'],
+ 'username': auth['USER'],
+ 'password': auth['PASS'],
'name': rfc,
'token_username': email,
'taxpayer_id': rfc,
'status': True,
}
- result = client.service.add_token(**args)
+ try:
+ result = client.service.add_token(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
return result
def get_date(self):
@@ -477,17 +501,31 @@ class Finkok(object):
return result.datetime
def add_client(self, rfc, type_user=False):
- """
+ """Agrega un nuevo cliente para timbrado.
Se requiere cuenta de reseller para usar este método
- type_user: False == 'P' == Prepago or True == 'O' == On demand
+
+ Args:
+ rfc (str): El RFC del nuevo cliente
+
+ Kwargs:
+ type_user (bool): False == 'P' == Prepago or True == 'O' == On demand
+
+ Returns:
+ dict
+ 'message':
+ 'Account Created successfully'
+ 'Account Already exists'
+ 'success': True or False
"""
+ auth = AUTH['RESELLER']
+
tu = {False: 'P', True: 'O'}
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
- 'reseller_username': AUTH['USER'],
- 'reseller_password': AUTH['PASS'],
+ 'reseller_username': auth['USER'],
+ 'reseller_password': auth['PASS'],
'taxpayer_id': rfc,
'type_user': tu[type_user],
'added': datetime.datetime.now().isoformat()[:19],
@@ -505,13 +543,15 @@ class Finkok(object):
Se requiere cuenta de reseller para usar este método
status = 'A' or 'S'
"""
+ auth = AUTH['RESELLER']
+
sv = {False: 'S', True: 'A'}
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
- 'reseller_username': AUTH['USER'],
- 'reseller_password': AUTH['PASS'],
+ 'reseller_username': auth['USER'],
+ 'reseller_password': auth['PASS'],
'taxpayer_id': rfc,
'status': sv[status],
}
@@ -524,15 +564,35 @@ class Finkok(object):
return result
def get_client(self, rfc):
- """
+ """Regresa el estatus del cliente
+ .
Se requiere cuenta de reseller para usar este método
+
+ Args:
+ rfc (str): El RFC del emisor
+
+ Returns:
+ dict
+ 'message': None,
+ 'users': {
+ 'ResellerUser': [
+ {
+ 'status': 'A',
+ 'counter': 0,
+ 'taxpayer_id': '',
+ 'credit': 0
+ }
+ ]
+ } or None si no existe
"""
+ auth = AUTH['RESELLER']
+
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
- 'reseller_username': AUTH['USER'],
- 'reseller_password': AUTH['PASS'],
+ 'reseller_username': auth['USER'],
+ 'reseller_password': auth['PASS'],
'taxpayer_id': rfc,
}
@@ -548,15 +608,30 @@ class Finkok(object):
return result
def assign_client(self, rfc, credit):
- """
+ """Agregar credito a un emisor
+
Se requiere cuenta de reseller para usar este método
+
+ Args:
+ rfc (str): El RFC del emisor, debe existir
+ credit (int): Cantidad de folios a agregar
+
+ Returns:
+ dict
+ 'success': True or False,
+ 'credit': nuevo credito despues de agregar or None
+ 'message':
+ 'Success, added {credit} of credit to {RFC}'
+ 'RFC no encontrado'
"""
+ auth = AUTH['RESELLER']
+
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
- 'username': AUTH['USER'],
- 'password': AUTH['PASS'],
+ 'username': auth['USER'],
+ 'password': auth['PASS'],
'taxpayer_id': rfc,
'credit': credit,
}
@@ -577,7 +652,7 @@ def _get_data_sat(path):
if os.path.isfile(path):
tree = etree.parse(path).getroot()
else:
- tree = etree.fromstring(path)
+ tree = etree.fromstring(path.encode())
data = {}
emisor = escape(
@@ -602,7 +677,7 @@ def _get_data_sat(path):
def get_status_sat(xml):
data = _get_data_sat(xml)
if not data:
- return
+ return 'XML inválido'
URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?wsdl'
client = Client(URL, transport=Transport(cache=SqliteCache()))
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index 2b4fea4..76a007d 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -33,7 +33,8 @@ from dateutil import parser
from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice
from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \
- PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE
+ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \
+ PATH_XMLSEC, TEMPLATE_CANCEL
#~ def _get_hash(password):
@@ -52,7 +53,7 @@ def _get_md5(data):
return hashlib.md5(data.encode()).hexdigest()
-def _save_temp(data, modo='wb'):
+def save_temp(data, modo='wb'):
path = tempfile.mkstemp()[1]
with open(path, modo) as f:
f.write(data)
@@ -276,21 +277,22 @@ def to_slug(string):
class Certificado(object):
- def __init__(self, key, cer):
- self._key = key
- self._cer = cer
+ def __init__(self, paths):
+ self._path_key = paths['path_key']
+ self._path_cer = paths['path_cer']
self._modulus = ''
- self._save_files()
+ #~ self._save_files()
self.error = ''
- def _save_files(self):
- try:
- self._path_key = _save_temp(self._key)
- self._path_cer = _save_temp(self._cer)
- except:
- self._path_key = ''
- self._path_cer = ''
- return
+ #~ def _save_files(self):
+ #~ try:
+ #~ self._path_key = _save_temp(bytes(self._key))
+ #~ self._path_cer = _save_temp(bytes(self._cer))
+ #~ except Exception as e:
+ #~ log.error(e)
+ #~ self._path_key = ''
+ #~ self._path_cer = ''
+ #~ return
def _kill(self, path):
try:
@@ -341,8 +343,7 @@ class Certificado(object):
hasta = parser.parse(dates[1].split('=')[1])
self._modulus = _call(args.format(self._path_cer, 'modulus'))
- data['cer'] = self._cer
- data['cer_tmp'] = None
+ data['cer'] = read_file(self._path_cer)
data['cer_pem'] = cer_pem
data['cer_txt'] = cer_txt.replace('\n', '')
data['serie'] = serie
@@ -365,7 +366,8 @@ class Certificado(object):
'pass:"{}" -out "{}"'
_call(args.format(tmp_cer, tmp_key, rfc,
hashlib.md5(rfc.encode()).hexdigest(), tmp_p12))
- data = open(tmp_p12, 'rb').read()
+ #~ data = open(tmp_p12, 'rb').read()
+ data = read_file(tmp_p12)
self._kill(tmp_cer)
self._kill(tmp_key)
@@ -396,18 +398,20 @@ class Certificado(object):
self._path_key, password, _get_md5(rfc))
key_enc = _call(args)
- data['key'] = self._key
- data['key_tmp'] = None
+ data['key'] = read_file(self._path_key)
data['key_enc'] = key_enc
data['p12'] = self._get_p12(password, rfc)
return data
def validate(self, password, rfc):
if not self._path_key or not self._path_cer:
- self.error = 'Error al cargar el certificado'
+ self.error = 'Error en las rutas temporales del certificado'
return {}
data = self._get_info_cer(rfc)
+ if not data:
+ return {}
+
llave = self._get_info_key(password, data['rfc'])
if not llave:
return {}
@@ -432,9 +436,9 @@ def make_xml(data, certificado):
data = {
'xsltproc': PATH_XSLTPROC,
'xslt': _join(PATH_XSLT, 'cadena.xslt'),
- 'xml': _save_temp(xml, 'w'),
+ 'xml': save_temp(xml, 'w'),
'openssl': PATH_OPENSSL,
- 'key': _save_temp(certificado.key_enc, 'w'),
+ 'key': save_temp(certificado.key_enc, 'w'),
'pass': _get_md5(certificado.rfc)
}
args = '"{xsltproc}" "{xslt}" "{xml}" | ' \
@@ -473,6 +477,11 @@ def timbra_xml(xml, auth):
return result
+def get_sat(xml):
+ from .pac import get_status_sat
+ return get_status_sat(xml)
+
+
class LIBO(object):
HOST = 'localhost'
PORT = '8100'
@@ -1036,6 +1045,59 @@ def get_path_temp():
return tempfile.mkstemp()[1]
+def get_date(value, next_day=False):
+ d = parser.parse(value)
+ if next_day:
+ return d + datetime.timedelta(days=1)
+ return d
+
+
+def cancel_cfdi(uuid, pk12, rfc, auth):
+ from .pac import Finkok as PAC
+
+ template = read_file(TEMPLATE_CANCEL, 'r')
+ data = {
+ 'rfc': rfc,
+ 'fecha': datetime.datetime.now().isoformat()[:19],
+ 'uuid': str(uuid).upper(),
+ }
+ template = template.format(**data)
+
+ data = {
+ 'xmlsec': PATH_XMLSEC,
+ 'pk12': save_temp(pk12),
+ 'pass': _get_md5(rfc),
+ 'template': save_temp(template, 'w'),
+ }
+ args = '"{xmlsec}" --sign --pkcs12 "{pk12}" --pwd {pass} ' \
+ '"{template}"'.format(**data)
+ xml_sign = _call(args)
+
+ if DEBUG:
+ auth = {}
+ else:
+ if not auth:
+ msg = 'Sin datos para cancelar'
+ result = {'ok': False, 'error': msg}
+ return result
+
+ msg = 'Factura cancelada correctamente'
+ data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'}}
+ pac = PAC(auth)
+ result = pac.cancel_signature(xml_sign)
+ if result:
+ codes = {None: '',
+ 'Could not get UUID Text': 'UUID no encontrado'}
+ if not result['CodEstatus'] is None:
+ data['ok'] = False
+ data['msg'] = codes.get(result['CodEstatus'], result['CodEstatus'])
+ else:
+ data['ok'] = False
+ data['msg'] = pac.error
+
+ return data, result
+
+
class ImportFacturaLibre(object):
def __init__(self, path):
diff --git a/source/app/main.ini b/source/app/main.ini
index 6041275..e4a68ca 100644
--- a/source/app/main.ini
+++ b/source/app/main.ini
@@ -2,6 +2,7 @@
socket = 127.0.0.1:3033
uid = nginx
gid = nginx
+#~ Establece una ruta accesible para nginx o el servidor web que uses
chdir = /srv/app/empresa-libre/app
wsgi-file = main.py
callable = app
@@ -10,4 +11,5 @@ processes = 4
threads = 4
thunder-lock = true
#~ stats = 127.0.0.1:9191
+#~ Establece una ruta accesible para nginx o el servidor web que uses
logger = file:/srv/log/empresalibre-uwsgi.log
diff --git a/source/app/main.py b/source/app/main.py
index a35833f..66da61d 100644
--- a/source/app/main.py
+++ b/source/app/main.py
@@ -45,7 +45,6 @@ api.add_route('/products', AppProducts(db))
api.add_route('/invoices', AppInvoices(db))
-
if DEBUG:
api.add_sink(static, '/static')
diff --git a/source/app/main_debug.ini b/source/app/main_debug.ini
index 89cb94f..065dcfc 100644
--- a/source/app/main_debug.ini
+++ b/source/app/main_debug.ini
@@ -1,6 +1,5 @@
[uwsgi]
http = 127.0.0.1:8000
-#~ http = 37.228.132.181:9000
wsgi-file = main.py
callable = app
master = true
diff --git a/source/app/models/db.py b/source/app/models/db.py
index f5a460d..1b09e1f 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -20,8 +20,8 @@ class StorageEngine(object):
def add_config(self, values):
return main.Configuracion.add(values)
- def add_cert(self, file_object):
- return main.Certificado.add(file_object)
+ def add_cert(self, file_obj):
+ return main.Certificado.add(file_obj)
def validate_cert(self, values, session):
return main.Certificado.validate(values, session)
@@ -32,6 +32,15 @@ class StorageEngine(object):
def send_email(self, values, session):
return main.Facturas.send(values['id'], session['rfc'])
+ def _get_cancelinvoice(self, values):
+ return main.Facturas.cancel(values['id'])
+
+ def _get_statussat(self, values):
+ return main.Facturas.get_status_sat(values['id'])
+
+ def _get_filteryears(self, values):
+ return main.Facturas.filter_years()
+
def _get_cert(self, values):
return main.Certificado.get_data()
diff --git a/source/app/models/main.py b/source/app/models/main.py
index bc4ae98..946d09f 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -64,6 +64,9 @@ class Configuracion(BaseModel):
clave = TextField(unique=True)
valor = TextField(default='')
+ def __str__(self):
+ return '{} = {}'.format(self.clave, self.valor)
+
@classmethod
def get_(cls, keys):
if keys['fields'] == 'correo':
@@ -74,7 +77,14 @@ class Configuracion(BaseModel):
.select()
.where(Configuracion.clave.in_(fields))
)
- values = {r.clave: r.valor for r in data}
+ elif keys['fields'] == 'path_cer':
+ fields = ('path_key', 'path_cer')
+ data = (Configuracion
+ .select()
+ .where(Configuracion.clave.in_(fields))
+ )
+
+ values = {r.clave: r.valor for r in data}
return values
@classmethod
@@ -289,10 +299,8 @@ class Emisor(BaseModel):
class Certificado(BaseModel):
key = BlobField(null=True)
- key_tmp = BlobField(null=True)
key_enc = TextField(default='')
cer = BlobField(null=True)
- cer_tmp = BlobField(null=True)
cer_pem = TextField(default='')
cer_txt = TextField(default='')
p12 = BlobField(null=True)
@@ -316,28 +324,26 @@ class Certificado(BaseModel):
return row
def get_(cls):
- if Certificado.select().count():
- obj = Certificado.select()[0]
- else:
- obj = Certificado()
- return obj
+ return Certificado.select()[0]
@classmethod
- def add(cls, file_object):
- obj = cls.get_(cls)
- if file_object.filename.endswith('key'):
- obj.key_tmp = file_object.file.read()
- elif file_object.filename.endswith('cer'):
- obj.cer_tmp = file_object.file.read()
- obj.save()
+ def add(cls, file_obj):
+ if file_obj.filename.endswith('key'):
+ path_key = util.save_temp(file_obj.file.read())
+ Configuracion.add({'path_key': path_key})
+ elif file_obj.filename.endswith('cer'):
+ path_cer = util.save_temp(file_obj.file.read())
+ Configuracion.add({'path_cer': path_cer})
return {'status': 'server'}
@classmethod
def validate(cls, values, session):
row = {}
result = False
+
obj = cls.get_(cls)
- cert = util.Certificado(obj.key_tmp, obj.cer_tmp)
+ paths = Configuracion.get_({'fields': 'path_cer'})
+ cert = util.Certificado(paths)
data = cert.validate(values['contra'], session['rfc'])
if data:
msg = 'Certificado guardado correctamente'
@@ -352,9 +358,9 @@ class Certificado(BaseModel):
}
else:
msg = cert.error
- obj.key_tmp = None
- obj.cer_tmp = None
- obj.save()
+
+ Configuracion.add({'path_key': ''})
+ Configuracion.add({'path_cer': ''})
return {'ok': result, 'msg': msg, 'data': row}
@@ -666,8 +672,10 @@ class Socios(BaseModel):
es_proveedor = BooleanField(default=False)
cuenta_cliente = TextField(default='')
cuenta_proveedor = TextField(default='')
- saldo_cliente = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- saldo_proveedor = DecimalField(default=0.0, decimal_places=6, auto_round=True)
+ saldo_cliente = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ saldo_proveedor = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
web = TextField(default='')
correo_facturas = TextField(default='')
forma_pago = ForeignKeyField(SATFormaPago, null=True)
@@ -723,7 +731,7 @@ class Socios(BaseModel):
.where((Socios.id==id) & (Socios.es_cliente==True))
.dicts()
)
- print (id, row)
+ #~ print (id, row)
if len(row):
return {'ok': True, 'row': row[0]}
return {'ok': False}
@@ -801,12 +809,17 @@ class Productos(BaseModel):
clave_sat = TextField(default='')
descripcion = TextField(index=True)
unidad = ForeignKeyField(SATUnidades)
- valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- ultimo_costo = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True)
+ valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ ultimo_costo = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
inventario = BooleanField(default=False)
- existencia = DoubleField(default=0.0)
- minimo = DoubleField(default=0.0)
+ existencia = DecimalField(default=0.0, max_digits=18, decimal_places=2,
+ auto_round=True)
+ minimo = DecimalField(default=0.0, max_digits=18, decimal_places=2,
+ auto_round=True)
codigo_barras = TextField(default='')
cuenta_predial = TextField(default='')
es_activo = BooleanField(default=True)
@@ -818,7 +831,11 @@ class Productos(BaseModel):
@classmethod
def next_key(cls):
- value = Productos.select(fn.Max(Productos.id)).scalar()
+ value = (Productos
+ .select(fn.Max(Productos.id))
+ .group_by(Productos.id)
+ .order_by(Productos.id)
+ .scalar())
if value is None:
value = 1
else:
@@ -982,28 +999,35 @@ class Facturas(BaseModel):
fecha_timbrado = DateTimeField(null=True)
forma_pago = TextField(default='')
condiciones_pago = TextField(default='')
- subtotal = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True)
+ subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
moneda = TextField(default='MXN')
tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True)
- total = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- total_mn = DecimalField(default=0.0, decimal_places=6, auto_round=True)
+ total = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
tipo_comprobante = TextField(default='I')
metodo_pago = TextField(default='PUE')
lugar_expedicion = TextField(default='')
confirmacion = TextField(default='')
uso_cfdi = TextField(default='')
total_retenciones = DecimalField(
- decimal_places=6, auto_round=True, null=True)
+ max_digits=20, decimal_places=6, auto_round=True, null=True)
total_trasladados = DecimalField(
- decimal_places=6, auto_round=True, null=True)
+ max_digits=20, decimal_places=6, auto_round=True, null=True)
xml = TextField(default='')
uuid = UUIDField(null=True)
estatus = TextField(default='Guardada')
+ estatus_sat = TextField(default='Vigente')
regimen_fiscal = TextField(default='')
notas = TextField(default='')
pagada = BooleanField(default=False)
cancelada = BooleanField(default=False)
+ fecha_cancelacion = DateTimeField(null=True)
+ acuse = TextField(default='')
donativo = BooleanField(default=False)
tipo_relacion = TextField(default='')
error = TextField(default='')
@@ -1011,6 +1035,38 @@ class Facturas(BaseModel):
class Meta:
order_by = ('fecha',)
+ @classmethod
+ def cancel(cls, id):
+ msg = 'Factura cancelada correctamente'
+ auth = Emisor.get_auth()
+ certificado = Certificado.select()[0]
+ obj = Facturas.get(Facturas.id==id)
+ data, result = util.cancel_cfdi(
+ obj.uuid, certificado.p12, certificado.rfc, auth)
+ if data['ok']:
+ obj.estatus = 'Cancelada'
+ obj.error = ''
+ obj.cancelada = True
+ obj.fecha_cancelacion = result['Fecha']
+ obj.acuse = result['Acuse']
+ else:
+ obj.error = data['msg']
+ obj.save()
+ return data
+
+ @classmethod
+ def filter_years(cls):
+ data = [{'id': -1, 'value': 'Todos'}]
+ rows = (Facturas
+ .select(Facturas.fecha.year)
+ .group_by(Facturas.fecha.year)
+ .order_by(Facturas.fecha.year)
+ .scalar(as_tuple=True)
+ )
+ if not rows is None:
+ data += [{'id': int(row), 'value': int(row)} for row in rows]
+ return tuple(data)
+
@classmethod
def get_xml(cls, id):
obj = Facturas.get(Facturas.id==id)
@@ -1171,10 +1227,27 @@ class Facturas(BaseModel):
@classmethod
def get_(cls, values):
+ if 'start' in values:
+ filters = Facturas.fecha.between(
+ util.get_date(values['start']),
+ util.get_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)
+
rows = tuple(Facturas
.select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid,
Facturas.fecha, Facturas.tipo_comprobante, Facturas.estatus,
Facturas.total_mn, Socios.nombre.alias('cliente'))
+ .where(filters)
.join(Socios)
.switch(Facturas).dicts()
)
@@ -1190,6 +1263,8 @@ class Facturas(BaseModel):
q.execute()
q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj)
q.execute()
+ q = FacturasRelacionadas.delete().where(FacturasRelacionadas.factura==obj)
+ q.execute()
return bool(obj.delete_instance())
def _get_folio(self, serie):
@@ -1198,6 +1273,8 @@ class Facturas(BaseModel):
inicio = (Facturas
.select(fn.Max(Facturas.folio))
.where(Facturas.serie==serie)
+ .group_by(Facturas.folio)
+ .order_by(Facturas.folio)
.scalar())
if inicio is None:
inicio = inicio_serie
@@ -1437,6 +1514,13 @@ class Facturas(BaseModel):
}
return util.make_xml(data, certificado)
+ @classmethod
+ def get_status_sat(cls, id):
+ obj = Facturas.get(Facturas.id == id)
+ obj.estatus_sat = util.get_sat(obj.xml)
+ obj.save()
+ return obj.estatus_sat
+
@classmethod
def timbrar(cls, id):
obj = Facturas.get(Facturas.id == id)
@@ -1446,7 +1530,7 @@ class Facturas(BaseModel):
auth = Emisor.get_auth()
- error = False
+ #~ error = False
msg = 'Factura timbrada correctamente'
result = util.timbra_xml(obj.xml, auth)
if result['ok']:
@@ -1455,10 +1539,10 @@ class Facturas(BaseModel):
obj.fecha_timbrado = result['fecha']
obj.estatus = 'Timbrada'
obj.error = ''
- obj.save()
+ #~ obj.save()
row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
else:
- error = True
+ #~ error = True
msg = result['error']
obj.estatus = 'Error'
obj.error = msg
@@ -1467,25 +1551,69 @@ class Facturas(BaseModel):
return {'ok': result['ok'], 'msg': msg, 'row': row}
+class PreFacturas(BaseModel):
+ cliente = ForeignKeyField(Socios)
+ serie = TextField(default='PRE')
+ folio = IntegerField(default=0)
+ fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
+ forma_pago = TextField(default='')
+ condiciones_pago = TextField(default='')
+ subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ moneda = TextField(default='MXN')
+ tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True)
+ total = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ tipo_comprobante = TextField(default='I')
+ metodo_pago = TextField(default='PUE')
+ lugar_expedicion = TextField(default='')
+ uso_cfdi = TextField(default='')
+ total_retenciones = DecimalField(
+ max_digits=20, decimal_places=6, auto_round=True, null=True)
+ total_trasladados = DecimalField(
+ max_digits=20, decimal_places=6, auto_round=True, null=True)
+ estatus = TextField(default='Generada')
+ regimen_fiscal = TextField(default='')
+ notas = TextField(default='')
+ donativo = BooleanField(default=False)
+ tipo_relacion = TextField(default='')
+
+ class Meta:
+ order_by = ('fecha',)
+
+
class FacturasRelacionadas(BaseModel):
factura = ForeignKeyField(Facturas, related_name='original')
factura_origen = ForeignKeyField(Facturas, related_name='relacion')
class Meta:
order_by = ('factura',)
- indexes = (
- (('factura', 'factura_origen'), True),
- )
+
+class PreFacturasRelacionadas(BaseModel):
+ factura = ForeignKeyField(PreFacturas, related_name='original')
+ factura_origen = ForeignKeyField(PreFacturas, related_name='relacion')
+
+ class Meta:
+ order_by = ('factura',)
class FacturasDetalle(BaseModel):
factura = ForeignKeyField(Facturas)
producto = ForeignKeyField(Productos, null=True)
- cantidad = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- precio_final = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- importe = DecimalField(default=0.0, decimal_places=6, auto_round=True)
+ cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ importe = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
descripcion = TextField(default='')
unidad = TextField(default='')
clave = TextField(default='')
@@ -1504,11 +1632,54 @@ class FacturasDetalle(BaseModel):
order_by = ('factura',)
+class PreFacturasDetalle(BaseModel):
+ factura = ForeignKeyField(PreFacturas)
+ producto = ForeignKeyField(Productos, null=True)
+ cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+ importe = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ aduana = TextField(default='')
+ pedimento = TextField(default='')
+ fecha_pedimento = DateField(null=True)
+ alumno = TextField(default='')
+ curp = TextField(default='')
+ nivel = TextField(default='')
+ autorizacion = TextField(default='')
+ cuenta_predial = TextField(default='')
+
+ class Meta:
+ order_by = ('factura',)
+
+
class FacturasImpuestos(BaseModel):
factura = ForeignKeyField(Facturas)
impuesto = ForeignKeyField(SATImpuestos)
- base = DecimalField(default=0.0, decimal_places=6, auto_round=True)
- importe = DecimalField(default=0.0, decimal_places=6, auto_round=True)
+ base = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ importe = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+
+ class Meta:
+ order_by = ('factura',)
+ indexes = (
+ (('factura', 'impuesto'), True),
+ )
+
+
+class PreFacturasImpuestos(BaseModel):
+ factura = ForeignKeyField(PreFacturas)
+ impuesto = ForeignKeyField(SATImpuestos)
+ base = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ importe = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
class Meta:
order_by = ('factura',)
@@ -1596,12 +1767,14 @@ def test_correo(values):
return util.send_mail(data)
-def _init_values():
+def _init_values(rfc):
data = (
{'clave': 'version', 'valor': VERSION},
{'clave': 'rfc_publico', 'valor': 'XAXX010101000'},
{'clave': 'rfc_extranjero', 'valor': 'XEXX010101000'},
{'clave': 'decimales', 'valor': '2'},
+ {'clave': 'path_key', 'valor': ''},
+ {'clave': 'path_cer', 'valor': ''},
)
for row in data:
try:
@@ -1609,6 +1782,10 @@ def _init_values():
Configuracion.create(**row)
except IntegrityError:
pass
+
+ if not Certificado.select().count():
+ Certificado.create(rfc=rfc)
+
log.info('Valores iniciales insertados...')
return
@@ -1617,6 +1794,8 @@ def _crear_tablas(rfc):
tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion,
Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios,
FacturasRelacionadas, Productos,
+ PreFacturas, PreFacturasDetalle, PreFacturasImpuestos,
+ PreFacturasRelacionadas,
SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes,
SATTipoRelacion, SATUnidades, SATUsoCfdi,
Socios, Tags, Usuarios,
@@ -1639,7 +1818,7 @@ def _crear_tablas(rfc):
msg = 'El usuario ya existe'
log.error(msg)
- _init_values()
+ _init_values(rfc)
_importar_valores('', rfc)
return True
diff --git a/source/app/settings.py b/source/app/settings.py
index 1776135..b968e87 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -11,7 +11,7 @@ from conf import DEBUG
DEBUG = DEBUG
-VERSION = '0.1.0'
+VERSION = '0.2.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
@@ -25,6 +25,8 @@ DB_SAT = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'sat.db'))
IV = 'valores_iniciales.json'
INIT_VALUES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', IV))
+CT = 'cancel_template.xml'
+TEMPLATE_CANCEL = os.path.abspath(os.path.join(PATH_TEMPLATES, CT))
PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt'))
PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin'))
@@ -50,6 +52,7 @@ if DEBUG:
level=LOG_LEVEL,
format_string=format_string).push_application()
else:
+ #~ Establece una ruta con acceso para nginx o el servidor web que uses
LOG_PATH = '/srv/log/empresalibre.log'
RotatingFileHandler(
LOG_PATH,
@@ -69,9 +72,11 @@ log = Logger(LOG_NAME)
PATH_XSLTPROC = 'xsltproc'
PATH_OPENSSL = 'openssl'
+PATH_XMLSEC = 'xmlsec1'
if 'win' in sys.platform:
PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe')
PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe')
+ PATH_XMLSEC = os.path.join(PATH_BIN, 'xmlsec.exe')
PRE = {
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index a79cdc2..b858d12 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -27,9 +27,7 @@ function get_series(){
function get_forma_pago(){
webix.ajax().get('/values/formapago', {key: true}, function(text, data){
var values = data.json()
- //~ pre = values[0]
$$('lst_forma_pago').getList().parse(values)
- //~ $$('lst_forma_pago').setValue(pre.id)
})
}
@@ -697,7 +695,19 @@ function grid_invoices_click(id, e, node){
function send_cancel(id){
- show(id)
+ webix.ajax().get('/values/cancelinvoice', {id: id}, function(text, data){
+ var values = data.json()
+ if(values.ok){
+ msg_sucess(values.msg)
+ gi.updateItem(id, values.row)
+ }else{
+ webix.alert({
+ title: 'Error al Cancelar',
+ text: values.msg,
+ type: 'alert-error'
+ })
+ }
+ })
}
function cmd_invoice_cancelar_click(){
@@ -717,6 +727,11 @@ function cmd_invoice_cancelar_click(){
return
}
+ if(row.estatus == 'Cancelada'){
+ msg_error('La factura ya esta cancelada')
+ return
+ }
+
msg = '¿Estás seguro de enviar a cancelar esta factura?
\
ESTA ACCIÓN NO SE PUEDE DESHACER'
webix.confirm({
@@ -732,3 +747,87 @@ function cmd_invoice_cancelar_click(){
}
})
}
+
+
+function get_invoices(rango){
+ if(rango == undefined){
+ var fy = $$('filter_year')
+ var fm = $$('filter_month')
+
+ var y = fy.getValue()
+ var m = fm.getValue()
+ rango = {'year': y, 'month': m}
+ }
+
+ var grid = $$('grid_invoices')
+ webix.ajax().get('/invoices', rango, {
+ error: function(text, data, xhr) {
+ webix.message({type: 'error', text: 'Error al consultar'})
+ },
+ success: function(text, data, xhr) {
+ var values = data.json();
+ grid.clearAll();
+ if (values.ok){
+ grid.parse(values.rows, 'json');
+ };
+ }
+ });
+}
+
+
+function filter_year_change(nv, ov){
+ get_invoices()
+}
+
+
+function filter_month_change(nv, ov){
+ get_invoices()
+}
+
+
+function filter_dates_change(range){
+ if(range.start != null && range.end != null){
+ get_invoices(range)
+ }
+}
+
+
+function cmd_invoice_sat_click(){
+ if(gi.count() == 0){
+ return
+ }
+
+ var row = gi.getSelectedItem()
+ if (row == undefined){
+ msg_error('Selecciona una factura')
+ return
+ }
+
+ if(!row.uuid){
+ msg_error('La factura no esta timbrada, solo es posible consultar \
+ el estatus en el SAT de facturas timbradas')
+ return
+ }
+
+ webix.ajax().get('/values/statussat', {id: row.id}, function(text, data){
+ var values = data.json()
+ show(values)
+ })
+
+}
+
+
+function cmd_prefactura_click(){
+ var form = this.getFormView()
+
+ if(!form.validate()) {
+ msg_error('Valores inválidos')
+ return
+ }
+
+ var values = form.getValues()
+ if(!validate_invoice(values)){
+ return
+ }
+ show('PreFactura')
+}
diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js
index 4dd4c24..b17e8ee 100644
--- a/source/static/js/controller/main.js
+++ b/source/static/js/controller/main.js
@@ -44,8 +44,13 @@ var controllers = {
$$('grid_details').attachEvent('onBeforeEditStart', grid_details_before_edit_start)
$$('grid_details').attachEvent('onBeforeEditStop', grid_details_before_edit_stop)
$$('cmd_invoice_timbrar').attachEvent('onItemClick', cmd_invoice_timbrar_click)
+ $$('cmd_invoice_sat').attachEvent('onItemClick', cmd_invoice_sat_click)
$$('cmd_invoice_cancelar').attachEvent('onItemClick', cmd_invoice_cancelar_click)
$$('grid_invoices').attachEvent('onItemClick', grid_invoices_click)
+ $$('filter_year').attachEvent('onChange', filter_year_change)
+ $$('filter_month').attachEvent('onChange', filter_month_change)
+ $$('filter_dates').attachEvent('onChange', filter_dates_change)
+ $$('cmd_prefactura').attachEvent('onItemClick', cmd_prefactura_click)
}
}
@@ -92,23 +97,6 @@ function get_products(){
}
-function get_invoices(){
- var grid = $$('grid_invoices')
- webix.ajax().get('/invoices', {}, {
- error: function(text, data, xhr) {
- webix.message({type: 'error', text: 'Error al consultar'})
- },
- success: function(text, data, xhr) {
- var values = data.json();
- grid.clearAll();
- if (values.ok){
- grid.parse(values.rows, 'json');
- };
- }
- });
-}
-
-
function menu_user_click(id, e, node){
if (id == 1){
window.location = '/logout';
@@ -117,6 +105,26 @@ function menu_user_click(id, e, node){
}
+function current_dates(){
+ var fy = $$('filter_year')
+ var fm = $$('filter_month')
+ var d = new Date()
+
+ fy.blockEvent()
+ fm.blockEvent()
+
+ fm.setValue(d.getMonth() + 1)
+ webix.ajax().sync().get('/values/filteryears', function(text, data){
+ var values = data.json()
+ fy.getList().parse(values)
+ fy.setValue(d.getFullYear())
+ })
+
+ fy.unblockEvent()
+ fm.unblockEvent()
+}
+
+
function multi_change(prevID, nextID){
//~ webix.message(nextID)
if(nextID == 'app_partners'){
@@ -138,6 +146,7 @@ function multi_change(prevID, nextID){
if(nextID == 'app_invoices'){
active = $$('multi_invoices').getActiveId()
if(active == 'invoices_home'){
+ current_dates()
get_invoices()
}
gi = $$('grid_invoices')
diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js
index 9e110c6..bfa028f 100644
--- a/source/static/js/ui/invoices.js
+++ b/source/static/js/ui/invoices.js
@@ -14,12 +14,41 @@ var toolbar_invoices = [
var toolbar_invoices_util = [
{view: 'button', id: 'cmd_invoice_timbrar', label: 'Timbrar',
type: 'iconButton', autowidth: true, icon: 'ticket'},
+ {view: 'button', id: 'cmd_invoice_sat', label: 'SAT',
+ type: 'iconButton', autowidth: true, icon: 'check-circle'},
{},
{view: 'button', id: 'cmd_invoice_cancelar', label: 'Cancelar',
type: 'iconButton', autowidth: true, icon: 'ban'},
]
+var months = [
+ {id: -1, value: 'Todos'},
+ {id: 1, value: 'Enero'},
+ {id: 2, value: 'Febrero'},
+ {id: 3, value: 'Marzo'},
+ {id: 4, value: 'Abril'},
+ {id: 5, value: 'Mayo'},
+ {id: 6, value: 'Junio'},
+ {id: 7, value: 'Julio'},
+ {id: 8, value: 'Agosto'},
+ {id: 9, value: 'Septiembre'},
+ {id: 10, value: 'Octubre'},
+ {id: 11, value: 'Noviembre'},
+ {id: 12, value: 'Diciembre'},
+]
+
+
+var toolbar_invoices_filter = [
+ {view: 'richselect', id: 'filter_year', label: 'Año', labelAlign: 'right',
+ labelWidth: 50, width: 150, options: []},
+ {view: 'richselect', id: 'filter_month', label: 'Mes', labelAlign: 'right',
+ labelWidth: 50, width: 200, options: months},
+ {view: 'daterangepicker', id: 'filter_dates', label: 'Fechas',
+ labelAlign: 'right', width: 300},
+]
+
+
function get_icon(tipo){
var node = "
"
return node
@@ -34,7 +63,8 @@ var grid_invoices_cols = [
sort:"int", css: "cell_right"},
{id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data",
sort:"string", hidden:true},
- {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort:"string"},
+ {id: "fecha", header: ["Fecha y Hora"],
+ adjust: "data", sort: "date"},
{id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}],
adjust: 'header', sort: 'string'},
{id: "estatus", header: ["Estatus", {content: "selectFilter"}],
@@ -228,6 +258,7 @@ var body_regimen_fiscal = {
var controls_generate = [
+ {minHeight: 15, maxHeight: 15},
{cols: [ {rows:[
{view: 'fieldset', label: 'Buscar Cliente', body: {rows: [
{cols: [
@@ -272,26 +303,86 @@ var controls_generate = [
{view: 'label', label: 'Detalle', height: 30, align: 'left'},
grid_details,
{minHeight: 15, maxHeight: 15},
- {cols: [{}, grid_totals]}
+ {cols: [{}, grid_totals]},
+ {minHeight: 15, maxHeight: 15},
+ {margin: 20, cols: [{},
+ {view: "button", id: "cmd_timbrar", label: "Timbrar",
+ type: "form", autowidth: true, align:"center"},
+ {view: "button", id: 'cmd_prefactura', label: "PreFactura",
+ type: "form", autowidth: true, align:"center"},
+ {}]
+ }
+]
+
+
+var toolbar_preinvoices = [
+ {view: 'button', id: 'cmd_facturar_preinvoice', label: 'Facturar',
+ type: 'iconButton', autowidth: true, icon: 'pencil'},
+ {},
+ {view: "button", id: "cmd_delete_preinvoice", label: "Eliminar",
+ type: "iconButton", autowidth: true, icon: "minus"},
+]
+
+
+var toolbar_prefilter = [
+ {view: 'richselect', id: 'prefilter_year', label: 'Año', labelAlign: 'right',
+ labelWidth: 50, width: 150, options: []},
+ {view: 'richselect', id: 'prefilter_month', label: 'Mes', labelAlign: 'right',
+ labelWidth: 50, width: 200, options: months},
+]
+
+
+var grid_preinvoices_cols = [
+ {id: "id", header:"ID", hidden:true},
+ {id: "folio", header: ["Folio", {content: "numberFilter"}], adjust: "data",
+ sort:"int", css: "cell_right"},
+ {id: "fecha", header: ["Fecha y Hora"],
+ adjust: "data", sort: "date"},
+ {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}],
+ adjust: 'header', sort: 'string'},
+ {id: 'total_mn', header: ['Total M.N.'], width: 150,
+ sort: 'int', format: webix.i18n.priceFormat, css: 'right'},
+ {id: "cliente", header: ["Razón Social", {content: "selectFilter"}],
+ fillspace:true, sort:"string"},
+ {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')},
+ {id: 'email', header: '', adjust: 'data', template: get_icon('email')}
+]
+
+
+var grid_preinvoices = {
+ view: 'datatable',
+ id: 'grid_preinvoices',
+ select: 'row',
+ adjust: true,
+ footer: true,
+ resizeColumn: true,
+ headermenu: true,
+ columns: grid_preinvoices_cols,
+}
+
+
+var controls_prefactura = [
+ {view: 'toolbar', elements: toolbar_preinvoices},
+ {view: 'toolbar', elements: toolbar_prefilter},
+ grid_preinvoices,
]
var controls_invoices = [
{
- view: "tabview",
- tabbar: {options: ["Generar"]}, animate: true,
+ view: 'tabview',
+ tabbar: {options: ['Generar', 'PreFacturas']}, animate: true,
cells: [
- {id: "Generar", rows: controls_generate},
+ {id: 'Generar', rows: controls_generate},
+ {id: 'PreFacturas', rows: controls_prefactura},
]
},
{rows: [
{template:"", type: "section" },
{margin: 10, cols: [{},
- {view: "button", id: "cmd_timbrar", label: "Timbrar",
- type: "form", autowidth: true, align:"center"},
{view: 'button', id: 'cmd_close_invoice', label: 'Cancelar',
- type: 'danger', autowidth: true, align: 'center'},
- {}]
+ type: 'danger', autowidth: true, align: 'center'}
+ ]
},
]}
]
@@ -316,6 +407,7 @@ var multi_invoices = {
{id: 'invoices_home', rows:[
{view: 'toolbar', elements: toolbar_invoices},
{view: 'toolbar', elements: toolbar_invoices_util},
+ {view: 'toolbar', elements: toolbar_invoices_filter},
grid_invoices,
]},
{id: 'invoices_new', rows:[form_invoice, {}]}
diff --git a/source/static/js/ui/main.js b/source/static/js/ui/main.js
index f594323..9ba1099 100644
--- a/source/static/js/ui/main.js
+++ b/source/static/js/ui/main.js
@@ -62,7 +62,8 @@ var ui_main = {
{view: 'label', label: 'Empresa Libre'},
{},
menu_user,
- {view: 'button', type: 'icon', width: 45, css: 'app_button', icon: 'bell-o', badge: 1}
+ {view: 'button', type: 'icon', width: 45, css: 'app_button',
+ icon: 'bell-o', badge: 0}
]
},
{
diff --git a/source/templates/cancel_template.xml b/source/templates/cancel_template.xml
new file mode 100644
index 0000000..6b58286
--- /dev/null
+++ b/source/templates/cancel_template.xml
@@ -0,0 +1,28 @@
+
+
+
+ {uuid}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+