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