diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py
index e3c3924..2afb7a0 100644
--- a/source/app/controllers/main.py
+++ b/source/app/controllers/main.py
@@ -140,6 +140,7 @@ class AppPartners(object):
def on_get(self, req, resp):
values = req.params
+ #~ print ('VALUES', values)
req.context['result'] = self._db.get_partners(values)
resp.status = falcon.HTTP_200
@@ -257,7 +258,31 @@ class AppCuentasBanco(object):
def on_get(self, req, resp):
values = req.params
session = req.env['beaker.session']
- #~ req.context['result'] = self._db.get_emisor(session['rfc'])
+ req.context['result'] = self._db.get_cuentasbanco(values)
+ resp.status = falcon.HTTP_200
+
+ def on_post(self, req, resp):
+ values = req.params
+ req.context['result'] = self._db.cuentasbanco(values)
+ resp.status = falcon.HTTP_200
+
+ def on_delete(self, req, resp):
+ values = req.params
+ if self._db.delete('cuentasbanco', values['id']):
+ resp.status = falcon.HTTP_200
+ else:
+ resp.status = falcon.HTTP_204
+
+
+class AppMovimientosBanco(object):
+
+ def __init__(self, db):
+ self._db = db
+
+ def on_get(self, req, resp):
+ values = req.params
+ session = req.env['beaker.session']
+ req.context['result'] = self._db.get_movimientosbanco(values)
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index f1fb86d..1a1e82d 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -36,7 +36,7 @@ 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_XMLSEC, TEMPLATE_CANCEL, IMPUESTOS
+ PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO
#~ def _get_hash(password):
@@ -767,9 +767,8 @@ class LIBO(object):
#~ Si no se encuentra, copia las celdas hacia abajo de
#~ {subtotal.titulo} y {subtotal}
- print (data['descuento'])
+ #~ print (data['descuento'])
if 'descuento' in data:
-
self._copy_cell(cell_title)
self._copy_cell(cell_value)
cell_title = self._set_cell(v='Descuento', cell=cell_title)
@@ -1277,10 +1276,10 @@ class ImportFacturaLibre(object):
self._rfc = rfc
self._con = None
self._cursor = None
+ self._error = ''
self._is_connect = self._connect(path)
self._clientes = []
self._clientes_rfc = []
- self._error = ''
@property
def error(self):
@@ -1299,7 +1298,7 @@ class ImportFacturaLibre(object):
return False
if obj['rfc'] != self._rfc:
- self._error = 'Los datos no corresponden al emisor: {}'.format(self._rfc)
+ self._error = 'Los datos no corresponden al RFC: {}'.format(self._rfc)
return False
return True
@@ -1328,6 +1327,7 @@ class ImportFacturaLibre(object):
tables = (
('receptores', 'Socios'),
('cfdfacturas', 'Facturas'),
+ ('categorias', 'Categorias'),
)
for source, target in tables:
data[target] = self._get_table(source)
@@ -1339,6 +1339,63 @@ class ImportFacturaLibre(object):
def _get_table(self, table):
return getattr(self, '_{}'.format(table))()
+ def import_productos(self):
+ sql = "SELECT * FROM productos"
+ self._cursor.execute(sql)
+ rows = self._cursor.fetchall()
+
+ fields = (
+ ('id_categoria', 'categoria'),
+ ('noIdentificacion', 'clave'),
+ ('descripcion', 'descripcion'),
+ ('unidad', 'unidad'),
+ ('valorUnitario', 'valor_unitario'),
+ ('existencia', 'existencia'),
+ ('inventario', 'inventario'),
+ ('codigobarras', 'codigo_barras'),
+ ('CuentaPredial', 'cuenta_predial'),
+ ('precio_compra', 'ultimo_precio'),
+ ('minimo', 'minimo'),
+ )
+ data = []
+
+ sql = """
+ SELECT nombre, tasa, tipo
+ FROM impuestos, productos, productosimpuestos
+ WHERE productos.id=productosimpuestos.id_producto
+ AND productosimpuestos.id_impuesto=impuestos.id
+ AND productos.id = ?
+ """
+ for row in rows:
+ new = {t: row[s] for s, t in fields}
+ new['descripcion'] = ' '.join(new['descripcion'].split())
+ new['clave_sat'] = DEFAULT_SAT_PRODUCTO
+ self._cursor.execute(sql, (row['id'],))
+ impuestos = self._cursor.fetchall()
+ new['impuestos'] = tuple(impuestos)
+ data.append(new)
+
+ return data
+
+ def _categorias(self):
+ sql = "SELECT * FROM categorias"
+ self._cursor.execute(sql)
+ rows = self._cursor.fetchall()
+
+ fields = (
+ ('categoria', 'categoria'),
+ ('id_padre', 'padre'),
+ )
+ data = []
+
+ for row in rows:
+ new = {t: row[s] for s, t in fields}
+ if new['padre'] == 0:
+ new['padre'] = None
+ data.append(new)
+
+ return data
+
def _get_cliente(self, invoice):
sql = "SELECT rfc, nombre FROM receptores WHERE id=?"
self._cursor.execute(sql, [invoice['id_cliente']])
@@ -1476,6 +1533,10 @@ class ImportFacturaLibre(object):
data = []
for row in rows:
new = {t: row[s] for s, t in fields}
+ if not new['uuid']:
+ new['uuid'] = None
+ if new['xml'] is None:
+ new['xml'] = ''
if row['estatus'] == 'Pagada':
new['pagada'] = True
elif row['estatus'] == 'Cancelada':
diff --git a/source/app/main.py b/source/app/main.py
index ed80b25..2db8add 100644
--- a/source/app/main.py
+++ b/source/app/main.py
@@ -15,7 +15,8 @@ from models.db import StorageEngine
from controllers.main import (
AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig,
AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios,
- AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco
+ AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco,
+ AppMovimientosBanco
)
from settings import DEBUG
@@ -46,6 +47,7 @@ api.add_route('/products', AppProducts(db))
api.add_route('/invoices', AppInvoices(db))
api.add_route('/preinvoices', AppPreInvoices(db))
api.add_route('/cuentasbanco', AppCuentasBanco(db))
+api.add_route('/movbanco', AppMovimientosBanco(db))
if DEBUG:
diff --git a/source/app/models/db.py b/source/app/models/db.py
index c0b498b..c41252e 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -58,6 +58,9 @@ class StorageEngine(object):
years2 = main.PreFacturas.filter_years()
return [years1, years2]
+ def _get_cuentayears(self, values):
+ return main.CuentasBanco.get_years()
+
def _get_cert(self, values):
return main.Certificado.get_data()
@@ -145,6 +148,9 @@ class StorageEngine(object):
def _get_usocfdi(self, values):
return main.SATUsoCfdi.get_activos()
+ def _get_ebancomov(self, values):
+ return main.MovimientosBanco.con(values['id'])
+
def delete(self, table, id):
if table == 'partner':
return main.Socios.remove(id)
@@ -158,6 +164,8 @@ class StorageEngine(object):
return main.PreFacturas.remove(id)
if table == 'satimpuesto':
return main.SATImpuestos.remove(id)
+ if table == 'cuentasbanco':
+ return main.CuentasBanco.remove(id)
return False
def _get_client(self, values):
@@ -217,6 +225,9 @@ class StorageEngine(object):
def cuentasbanco(self, values):
return main.CuentasBanco.add(values)
+ def get_cuentasbanco(self, values):
+ return main.CuentasBanco.get_(values)
+
def get_folios(self):
return main.Folios.get_()
@@ -239,3 +250,6 @@ class StorageEngine(object):
return data, file_name, content_type
+ def get_movimientosbanco(self, values):
+ return main.MovimientosBanco.get_(values)
+
diff --git a/source/app/models/main.py b/source/app/models/main.py
index b304403..4c3c0f2 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python
+from decimal import Decimal
import sqlite3
import click
from peewee import *
@@ -15,7 +16,7 @@ if __name__ == '__main__':
from controllers import util
from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
- INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS
+ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO
FORMAT = '{0:.2f}'
@@ -529,6 +530,10 @@ class Categorias(BaseModel):
(('categoria', 'padre'), True),
)
+ @classmethod
+ def exists(cls, filters):
+ return Categorias.select().where(filters).exists()
+
@classmethod
def get_all(cls):
rows = (Categorias.select(
@@ -536,6 +541,9 @@ class Categorias(BaseModel):
Categorias.categoria.alias('value'),
Categorias.padre.alias('parent_id'))
).dicts()
+ for row in rows:
+ if row['parent_id'] is None:
+ row['parent_id'] = 0
return tuple(rows)
@@ -574,6 +582,9 @@ class SATUnidades(BaseModel):
(('key', 'name'), True),
)
+ def __str__(self):
+ return '{} ({})'.format(self.name, self.key)
+
@classmethod
def get_(self):
rows = SATUnidades.select().dicts()
@@ -635,6 +646,10 @@ class SATFormaPago(BaseModel):
def __str__(self):
return 'Forma de pago: ({}) {}'.format(self.key, self.name)
+ @classmethod
+ def get_by_key(cls, key):
+ return SATFormaPago.get(SATFormaPago.key==key)
+
@classmethod
def get_activos(cls, values):
field = SATFormaPago.id
@@ -740,12 +755,17 @@ class SATImpuestos(BaseModel):
(('key', 'factor', 'tipo', 'tasa'), True),
)
+ @classmethod
+ def get_o_crea(self, values):
+ obj, _ = SATImpuestos.get_or_create(**values)
+ return obj
+
@classmethod
def add(self, values):
tasa = float(values['tasa'])
tipo = 'T'
if tasa < 0:
- tipo: 'R'
+ tipo = 'R'
row = {
'key': IMPUESTOS.get(values['impuesto']),
@@ -753,6 +773,7 @@ class SATImpuestos(BaseModel):
'tipo': tipo,
'tasa': abs(tasa),
}
+
try:
obj = SATImpuestos.create(**row)
row['id'] = obj.id
@@ -764,8 +785,11 @@ class SATImpuestos(BaseModel):
@classmethod
def remove(cls, id):
with database_proxy.transaction():
- q = SATImpuestos.delete().where(SATImpuestos.id==id)
- return bool(q.execute())
+ try:
+ q = SATImpuestos.delete().where(SATImpuestos.id==id)
+ return bool(q.execute())
+ except IntegrityError:
+ return False
@classmethod
def get_(self):
@@ -774,7 +798,9 @@ class SATImpuestos(BaseModel):
SQL(" '-' AS delete"),
SATImpuestos.name,
SATImpuestos.tipo,
- SATImpuestos.tasa)
+ SATImpuestos.tasa,
+ SATImpuestos.activo,
+ SATImpuestos.default)
.dicts()
)
return tuple(rows)
@@ -902,6 +928,58 @@ class CuentasBanco(BaseModel):
def __str__(self):
return '{} ({})'.format(self.banco.name, self.cuenta[-4:])
+ @classmethod
+ def remove(cls, id):
+ try:
+ with database_proxy.atomic() as txn:
+ q = MovimientosBanco.delete().where(MovimientosBanco.cuenta==id)
+ q.execute()
+ q = CuentasBanco.delete().where(CuentasBanco.id==id)
+ q.execute()
+ return True
+ except:
+ return False
+
+ @classmethod
+ def get_years(cls):
+ data = [{'id': -1, 'value': 'Todos'}]
+ year1 = (CuentasBanco
+ .select(fn.Min(CuentasBanco.fecha_apertura.year))
+ .where(CuentasBanco.de_emisor==True, CuentasBanco.activa==True)
+ .group_by(CuentasBanco.fecha_apertura.year)
+ .order_by(CuentasBanco.fecha_apertura.year)
+ .scalar()
+ )
+
+ if year1:
+ year2 = util.now().year + 1
+ data += [{'id': y, 'value': y} for y in range(int(year1), year2)]
+
+ return data
+
+ @classmethod
+ def get_(cls, values):
+ if values['tipo'] == '1':
+ rows = (CuentasBanco
+ .select()
+ .where(CuentasBanco.de_emisor==True, CuentasBanco.activa==True)
+ )
+ if not (len(rows)):
+ return {'ok': False}
+
+ first = rows[0]
+ rows = [{'id': r.id, 'value': '{} ({})'.format(
+ r.banco.name, r.cuenta[-4:])} for r in rows]
+ data = {
+ 'ok': True,
+ 'rows': tuple(rows),
+ 'moneda': first.moneda.name,
+ 'saldo': first.saldo,
+ }
+ return data
+
+ return
+
@classmethod
def emisor(cls):
rows = (CuentasBanco
@@ -926,15 +1004,32 @@ class CuentasBanco(BaseModel):
def add(cls, values):
w = '37137137137137137'
dv = str(
- 10 -
+ (10 -
sum([(int(v) * int(values['clabe'][i])) % 10 for i, v in enumerate(w)])
- % 10)
+ % 10) % 10)
if dv != values['clabe'][-1]:
msg = 'Digito de control de la CLABE es incorrecto'
return {'ok': False, 'msg': msg}
+ fecha_deposito = values.pop('fecha_deposito', None)
+
with database_proxy.transaction():
- obj = CuentasBanco.create(**values)
+ try:
+ obj = CuentasBanco.create(**values)
+ except IntegrityError:
+ msg = 'Esta cuenta ya existe'
+ return {'ok': False, 'msg': msg}
+
+ nuevo_mov= {
+ 'cuenta': obj.id,
+ 'fecha': fecha_deposito,
+ 'movimiento': 1,
+ 'descripcion': 'Saldo inicial',
+ 'forma_pago': SATFormaPago.get_by_key('99'),
+ 'deposito': values['saldo'],
+ 'saldo': values['saldo'],
+ }
+ MovimientosBanco.add(nuevo_mov)
rows = (CuentasBanco
.select(
@@ -956,6 +1051,113 @@ class CuentasBanco(BaseModel):
return data
+class MovimientosBanco(BaseModel):
+ cuenta = ForeignKeyField(CuentasBanco)
+ fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
+ movimiento = IntegerField(default=0)
+ descripcion = TextField(default='')
+ forma_pago = ForeignKeyField(SATFormaPago)
+ retiro = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ deposito = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6,
+ auto_round=True)
+ moneda = TextField(default='MXN') # Complemento de pagos
+ tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6,
+ auto_round=True)
+ numero_operacion = TextField(default='')
+ origen_rfc = TextField(default='')
+ origen_nombre = TextField(default='')
+ origen_cuenta = TextField(default='')
+ destino_rfc = TextField(default='')
+ destino_cuenta = TextField(default='')
+ tipo_cadena_pago = TextField(default='')
+ certificado_pago = TextField(default='')
+ cadena_pago = TextField(default='')
+ sello_pago = TextField(default='')
+
+ class Meta:
+ order_by = ('fecha',)
+ indexes = (
+ (('cuenta', 'movimiento'), True),
+ )
+
+ @classmethod
+ def add(cls, values):
+ with database_proxy.transaction():
+ try:
+ obj = MovimientosBanco.create(**values)
+ except IntegrityError:
+ msg = 'Este movimiento ya existe'
+ return {'ok': False, 'msg': msg}
+
+ return {'ok': True}
+
+ @classmethod
+ def con(cls, id):
+ cant = (MovimientosBanco
+ .select(MovimientosBanco.id)
+ .where(MovimientosBanco.cuenta==id)
+ .count()
+ )
+ if cant > 2:
+ return {'ok': True}
+
+ return {'ok': False}
+
+
+ @classmethod
+ def get_(cls, values):
+ cuenta = int(values['cuenta'])
+ if 'fechas' in values:
+ rango = values['fechas']
+ fd = (MovimientosBanco.fecha.between(
+ util.get_date(rango['start']),
+ util.get_date(rango['end'], True)))
+ filtros = (fd & (MovimientosBanco.cuenta==cuenta))
+ else:
+ year = int(values['year'])
+ mes = int(values['mes'])
+ if year == -1:
+ fy = (MovimientosBanco.fecha.year > 0)
+ else:
+ fy = (MovimientosBanco.fecha.year == year)
+ if mes == -1:
+ fm = (MovimientosBanco.fecha.month > 0)
+ else:
+ fm = (MovimientosBanco.fecha.month == mes)
+ filtros = (fy & fm & (MovimientosBanco.cuenta==cuenta))
+
+ rows = tuple(MovimientosBanco
+ .select(
+ MovimientosBanco.id,
+ MovimientosBanco.fecha,
+ MovimientosBanco.numero_operacion,
+ MovimientosBanco.descripcion,
+ MovimientosBanco.retiro,
+ MovimientosBanco.deposito,
+ MovimientosBanco.saldo)
+ .where(filtros)
+ .dicts()
+ )
+
+ return {'ok': True, 'rows': rows}
+
+
+class CfdiPagos(BaseModel):
+ movimiento = ForeignKeyField(MovimientosBanco)
+ xml = TextField(default='')
+ uuid = UUIDField(null=True)
+ estatus = TextField(default='Guardado')
+ estatus_sat = TextField(default='')
+ notas = TextField(default='')
+ cancelado = BooleanField(default=False)
+
+ class Meta:
+ order_by = ('movimiento',)
+
+
class SATUsoCfdi(BaseModel):
key = TextField(index=True, unique=True)
name = TextField(default='', index=True)
@@ -1076,7 +1278,9 @@ class Socios(BaseModel):
@classmethod
def get_(cls, values):
- if values:
+ print ('values', values)
+ id = values.get('id', 0)
+ if id:
id = int(values['id'])
row = Socios.select().where(Socios.id==id).dicts()[0]
row['uso_cfdi_socio'] = row.pop('uso_cfdi')
@@ -1085,6 +1289,18 @@ class Socios(BaseModel):
str(CondicionesPago.get(id=row['condicion_pago']))
return row
+ #~ return {'data': data['rows'][:100], 'pos':0, 'total_count': 1300}
+ #~ start = 0
+ #~ count = 0
+ #~ end = 100
+ #~ if values:
+ #~ {'start': '100', 'count': '100', 'continue': 'true'}
+ #~ start = int(values['start'])
+ #~ cont = int(values['count'])
+ #~ end = start + count
+
+ total = Socios.select().count()
+
rows = (Socios
.select(
Socios.id,
@@ -1093,7 +1309,7 @@ class Socios(BaseModel):
Socios.saldo_cliente)
.dicts()
)
- return {'ok': True, 'rows': tuple(rows)}
+ return {'pos': 0, 'total_count': total, 'data': tuple(rows)}
@classmethod
def get_by_client(cls, values):
@@ -1834,8 +2050,8 @@ class Facturas(BaseModel):
invoice_tax = {
'factura': invoice.id,
- 'impuesto': tax['id'],
- 'base': tax['importe'],
+ 'impuesto': tax.id,
+ 'base': tax.importe,
'importe': import_tax,
}
FacturasImpuestos.create(**invoice_tax)
@@ -2497,6 +2713,17 @@ class FacturasRelacionadas(BaseModel):
return [str(r.factura_origen.uuid) for r in query]
+class CfdiPagosFacturas(BaseModel):
+ pago = ForeignKeyField(CfdiPagos)
+ factura = ForeignKeyField(Facturas)
+
+ class Meta:
+ order_by = ('pago',)
+ indexes = (
+ (('pago', 'factura'), True),
+ )
+
+
class PreFacturasRelacionadas(BaseModel):
factura = ForeignKeyField(PreFacturas, related_name='original')
factura_origen = ForeignKeyField(PreFacturas, related_name='relacion')
@@ -2639,6 +2866,23 @@ class FacturasImpuestos(BaseModel):
)
+class FacturasPagos(BaseModel):
+ factura = ForeignKeyField(Facturas)
+ numero = IntegerField(default=1)
+ saldo_anterior = 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)
+ saldo = DecimalField(default=0.0, max_digits=18, decimal_places=6,
+ auto_round=True)
+
+ class Meta:
+ order_by = ('factura',)
+ indexes = (
+ (('factura', 'numero'), True),
+ )
+
+
class PreFacturasImpuestos(BaseModel):
factura = ForeignKeyField(PreFacturas)
impuesto = ForeignKeyField(SATImpuestos)
@@ -2789,13 +3033,15 @@ def _init_values(rfc):
def _crear_tablas(rfc):
tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion,
- Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios,
+ Folios,
+ Emisor, Facturas, FacturasDetalle, FacturasImpuestos, FacturasPagos,
FacturasRelacionadas, Productos,
PreFacturas, PreFacturasDetalle, PreFacturasImpuestos,
PreFacturasRelacionadas,
SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes,
SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos,
- Socios, Tags, Usuarios, CuentasBanco, TipoCambio,
+ Socios, Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco,
+ CfdiPagos, CfdiPagosFacturas,
Emisor.regimenes.get_through_model(),
Socios.tags.get_through_model(),
Productos.impuestos.get_through_model(),
@@ -3039,12 +3285,22 @@ def _importar_socios(rows):
with database_proxy.atomic() as txn:
Socios.create(**row)
except IntegrityError:
- msg = '\tSocio: id: {}'.format(row['nombre'])
- log.error(msg)
+ msg = '\tSocio existente: {}'.format(row['nombre'])
+ log.info(msg)
log.info('\tClientes importados...')
return
+def _existe_factura(row):
+ filtro = (Facturas.uuid==row['uuid'])
+ if row['uuid'] is None:
+ filtro = (
+ (Facturas.serie==row['serie']) &
+ (Facturas.folio==row['folio'])
+ )
+ return Facturas.select().where(filtro).exists()
+
+
def _importar_facturas(rows):
log.info('\tImportando Facturas...')
for row in rows:
@@ -3054,6 +3310,11 @@ def _importar_facturas(rows):
cliente = row.pop('cliente')
row['cliente'] = Socios.get(**cliente)
with database_proxy.atomic() as txn:
+ if _existe_factura(row):
+ msg = '\tFactura existente: {}{}'.format(
+ row['serie'], row['folio'])
+ log.info(msg)
+ continue
obj = Facturas.create(**row)
for detalle in detalles:
detalle['factura'] = obj
@@ -3066,13 +3327,129 @@ def _importar_facturas(rows):
'importe': impuesto['importe'],
}
FacturasImpuestos.create(**new)
- except IntegrityError:
- msg = '\tFactura: id: {}'.format(row['serie'] + row['folio'])
+ except IntegrityError as e:
+ #~ print (e)
+ msg = '\tFactura: id: {}'.format(row['serie'] + str(row['folio']))
log.error(msg)
log.info('\tFacturas importadas...')
return
+def _importar_categorias(rows):
+ log.info('\tImportando Categorías...')
+ for row in rows:
+ if row['padre'] is None:
+ filters = (
+ (Categorias.categoria==row['categoria']) &
+ (Categorias.padre.is_null(True))
+ )
+ else:
+ filters = (
+ (Categorias.categoria==row['categoria']) &
+ (Categorias.padre==row['padre'])
+ )
+
+ if Categorias.exists(filters):
+ continue
+
+ try:
+ Categorias.create(**row)
+ except IntegrityError:
+ msg = '\tCategoria: ({}) {}'.format(row['padre'], row['categoria'])
+ log.error(msg)
+
+ log.info('\tCategorías importadas...')
+ return
+
+
+def _get_id_unidad(unidad):
+ try:
+ if 'pieza' in unidad.lower():
+ unidad = 'pieza'
+ obj = SATUnidades.get(SATUnidades.name.contains(unidad))
+ except SATUnidades.DoesNotExist:
+ msg = '\tNo se encontró la unidad: {}'.format(unidad)
+ log.error(msg)
+ return unidad
+
+ return str(obj.id)
+
+
+def _get_impuestos(impuestos):
+ lines = '|'
+ for impuesto in impuestos:
+ if impuesto['tasa'] == '-2/3':
+ tasa = str(round(2/3, 6))
+ else:
+ tasa = str(round(float(impuesto['tasa']) / 100.0, 6))
+
+ info = (
+ IMPUESTOS.get(impuesto['nombre']),
+ impuesto['nombre'],
+ impuesto['tipo'][0],
+ tasa,
+ )
+ lines += '|'.join(info)
+ return lines
+
+
+def _generar_archivo_productos(archivo):
+ rfc = input('Introduce el RFC: ').strip().upper()
+ if not rfc:
+ msg = 'El RFC es requerido'
+ log.error(msg)
+ return
+
+ args = util.get_con(rfc)
+ if not args:
+ return
+
+ conectar(args)
+
+ log.info('Importando datos...')
+ app = util.ImportFacturaLibre(archivo, rfc)
+ if not app.is_connect:
+ log.error('\t{}'.format(app._error))
+ return
+
+ rows = app.import_productos()
+
+ p, _, _, _ = util.get_path_info(archivo)
+ path_txt = util._join(p, 'productos.txt')
+ log.info('\tGenerando archivo: {}'.format(path_txt))
+
+ fields = (
+ 'clave',
+ 'clave_sat',
+ 'unidad',
+ 'categoria',
+ 'descripcion',
+ 'valor_unitario',
+ 'existencia',
+ 'inventario',
+ 'codigo_barras',
+ 'cuenta_predial',
+ 'ultimo_precio',
+ 'minimo',
+ )
+
+ data = ['|'.join(fields)]
+ for row in rows:
+ impuestos = row.pop('impuestos', ())
+ line = [str(row[r]) for r in fields]
+ if line[10] == 'None':
+ line[10] = '0.0'
+ line[2] = _get_id_unidad(line[2])
+ line = '|'.join(line) + _get_impuestos(impuestos)
+ data.append(line)
+
+ with open(path_txt, 'w') as fh:
+ fh.write('\n'.join(data))
+
+ log.info('\tArchivo generado: {}'.format(path_txt))
+ return
+
+
def _importar_factura_libre(archivo):
rfc = input('Introduce el RFC: ').strip().upper()
if not rfc:
@@ -3089,18 +3466,115 @@ def _importar_factura_libre(archivo):
log.info('Importando datos...')
app = util.ImportFacturaLibre(archivo, rfc)
if not app.is_connect:
- log.error('\t{}'.format(app.error))
+ log.error('\t{}'.format(app._error))
return
data = app.import_data()
_importar_socios(data['Socios'])
_importar_facturas(data['Facturas'])
+ _importar_categorias(data['Categorias'])
log.info('Importación terminada...')
return
+def _importar_productos(archivo):
+ rfc = input('Introduce el RFC: ').strip().upper()
+ if not rfc:
+ msg = 'El RFC es requerido'
+ log.error(msg)
+ return
+
+ args = util.get_con(rfc)
+ if not args:
+ return
+
+ conectar(args)
+ log.info('Importando productos...')
+
+ fields = (
+ 'clave',
+ 'clave_sat',
+ 'unidad',
+ 'categoria',
+ 'descripcion',
+ 'valor_unitario',
+ 'existencia',
+ 'inventario',
+ 'codigo_barras',
+ 'cuenta_predial',
+ 'ultimo_precio',
+ 'minimo',
+ )
+
+ rows = util.read_file(archivo, 'r').split('\n')
+ for i, row in enumerate(rows):
+ if i == 0:
+ continue
+ data = row.split('|')
+
+ new = {}
+ for i, f in enumerate(fields):
+ if not len(data[0]):
+ continue
+
+ if i in (2, 3):
+ try:
+ new[f] = int(data[i])
+ except ValueError:
+ continue
+ elif i in (5, 6, 10, 11):
+ new[f] = float(data[i])
+ elif i == 7:
+ new[f] = bool(data[i])
+ else:
+ new[f] = data[i]
+
+ impuestos = data[i + 1:]
+ if not impuestos:
+ taxes = [SATImpuestos.select().where(SATImpuestos.id==6)]
+ else:
+ taxes = []
+ for i in range(0, len(impuestos), 4):
+ w = {
+ 'key': impuestos[i],
+ 'name': impuestos[i+1],
+ 'tipo': impuestos[i+2],
+ 'tasa': float(impuestos[i+3]),
+ }
+ taxes.append(SATImpuestos.get_o_crea(w))
+
+ with database_proxy.transaction():
+ try:
+ obj = Productos.create(**new)
+ obj.impuestos = taxes
+ except IntegrityError:
+ pass
+
+ log.info('Importación terminada...')
+ return
+
+
+def _test():
+ rfc = input('Introduce el RFC: ').strip().upper()
+ if not rfc:
+ msg = 'El RFC es requerido'
+ log.error(msg)
+ return
+
+ args = util.get_con(rfc)
+ if not args:
+ return
+
+ conectar(args)
+
+ rows = Categorias.select().where(
+ Categorias.categoria=='Productos', Categorias.padre.is_null(True)).exists()
+ print (rows)
+ return
+
+
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
help_create_tables = 'Crea las tablas en la base de datos'
help_migrate_db = 'Migra las tablas en la base de datos'
@@ -3125,10 +3599,19 @@ help_lr = 'Listar RFCs'
@click.option('-i', '--importar-valores', is_flag=True, default=False)
@click.option('-a', '--archivo')
@click.option('-fl', '--factura-libre', is_flag=True, default=False)
+@click.option('-t', '--test', is_flag=True, default=False)
+@click.option('-gap', '--generar-archivo-productos', is_flag=True, default=False)
+@click.option('-ip', '--importar-productos', is_flag=True, default=False)
def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc,
- borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre):
+ borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre, test,
+ generar_archivo_productos, importar_productos):
+
opt = locals()
+ if opt['test']:
+ _test()
+ sys.exit(0)
+
if opt['iniciar_bd']:
_iniciar_bd()
sys.exit(0)
@@ -3183,6 +3666,36 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc,
_importar_factura_libre(opt['archivo'])
sys.exit(0)
+ if opt['generar_archivo_productos']:
+ if not opt['archivo']:
+ msg = 'Falta la ruta de la base de datos'
+ raise click.ClickException(msg)
+ if not util.is_file(opt['archivo']):
+ msg = 'No es un archivo'
+ raise click.ClickException(msg)
+ _, _, _, ext = util.get_path_info(opt['archivo'])
+ if ext != '.sqlite':
+ msg = 'No es una base de datos'
+ raise click.ClickException(msg)
+
+ _generar_archivo_productos(opt['archivo'])
+ sys.exit(0)
+
+ if opt['importar_productos']:
+ if not opt['archivo']:
+ msg = 'Falta la ruta del archivo'
+ raise click.ClickException(msg)
+ if not util.is_file(opt['archivo']):
+ msg = 'No es un archivo'
+ raise click.ClickException(msg)
+ _, _, _, ext = util.get_path_info(opt['archivo'])
+ if ext != '.txt':
+ msg = 'No es un archivo de texto'
+ raise click.ClickException(msg)
+
+ _importar_productos(opt['archivo'])
+ sys.exit(0)
+
return
diff --git a/source/app/settings.py b/source/app/settings.py
index c670fb1..2a4e9df 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -102,3 +102,4 @@ IMPUESTOS = {
'ICIC': '000',
'CEDULAR': '000',
}
+DEFAULT_SAT_PRODUCTO = '01010101'
diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js
index dfabde5..17377c0 100644
--- a/source/static/js/controller/admin.js
+++ b/source/static/js/controller/admin.js
@@ -22,6 +22,7 @@ var controllers = {
$$('cmd_guardar_correo').attachEvent('onItemClick', cmd_guardar_correo_click)
$$('emisor_logo').attachEvent('onItemClick', emisor_logo_click)
$$('cmd_emisor_agregar_cuenta').attachEvent('onItemClick', cmd_emisor_agregar_cuenta_click)
+ $$('cmd_emisor_eliminar_cuenta').attachEvent('onItemClick', cmd_emisor_eliminar_cuenta_click)
$$('emisor_cuenta_saldo_inicial').attachEvent('onChange', emisor_cuenta_saldo_inicial_change)
//~ SAT
tb_sat = $$('tab_sat').getTabbar()
@@ -184,7 +185,7 @@ function get_certificado(){
}
-function get_cuentas_banco(){
+function get_admin_cuentas_banco(){
webix.ajax().get('/values/monedasid', function(text, data){
var values = data.json()
@@ -310,7 +311,7 @@ function multi_admin_change(prevID, nextID){
$$('tab_emisor').setValue('Datos Fiscales')
get_emisor()
get_certificado()
- get_cuentas_banco()
+ get_admin_cuentas_banco()
return
}
@@ -906,17 +907,18 @@ function cmd_emisor_agregar_cuenta_click(){
var values = form.getValues()
- var si = parseFloat(values.emisor_cuenta_saldo_inicial.replace('$', '').replace(',', ''))
+ var saldo_inicial = parseFloat(values.emisor_cuenta_saldo_inicial.replace('$', '').replace(',', ''))
var cuenta = {
de_emisor: true,
activa: true,
nombre: values.emisor_cuenta_nombre.trim(),
banco: values.emisor_banco,
fecha_apertura: values.emisor_cuenta_fecha,
+ fecha_deposito: values.emisor_fecha_saldo,
cuenta: values.emisor_cuenta.trim(),
clabe: values.emisor_clabe.trim(),
moneda: values.emisor_cuenta_moneda,
- saldo_inicial: si
+ saldo_inicial: saldo_inicial
}
if(!cuenta.nombre){
@@ -979,6 +981,7 @@ function cmd_emisor_agregar_cuenta_click(){
var values = data.json()
if(values.ok){
$$('grid_emisor_cuentas_banco').add(values.row)
+ form.setValues({})
}else{
msg_error(values.msg)
}
@@ -1096,10 +1099,13 @@ function borrar_impuesto(row){
var grid = $$('grid_admin_taxes')
webix.ajax().del('/values/satimpuesto', {id: row}, function(text, xml, xhr){
- var msg = 'Impuesto eliminado correctamente'
+ msg = 'Impuesto eliminado correctamente'
if(xhr.status == 200){
grid.remove(row)
msg_sucess(msg)
+ }else{
+ msg = 'Impuesto en uso, no se pudo eliminar.'
+ msg_sucess(msg)
}
})
}
@@ -1125,3 +1131,57 @@ function grid_admin_taxes_click(id, e, node){
})
}
+
+
+function eliminar_cuenta_banco(id){
+ var grid = $$('grid_emisor_cuentas_banco')
+
+ webix.ajax().del('/cuentasbanco', {id: id}, function(text, xml, xhr){
+ msg = 'Cuenta eliminada correctamente'
+ if(xhr.status == 200){
+ grid.remove(id)
+ msg_sucess(msg)
+ }else{
+ msg = 'No se pudo eliminar'
+ msg_error(msg)
+ }
+ })
+}
+
+
+function cmd_emisor_eliminar_cuenta_click(){
+ var respuesta = undefined
+ var row = $$('grid_emisor_cuentas_banco').getSelectedItem()
+
+ if (row == undefined){
+ msg = 'Selecciona una cuenta de banco'
+ msg_error(msg)
+ return
+ }
+
+ webix.ajax().sync().get('/values/ebancomov', {id: row['id']}, function(text, data){
+ respuesta = data.json()
+ })
+
+ if(respuesta.ok){
+ msg = 'La cuenta tiene movimientos, no se puede eliminar'
+ msg_error(msg)
+ return
+ }
+
+ var msg = '¿Estás seguro de eliminar la cuenta de banco?
'
+ msg += row['banco'] + ' (' + row['cuenta'] + ')'
+ msg += '
ESTA ACCIÓN NO SE PUEDE DESHACER'
+ webix.confirm({
+ title: 'Eliminar Cuenta de Banco',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if (result){
+ eliminar_cuenta_banco(row['id'])
+ }
+ }
+ })
+}
diff --git a/source/static/js/controller/bancos.js b/source/static/js/controller/bancos.js
new file mode 100644
index 0000000..5647674
--- /dev/null
+++ b/source/static/js/controller/bancos.js
@@ -0,0 +1,106 @@
+var msg = ''
+
+var bancos_controllers = {
+ init: function(){
+ $$('lst_cuentas_banco').attachEvent('onChange', lst_cuentas_banco_change)
+ $$('cmd_agregar_retiro').attachEvent('onItemClick', cmd_agregar_retiro_click)
+ $$('cmd_agregar_deposito').attachEvent('onItemClick', cmd_agregar_deposito_click)
+ set_year_month()
+ }
+}
+
+
+function set_year_month(){
+ var d = new Date()
+ var y = $$('filtro_cuenta_year')
+ var m = $$('filtro_cuenta_mes')
+
+ webix.ajax().get('/values/cuentayears', {
+ error:function(text, data, XmlHttpRequest){
+ msg = 'Ocurrio un error, consulta a soporte técnico'
+ msg_error(msg)
+ },
+ success:function(text, data, XmlHttpRequest){
+ var values = data.json()
+ y.getList().parse(values)
+ y.blockEvent()
+ m.blockEvent()
+ y.setValue(d.getFullYear())
+ m.setValue(d.getMonth() + 1)
+ y.unblockEvent()
+ m.unblockEvent()
+ }
+ })
+
+}
+
+
+function get_cuentas_banco(){
+ var list = $$('lst_cuentas_banco')
+
+ webix.ajax().get('/cuentasbanco', {'tipo': 1}, {
+ error:function(text, data, XmlHttpRequest){
+ msg = 'Ocurrio un error, consulta a soporte técnico'
+ msg_error(msg)
+ },
+ success:function(text, data, XmlHttpRequest){
+ var values = data.json()
+ if(values.ok){
+ list.getList().parse(values.rows)
+ list.blockEvent()
+ list.setValue(values.rows[0].id)
+ list.unblockEvent()
+ $$('txt_cuenta_moneda').setValue(values.moneda)
+ $$('txt_cuenta_saldo').setValue(values.saldo)
+ get_estado_cuenta()
+ }
+ }
+ })
+}
+
+
+function get_estado_cuenta(rango){
+ if(rango == undefined){
+ var filtro = {
+ cuenta: $$('lst_cuentas_banco').getValue(),
+ year: $$('filtro_cuenta_year').getValue(),
+ mes: $$('filtro_cuenta_mes').getValue(),
+ }
+ }else{
+ var filtro = {
+ cuenta: $$('lst_cuentas_banco').getValue(),
+ fechas: rango,
+ }
+ }
+
+ var grid = $$('grid_cuentabanco')
+
+ webix.ajax().get('/movbanco', filtro, {
+ error:function(text, data, XmlHttpRequest){
+ msg = 'Ocurrio un error, consulta a soporte técnico'
+ msg_error(msg)
+ },
+ success:function(text, data, XmlHttpRequest){
+ var values = data.json()
+ grid.clearAll()
+ if (values.ok){
+ grid.parse(values.rows, 'json')
+ }
+ }
+ })
+}
+
+
+function lst_cuentas_banco_change(nv, ov){
+ show('Cuenta change')
+}
+
+
+function cmd_agregar_retiro_click(){
+ show('Retiro')
+}
+
+
+function cmd_agregar_deposito_click(){
+ show('Depósito')
+}
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index d23b1c1..593cab9 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -953,6 +953,8 @@ function get_invoices(rango){
}
var grid = $$('grid_invoices')
+ grid.showProgress({type: 'icon'})
+
webix.ajax().get('/invoices', rango, {
error: function(text, data, xhr) {
webix.message({type: 'error', text: 'Error al consultar'})
diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js
index ab9fcdb..d44948d 100644
--- a/source/static/js/controller/main.js
+++ b/source/static/js/controller/main.js
@@ -63,6 +63,10 @@ var controllers = {
$$('cmd_delete_preinvoice').attachEvent('onItemClick', cmd_delete_preinvoice_click)
$$('cmd_facturar_preinvoice').attachEvent('onItemClick', cmd_facturar_preinvoice_click)
$$('grid_preinvoices').attachEvent('onItemClick', grid_preinvoices_click)
+
+ webix.extend($$('grid_invoices'), webix.ProgressBar)
+
+ bancos_controllers.init()
}
}
@@ -77,18 +81,18 @@ function get_uso_cfdi_to_table(){
function get_partners(){
- webix.ajax().get("/partners", {}, {
+ webix.ajax().get('/partners', {}, {
error: function(text, data, xhr) {
- webix.message({ type:"error", text: "Error al consultar"});
+ webix.message({type: 'error', text: 'Error al consultar'});
},
success: function(text, data, xhr) {
var values = data.json();
- $$("grid_partners").clearAll();
- if (values.ok){
- $$("grid_partners").parse(values.rows, 'json');
+ $$('grid_partners').clearAll();
+ if (values.data){
+ $$('grid_partners').parse(values.data, 'json');
};
}
- });
+ })
}
@@ -164,6 +168,14 @@ function multi_change(prevID, nextID){
return
}
+ if(nextID == 'app_bancos'){
+ active = $$('multi_bancos').getActiveId()
+ if(active == 'bancos_home'){
+ get_cuentas_banco()
+ }
+ return
+ }
+
if(nextID == 'app_invoices'){
active = $$('multi_invoices').getActiveId()
if(active == 'invoices_home'){
diff --git a/source/static/js/controller/products.js b/source/static/js/controller/products.js
index 87255ff..da87d4f 100644
--- a/source/static/js/controller/products.js
+++ b/source/static/js/controller/products.js
@@ -1,13 +1,21 @@
+function get_categorias(){
+ webix.ajax().sync().get('/values/categorias', function(text, data){
+ var values = data.json()
+ $$('categoria').getList().parse(values, 'plainjs')
+ })
+}
+
+
function cmd_new_product_click(id, e, node){
$$('form_product').setValues({
- id: 0, es_activo_producto: true})
+ id: 0, es_activo_producto: true})
add_config({'key': 'id_product', 'value': ''})
get_new_key()
get_taxes()
+ get_categorias()
$$('grid_products').clearSelection()
- $$('categoria').getList().load('/values/categorias')
$$('unidad').getList().load('/values/unidades')
$$("multi_products").setValue("product_new")
}
diff --git a/source/static/js/controller/util.js b/source/static/js/controller/util.js
index dd26684..ac5ca32 100644
--- a/source/static/js/controller/util.js
+++ b/source/static/js/controller/util.js
@@ -15,6 +15,23 @@ var table_usocfdi = db.addCollection('usocfdi')
var table_relaciones = db.addCollection('relaciones')
+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'},
+]
+
+
function show(values){
webix.message(JSON.stringify(values, null, 2))
}
@@ -153,3 +170,29 @@ function get_config(value){
return key.value
}
}
+
+
+webix.DataDriver.plainjs = webix.extend({
+ arr2hash:function(data){
+ var hash = {};
+ for (var i=0; ionStoreUpdated':function(){
this.data.each(function(obj, i){
@@ -286,12 +295,12 @@ var form_partner = {
view: 'form',
id: 'form_partner',
complexData: true,
+ scroll: true,
elements: controls_partner,
elementsConfig: {
labelWidth: 150,
labelAlign: 'right'
},
- autoheight: true,
rules: {
nombre: function(value){ return value.trim() != '';},
rfc: validate_rfc,
@@ -300,6 +309,15 @@ var form_partner = {
}
+var pager_clientes = {
+ view: "pager",
+ id: "pager_clientes",
+ template: "{common.prev()} {common.pages()} {common.next()}",
+ size: 100,
+ group: 10,
+}
+
+
var multi_partners = {
id: 'multi_partners',
animate: true,
@@ -307,8 +325,9 @@ var multi_partners = {
{id: 'partners_home', rows:[
{view: 'toolbar', elements: toolbar_partners},
grid_partners,
+ pager_clientes,
]},
- {id: 'partners_new', rows:[form_partner, {}]}
+ {id: 'partners_new', rows:[form_partner]}
]
}
diff --git a/source/static/js/ui/products.js b/source/static/js/ui/products.js
index 2e639ca..e2bbdaf 100644
--- a/source/static/js/ui/products.js
+++ b/source/static/js/ui/products.js
@@ -13,9 +13,9 @@ var toolbar_products = [
var grid_products_cols = [
{ id: "id", header: "ID", width: 75},
{ id: "clave", header: ["Clave", {content: "textFilter"}], width: 100,
- sort:"string" },
+ sort: 'string', footer: {content: 'rowCount', css: 'right'}},
{ id: "descripcion", header: ["Descripción", {content: "textFilter"}],
- fillspace:true, sort:"string" },
+ fillspace:true, sort: 'string', footer: 'Productos y Servicios'},
{ id: "unidad", header: ["Unidad", {content: "selectFilter"}], width: 150,
sort:"string" },
{ id: "valor_unitario", header: ["Precio", {content: "numberFilter"}], width: 150,
@@ -34,6 +34,7 @@ var grid_products = {
columns: grid_products_cols,
}
+
var suggest_categories = {
view: 'datasuggest',
type: 'tree',
@@ -85,7 +86,7 @@ var suggest_sat_producto = {
this.hide()
}
}
- }
+ },
}
@@ -95,7 +96,7 @@ var controls_generals = [
bottomLabel: 'Se recomienda solo desactivar y no eliminar'},
{cols: [
{view: 'combo', id: 'categoria', name: 'categoria', label: 'Categoría',
- labelPosition: 'top', options: suggest_categories},
+ labelPosition: 'top', suggest: suggest_categories},
{view: 'text', id: 'clave', name: 'clave', label: 'Clave',
labelPosition: 'top', readonly: true, required: true},
{view: 'checkbox', id: 'chk_automatica', label: 'Automática',
@@ -163,7 +164,7 @@ var form_product = {
cols: [{
view: "form",
id: "form_product",
- //~ width: 600,
+ scroll: true,
complexData: true,
elements: controls_products,
rules: {
@@ -182,7 +183,7 @@ var multi_products = {
{view:"toolbar", elements: toolbar_products},
grid_products,
]},
- {id: "product_new", rows:[form_product, {}]}
+ {id: "product_new", rows:[form_product]}
],
}
diff --git a/source/templates/main.html b/source/templates/main.html
index 2312e84..8c95768 100644
--- a/source/templates/main.html
+++ b/source/templates/main.html
@@ -7,11 +7,13 @@
+
+