#!/usr/bin/env python

from decimal import Decimal
import sqlite3
import click
from peewee import *
from playhouse.fields import PasswordField, ManyToManyField
from playhouse.shortcuts import case, SQL, cast


if __name__ == '__main__':
    import os, sys
    parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.insert(0, parent_dir)


from controllers import util
from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
    INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO


FORMAT = '{0:.2f}'


database_proxy = Proxy()
class BaseModel(Model):
    class Meta:
        database = database_proxy


def conectar(opt):
    db = {
        'sqlite': SqliteDatabase,
        'postgres': PostgresqlDatabase,
        'mysql': MySQLDatabase,
    }
    db_type = opt.pop('type')
    db_name = opt.pop('name')
    if not db_type in db:
        log.error('Tipo de base de datos no soportado')
        return False
    #~ print ('DB NAME', db_name)
    database = db[db_type](db_name, **opt)
    try:
        database_proxy.initialize(database)
        database_proxy.connect()
        log.info('Conectado a la BD...')
        return True
    except OperationalError as e:
        log.error('Error al intentar conectar a la base de datos')
        log.error(e)
        return False


def desconectar():
    if database_proxy.obj is None:
        return
    if not database_proxy.is_closed():
        database_proxy.close()
        log.info('Desconectado a la BD...')
    return


def upload_file(rfc, opt, file_obj):
    result = util.upload_file(rfc, opt, file_obj)
    if result['ok']:
        if opt != 'bdfl':
            Configuracion.add({opt: file_obj.filename})
    return result


def validar_timbrar():
    try:
        obj = Emisor.select()[0]
    except IndexError:
        msg = 'Es necesario agregar los datos del emisor'
        return {'ok': False, 'msg': msg}

    try:
        obj = Folios.select()[0]
    except IndexError:
        msg = 'Es necesaria al menos una serie de folios'
        return {'ok': False, 'msg': msg}

    msg = 'Es necesario configurar un certificado de sellos'
    try:
        obj = Certificado.select()[0]
    except IndexError:
        return {'ok': False, 'msg': msg}

    if not obj.serie:
        return {'ok': False, 'msg': msg}

    dias = obj.hasta - util.now()
    if dias.days < 0:
        msg = 'El certificado ha vencido, es necesario cargar uno nuevo'
        return {'ok': False, 'msg': msg}

    msg = ''
    if dias.days < 15:
        msg = 'El certificado vence en: {} días.'.format(dias.days)

    return {'ok': True, 'msg': msg}


def config_main():
    try:
        obj = Emisor.select()[0]
    except IndexError:
        obj = None

    data = {
        'empresa': 'Empresa Libre',
    }
    if not obj is None:
        titulo = 'Empresa Libre - <b><font color="#610B0B">{}</font></b>'
        data['empresa'] = titulo.format(obj.nombre)

    return data


def config_timbrar():
    try:
        obj = Emisor.select()[0]
    except IndexError:
        return {'cfdi_donativo': False}

    conf = {
        'cfdi_anticipo': Configuracion.get_('chk_config_anticipo'),
        'cfdi_donativo': obj.es_ong,
        'cfdi_ine': Configuracion.get_('chk_config_ine'),
    }

    return conf


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 isinstance(keys, str):
            data = (Configuracion
                .select(Configuracion.valor)
                .where(Configuracion.clave == keys)
            )
            if data:
                return data[0].valor
            return ''

        if keys['fields'] == 'correo':
            fields = ('correo_servidor', 'correo_puerto', 'correo_ssl',
                'correo_usuario', 'correo_contra', 'correo_copia',
                'correo_asunto', 'correo_mensaje', 'correo_directo')
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )
        elif keys['fields'] == 'path_cer':
            fields = ('path_key', 'path_cer')
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )
        elif keys['fields'] == 'templates':
            fields = (
                'txt_plantilla_factura_32',
                'txt_plantilla_factura_33',
                'txt_plantilla_factura_33j',
                'txt_plantilla_donataria',
            )
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )
        elif keys['fields'] == 'configotros':
            fields = (
                'chk_config_anticipo',
                'chk_config_ine',
            )
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )

        values = {r.clave: r.valor for r in data}
        return values

    @classmethod
    def add(cls, values):
        try:
            for k, v in values.items():
                #~ print (k, v)
                obj, created = Configuracion.get_or_create(clave=k)
                obj.valor = v
                obj.save()
            return {'ok': True}
        except Exception as e:
            log.error(str(e))
            return {'ok': False, 'msg': str(e)}

    class Meta:
        order_by = ('clave',)
        indexes = (
            (('clave', 'valor'), True),
        )


class Tags(BaseModel):
    tag = TextField(index=True, unique=True)

    class Meta:
        order_by = ('tag',)


class Usuarios(BaseModel):
    usuario = TextField(unique=True)
    nombre = TextField(default='')
    apellidos = TextField(default='')
    correo = TextField(default='')
    contraseña = PasswordField()
    es_superusuario = BooleanField(default=False)
    es_admin = BooleanField(default=False)
    es_activo = BooleanField(default=True)
    fecha_ingreso = DateTimeField(default=util.now)
    ultimo_ingreso = DateTimeField(null=True)

    def __str__(self):
        t = '{} {} ({})'
        return t.format(self.nombre, self.apellidos, self.usuario)

    class Meta:
        order_by = ('nombre', 'apellidos')


class Registro(BaseModel):
    usuario = TextField()
    accion = TextField(default='')
    tabla = TextField(default='')
    fecha = DateTimeField(default=util.now)

    def __str__(self):
        t = '{} {}-{} ({})'
        return t.format(self.usuario, self.accion, self.tabla, self.fecha)

    class Meta:
        order_by = ('usuario', 'fecha')

    @classmethod
    def add(cls, values):
        try:
            Registro.create(**values)
            return
        except:
            return


class SATRegimenes(BaseModel):
    key = TextField(index=True, unique=True)
    name = TextField(index=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)
    fisica = BooleanField(default=False)
    moral = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return '{} ({})'.format(self.name, self.key)

    @classmethod
    def get_(cls, ids):
        if isinstance(ids, int):
            ids = [ids]
        return SATRegimenes.select().where(SATRegimenes.id.in_(ids))

    @classmethod
    def get_activos(cls, rfc):
        where = ((SATRegimenes.activo==True) & (SATRegimenes.fisica==True))
        if (len(rfc) == 12):
            where = ((SATRegimenes.activo==True) & (SATRegimenes.moral==True))

        rows = (SATRegimenes
            .select(
                SATRegimenes.id,
                SATRegimenes.name.alias('value'))
            .where(where)
            .dicts()
        )
        return tuple(rows)


class Emisor(BaseModel):
    rfc = TextField(unique=True)
    nombre = TextField(default='')
    nombre_comercial = TextField(default='')
    calle = TextField(default='')
    no_exterior = TextField(default='')
    no_interior = TextField(default='')
    colonia = TextField(default='')
    municipio = TextField(default='')
    estado = TextField(default='')
    pais = TextField(default='México')
    codigo_postal = TextField(default='')
    cp_expedicion = TextField(default='')
    es_moral = BooleanField(default=False)
    es_ong = BooleanField(default=False)
    es_escuela = BooleanField(default=False)
    autorizacion = TextField(default='')
    fecha_autorizacion = DateField(null=True)
    fecha_dof = DateField(null=True)
    telefono = TextField(default='')
    correo = TextField(default='')
    web = TextField(default='')
    curp = TextField(default='')
    correo_timbrado = TextField(default='')
    token_timbrado = TextField(default='')
    token_soporte = TextField(default='')
    logo = TextField(default='')
    regimenes = ManyToManyField(SATRegimenes, related_name='emisores')

    def __str__(self):
        t = '{} ({})'
        return t.format(self.nombre, self.rfc)

    class Meta:
        order_by = ('nombre',)

    @classmethod
    def get_(cls, rfc):
        regimenes = SATRegimenes.get_activos(rfc)
        row = {'regimenes': regimenes}

        obj = Emisor.select().where(Emisor.rfc==rfc)
        if bool(obj):
            obj = obj[0]
            row['emisor'] = {
                'emisor_rfc': obj.rfc,
                'emisor_nombre': obj.nombre,
                'emisor_cp': obj.codigo_postal,
                'emisor_cp2': obj.cp_expedicion,
                'emisor_calle': obj.calle,
                'emisor_no_exterior': obj.no_exterior,
                'emisor_no_interior': obj.no_interior,
                'emisor_colonia': obj.colonia,
                'emisor_municipio': obj.municipio,
                'emisor_estado': obj.estado,
                'emisor_pais': obj.pais,
                'emisor_logo': obj.logo,
                'emisor_nombre_comercial': obj.nombre_comercial,
                'emisor_telefono': obj.telefono,
                'emisor_correo': obj.correo,
                'emisor_web': obj.web,
                'es_escuela': obj.es_escuela,
                'es_ong': obj.es_ong,
                'ong_autorizacion': obj.autorizacion,
                'ong_fecha': obj.fecha_autorizacion,
                'ong_fecha_dof': obj.fecha_dof,
                'correo_timbrado': obj.correo_timbrado,
                'token_timbrado': obj.token_timbrado,
                'token_soporte': obj.token_soporte,
                'regimenes': [row.id for row in obj.regimenes]
            }
        else:
            row['emisor'] = {'emisor_rfc': rfc}

        return {'ok': True, 'row': row}

    @classmethod
    def get_auth(cls):
        try:
            obj = Emisor.select()[0]
            data = {
                'RFC': obj.rfc,
                'USER': obj.correo_timbrado,
                'PASS': obj.token_timbrado,
            }
            return data
        except:
            return {}

    @classmethod
    def get_regimenes(cls):
        try:
            obj = Emisor.select()[0]
        except IndexError:
            return ()

        rows = [{'id': row.key, 'value': row.name} for row in obj.regimenes]
        return tuple(rows)

    def _clean(self, values):
        fields = util.clean(values)
        fields['rfc'] = fields.pop('emisor_rfc')
        fields['nombre'] = fields.pop('emisor_nombre')
        fields['codigo_postal'] = fields.pop('emisor_cp')
        fields['cp_expedicion'] = fields.pop('emisor_cp2', '') or fields['codigo_postal']
        fields['calle'] = fields.pop('emisor_calle', '')
        fields['no_exterior'] = fields.pop('emisor_no_exterior', '')
        fields['no_interior'] = fields.pop('emisor_no_interior', '')
        fields['colonia'] = fields.pop('emisor_colonia', '')
        fields['municipio'] = fields.pop('emisor_municipio', '')
        fields['estado'] = fields.pop('emisor_estado', '')
        fields['pais'] = fields.pop('emisor_pais', 'México')
        fields['logo'] = fields.pop('emisor_logo', '')
        fields['nombre_comercial'] = fields.pop('emisor_nombre_comercial', '')
        fields['telefono'] = fields.pop('emisor_telefono', '')
        fields['correo'] = fields.pop('emisor_correo', '')
        fields['web'] = fields.pop('emisor_web', '')
        fields['es_escuela'] = bool(fields['es_escuela'].replace('0', ''))
        fields['es_ong'] = bool(fields['es_ong'].replace('0', ''))
        fields['autorizacion'] = fields.pop('ong_autorizacion', '')
        fields['fecha_autorizacion'] = fields.pop('ong_fecha', None)
        fields['fecha_dof'] = fields.pop('ong_fecha_dof', None)
        if len(fields['rfc']) == 12:
            fields['es_moral'] = True
        fields['regimenes'] = SATRegimenes.get_(
            util.loads(fields['regimenes']))
        return fields

    @classmethod
    def add(cls, values):
        fields = cls._clean(cls, values)
        obj, created = Emisor.get_or_create(rfc=fields['rfc'])
        obj.regimenes = fields.pop('regimenes')
        q = Emisor.update(**fields).where(Emisor.id==obj.id)
        return {'ok': bool(q.execute())}


class Certificado(BaseModel):
    key = BlobField(null=True)
    key_enc = TextField(default='')
    cer = BlobField(null=True)
    cer_pem = TextField(default='')
    cer_txt = TextField(default='')
    p12 = BlobField(null=True)
    serie = TextField(default='')
    rfc = TextField(default='')
    desde = DateTimeField(null=True)
    hasta = DateTimeField(null=True)

    def __str__(self):
        return self.serie

    @classmethod
    def get_data(cls):
        obj = cls.get_(cls)
        row = {
            'cert_rfc': obj.rfc,
            'cert_serie': obj.serie,
            'cert_desde': obj.desde,
            'cert_hasta': obj.hasta,
        }
        return row

    def get_(cls):
        return Certificado.select()[0]

    @classmethod
    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)
        paths = Configuracion.get_({'fields': 'path_cer'})
        cert = util.Certificado(paths)
        data = cert.validate(values['contra'], session['rfc'])
        if data:
            msg = 'Certificado guardado correctamente'
            q = Certificado.update(**data).where(Certificado.id==obj.id)
            if q.execute():
                result = True
                row = {
                    'cert_rfc': data['rfc'],
                    'cert_serie': data['serie'],
                    'cert_desde': data['desde'],
                    'cert_hasta': data['hasta'],
                }
        else:
            msg = cert.error

        Configuracion.add({'path_key': ''})
        Configuracion.add({'path_cer': ''})

        return {'ok': result, 'msg': msg, 'data': row}



class Folios(BaseModel):
    serie = TextField(unique=True)
    inicio = IntegerField(default=1)
    default = BooleanField(default=False)
    usarcon = TextField(default='')

    class Meta:
        order_by = ('-default', 'serie', 'inicio')
        indexes = (
            (('serie', 'inicio'), True),
        )

    @classmethod
    def get_all(cls):
        rows = (Folios
            .select(
                Folios.id,
                Folios.serie.alias('value'),
                Folios.usarcon,
            )
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_(cls):
        rows = (Folios
            .select(
                Folios.id,
                SQL(" '-' AS delete"),
                Folios.serie,
                Folios.inicio,
                case(Folios.usarcon, (
                    ('I', 'Ingreso'),
                    ('E', 'Egreso'),
                    ('T', 'Traslado'),
                ), 'Todos').alias('usarcon'),
                case(Folios.default, (
                    (True, 'Si'),
                    (False, 'No'),
                )).alias('pre')
            )
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def add(cls, values):
        uc = {
            '': 'Todos',
            'I': 'Ingreso',
            'E': 'Egreso',
            'T': 'Traslado',
        }
        pre = {
            True: 'Si',
            False: 'No',
        }

        if 'default' in values:
            values['default'] = True
        try:
            obj = Folios.create(**values)
        except IntegrityError:
            msg = 'Serie ya existe'
            return {'ok': False, 'msg': msg}
        row = {
            'id': obj.id,
            'delete' : '-',
            'serie' : obj.serie,
            'inicio' : obj.inicio,
            'usarcon' : uc[obj.usarcon],
            'pre' : pre[obj.default],
        }
        return {'ok': True, 'row': row}

    @classmethod
    def remove(cls, id):
        q = Folios.delete().where(Folios.id==id)
        return bool(q.execute())


class Categorias(BaseModel):
    categoria = TextField()
    padre = ForeignKeyField('self', null=True, related_name='hijos')

    class Meta:
        order_by = ('categoria',)
        indexes = (
            (('categoria', 'padre'), True),
        )

    @classmethod
    def exists(cls, filters):
        return Categorias.select().where(filters).exists()

    @classmethod
    def get_all(cls):
        rows = (Categorias.select(
            Categorias.id,
            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)


class CondicionesPago(BaseModel):
    condicion = TextField(unique=True)

    class Meta:
        order_by = ('condicion',)

    def __str__(self):
        return self.condicion

    @classmethod
    def get_(cls):
        q = CondicionesPago.select(CondicionesPago.condicion).tuples()
        data = [r[0] for r in q]
        return data

    @classmethod
    def get_or(cls, value):
        if value is None:
            return value
        obj, _  = CondicionesPago.get_or_create(condicion=value)
        return obj


class SATUnidades(BaseModel):
    key = TextField(unique=True, index=True)
    name = TextField(default='', index=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name')
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return '{} ({})'.format(self.name, self.key)

    @classmethod
    def get_(self):
        rows = SATUnidades.select().dicts()
        return tuple(rows)

    @classmethod
    def add(self, values):
        try:
            SATUnidades.create(**values)
            return {'ok': True}
        except:
            return {'ok': False}

    @classmethod
    def actualizar(self, values):
        id = int(values['id'])
        if values['field'] == 'activo':
            v = {'0': False, '1': True}
            q = (SATUnidades
                .update(**{'activo': v[values['value']]})
                .where(SATUnidades.id==id))
            result = bool(q.execute())
        elif values['field'] == 'default':
            q = SATUnidades.update(**{'default': False})
            q.execute()

            v = {'false': False, 'true': True}
            q = (SATUnidades
                .update(**{'default': v[values['value']]})
                .where(SATUnidades.id==id))
            result = bool(q.execute())

        return {'ok': result}

    @classmethod
    def get_activos(cls):
        rows = (SATUnidades
            .select(
                SATUnidades.id,
                SATUnidades.name.alias('value'))
            .where(SATUnidades.activo==True)
            .dicts()
        )
        return tuple(rows)


class SATFormaPago(BaseModel):
    key = TextField(unique=True, index=True)
    name = TextField(default='', index=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return 'Forma de pago: ({}) {}'.format(self.key, self.name)

    @classmethod
    def get_(self):
        rows = SATFormaPago.select().dicts()
        return tuple(rows)

    @classmethod
    def get_by_key(cls, key):
        return SATFormaPago.get(SATFormaPago.key==key)

    @classmethod
    def get_activos(cls, values):
        field = SATFormaPago.id
        if values:
            field = SATFormaPago.key.alias('id')
        rows = (SATFormaPago
            .select(field, SATFormaPago.name.alias('value'))
            .where(SATFormaPago.activo==True)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def actualizar(self, values):
        id = int(values['id'])
        if values['field'] == 'activo':
            v = {'0': False, '1': True}
            q = (SATFormaPago
                .update(**{'activo': v[values['value']]})
                .where(SATFormaPago.id==id))
            result = bool(q.execute())
        elif values['field'] == 'default':
            q = SATFormaPago.update(**{'default': False})
            q.execute()

            v = {'false': False, 'true': True}
            q = (SATFormaPago
                .update(**{'default': v[values['value']]})
                .where(SATFormaPago.id==id))
            result = bool(q.execute())

        return {'ok': result}


class SATAduanas(BaseModel):
    key = TextField(unique=True, index=True)
    name = TextField(default='', index=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('key', 'name'), True),
        )


class SATMonedas(BaseModel):
    key = TextField(unique=True, index=True)
    name = TextField(default='', index=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return 'Moneda: ({}) {}'.format(self.key, self.name)

    @classmethod
    def get_(self):
        rows = SATMonedas.select().dicts()
        return tuple(rows)

    @classmethod
    def get_activos(cls):
        rows = (SATMonedas
            .select(
                SATMonedas.key.alias('id'),
                SATMonedas.name.alias('value'))
            .where(SATMonedas.activo==True)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_activos_by_id(cls):
        rows = (SATMonedas
            .select(
                SATMonedas.id,
                SATMonedas.name.alias('value'))
            .where(SATMonedas.activo==True)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def actualizar(self, values):
        id = int(values['id'])
        if values['field'] == 'activo':
            v = {'0': False, '1': True}
            q = (SATMonedas
                .update(**{'activo': v[values['value']]})
                .where(SATMonedas.id==id))
            result = bool(q.execute())
        elif values['field'] == 'default':
            q = SATMonedas.update(**{'default': False})
            q.execute()

            v = {'false': False, 'true': True}
            q = (SATMonedas
                .update(**{'default': v[values['value']]})
                .where(SATMonedas.id==id))
            result = bool(q.execute())

        return {'ok': result}


class SATImpuestos(BaseModel):
    key = TextField(index=True)
    name = TextField(default='', index=True)
    factor = TextField(default='T')
    tipo = TextField(default='T')
    tasa = DecimalField(default=0.0, decimal_places=6, auto_round=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('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'

        row = {
            'key': IMPUESTOS.get(values['impuesto']),
            'name': values['impuesto'],
            'tipo': tipo,
            'tasa': abs(tasa),
        }

        try:
            obj = SATImpuestos.create(**row)
            row['id'] = obj.id
            row['delete'] = '-'
            return {'ok': True, 'row': row}
        except IntegrityError:
            return {'ok': False, 'msg': 'El impuesto ya existe'}

    @classmethod
    def remove(cls, id):
        with database_proxy.transaction():
            try:
                q = SATImpuestos.delete().where(SATImpuestos.id==id)
                return bool(q.execute())
            except IntegrityError:
                return False

    @classmethod
    def get_(self):
        rows = (SATImpuestos.select(
            SATImpuestos.id,
            SQL(" '-' AS delete"),
            SATImpuestos.name,
            SATImpuestos.tipo,
            SATImpuestos.tasa,
            SATImpuestos.activo,
            SATImpuestos.default)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_activos(self):
        rows = SATImpuestos.select().where(SATImpuestos.activo==True).dicts()
        return tuple(rows)

    @classmethod
    def actualizar(self, values):
        id = int(values['id'])
        if values['field'] == 'activo':
            v = {'0': False, '1': True}
            q = (SATImpuestos
                .update(**{'activo': v[values['value']]})
                .where(SATImpuestos.id==id))
            result = bool(q.execute())
        elif values['field'] == 'default':
            q = SATImpuestos.update(**{'default': False})
            q.execute()

            v = {'false': False, 'true': True}
            q = (SATImpuestos
                .update(**{'default': v[values['value']]})
                .where(SATImpuestos.id==id))
            result = bool(q.execute())

        return {'ok': result}


class SATTipoRelacion(BaseModel):
    key = TextField(index=True, unique=True)
    name = TextField(default='', index=True)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return 'Tipo de relación: ({}) {}'.format(self.key, self.name)

    @classmethod
    def get_activos(cls, values):
        field = SATTipoRelacion.id
        if values:
            field = SATTipoRelacion.key.alias('id')
        rows = (SATTipoRelacion
            .select(field, SATTipoRelacion.name.alias('value'))
            .where(SATTipoRelacion.activo==True)
            .dicts()
        )
        return ({'id': '-', 'value': ''},) + tuple(rows)



class SATBancos(BaseModel):
    key = TextField(index=True, unique=True)
    name = TextField(index=True)
    razon_social = TextField(default='')
    rfc = TextField(default='')
    activo = BooleanField(default=False)

    class Meta:
        order_by = ('-activo', 'name')
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return 'Banco: {} ({})'.format(self.name, self.key)

    @classmethod
    def get_(cls):
        rows = SATBancos.select().dicts()
        return tuple(rows)

    @classmethod
    def get_activos_by_id(cls):
        rows = (SATBancos
            .select(
                SATBancos.id,
                SATBancos.name.alias('value'))
            .where(SATBancos.activo==True)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def actualizar(cls, values):
        id = int(values['id'])
        if values['field'] == 'activo':
            v = {'0': False, '1': True}
            q = (SATBancos
                .update(**{'activo': v[values['value']]})
                .where(SATBancos.id==id))
            result = bool(q.execute())
        return {'ok': result}


class CuentasBanco(BaseModel):
    de_emisor = BooleanField(default=False)
    activa = BooleanField(default=True)
    nombre = TextField()
    banco = ForeignKeyField(SATBancos)
    fecha_apertura = DateField(null=True)
    cuenta = TextField(default='')
    clabe = TextField(default='')
    moneda = ForeignKeyField(SATMonedas)
    saldo_inicial = 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)

    class Meta:
        order_by = ('nombre',)
        indexes = (
            (('banco', 'cuenta'), True),
        )

    def __str__(self):
        return '{} ({})'.format(self.banco.name, self.cuenta[-4:])

    @classmethod
    def actualizar_saldo(cls, id, saldo):
        fields = {'saldo': saldo}
        q = CuentasBanco.update(**fields).where(CuentasBanco.id==id)
        return bool(q.execute())

    @classmethod
    def get_saldo(cls, id):
        try:
            obj = CuentasBanco.get(CuentasBanco.id==id)
            return obj.saldo
        except CuentasBanco.DoesNotExist:
            return 0

    @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
            .select(
                CuentasBanco.id,
                CuentasBanco.activa,
                CuentasBanco.nombre,
                SATBancos.name.alias('banco'),
                CuentasBanco.fecha_apertura,
                CuentasBanco.cuenta,
                CuentasBanco.clabe,
                SATMonedas.name.alias('moneda'),
                CuentasBanco.saldo
            )
            .join(SATBancos).switch(CuentasBanco)
            .join(SATMonedas).switch(CuentasBanco)
            .where(CuentasBanco.de_emisor==True).dicts()
        )
        return tuple(rows)

    @classmethod
    def add(cls, values):
        w = '37137137137137137'
        dv = str(
            (10 -
            sum([(int(v) * int(values['clabe'][i])) % 10 for i, v in enumerate(w)])
            % 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():
            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,
                'descripcion': 'Saldo inicial',
                'forma_pago': SATFormaPago.get_by_key('99'),
                'deposito': values['saldo'],
                'saldo': values['saldo'],
            }
            MovimientosBanco.add(nuevo_mov)

        rows = (CuentasBanco
            .select(
                CuentasBanco.id,
                CuentasBanco.activa,
                CuentasBanco.nombre,
                SATBancos.name.alias('banco'),
                CuentasBanco.fecha_apertura,
                CuentasBanco.cuenta,
                CuentasBanco.clabe,
                SATMonedas.name.alias('moneda'),
                CuentasBanco.saldo
            )
            .join(SATBancos).switch(CuentasBanco)
            .join(SATMonedas).switch(CuentasBanco)
            .where(CuentasBanco.id==obj.id).dicts()
        )
        data = {'ok': True, 'row': rows[0]}
        return data


class MovimientosBanco(BaseModel):
    cuenta = ForeignKeyField(CuentasBanco)
    fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
    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)
    cancelado = BooleanField(default=False)
    conciliado = BooleanField(default=False)
    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',)

    def _ultimo_saldo(self, cuenta, fecha):
        query = (MovimientosBanco
            .select()
            .where(
                (MovimientosBanco.cuenta==cuenta) &
                (MovimientosBanco.fecha<fecha) &
                (MovimientosBanco.cancelado==False))[-1]
        )
        return round(float(query.saldo), DECIMALES)

    def _movimiento_anterior(self, cuenta, fecha):
        query = (MovimientosBanco
            .select()
            .where(
                (MovimientosBanco.cuenta==cuenta) &
                (MovimientosBanco.fecha<fecha) &
                (MovimientosBanco.cancelado==False))[-1]
        )
        return query

    def _actualizar_saldos(self, row):
        query = (MovimientosBanco
            .select()
            .where(
                (MovimientosBanco.cuenta==row.cuenta) &
                (MovimientosBanco.fecha>row.fecha) &
                (MovimientosBanco.cancelado==False))
        )

        saldo = round(Decimal(row.saldo), DECIMALES)
        for mov in query:
            mov.saldo = saldo + mov.deposito - mov.retiro
            mov.save()
            saldo = mov.saldo
        CuentasBanco.actualizar_saldo(row.cuenta, saldo)
        return saldo

    @classmethod
    def add(cls, values):
        ids = values.pop('ids', '')
        actualizar = False
        if 'saldo' in values:
            saldo = values['saldo']
        else:
            actualizar = True
            hora = values.pop('hora')
            values['fecha'] = '{}T{}'.format(values['fecha'][:10], hora)
            values['cuenta'] = int(values['cuenta'])
            values['retiro'] = util.get_float(values['retiro'])
            values['deposito'] = util.get_float(values['deposito'])
            values['forma_pago'] = int(values['forma_pago'])

            ultimo_saldo = cls._ultimo_saldo(
                cls, values['cuenta'], values['fecha'])
            values['saldo'] = \
                ultimo_saldo - values['retiro'] + values['deposito']

        with database_proxy.transaction():
            try:
                obj = MovimientosBanco.create(**values)
            except IntegrityError:
                msg = 'Este movimiento ya existe'
                return {'ok': False, 'msg': msg}

            if actualizar:
                saldo = cls._actualizar_saldos(cls, obj)
            if ids:
                FacturasPagos.add(obj, util.loads(ids))

        return {'ok': True, 'saldo': saldo}

    @classmethod
    def remove(cls, id):
        try:
            obj = MovimientosBanco.get(MovimientosBanco.id==id)
        except MovimientosBanco.DoesNotExist:
            return False

        if obj.conciliado or obj.cancelado:
            return False

        with database_proxy.transaction():
            obj.cancelado = True
            obj.save()
            FacturasPagos.cancelar(obj)

            obj = cls._movimiento_anterior(cls, obj.cuenta, obj.fecha)
            cls._actualizar_saldos(cls, obj)

        return 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) &
                (MovimientosBanco.cancelado==False)
            )
        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) &
                (MovimientosBanco.cancelado==False)
            )

        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)
    fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
    fecha_timbrado = DateTimeField(null=True)
    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)
    activo = BooleanField(default=False)
    default = BooleanField(default=False)
    fisica = BooleanField(default=True)
    moral = BooleanField(default=False)

    class Meta:
        order_by = ('-default', 'name',)
        indexes = (
            (('key', 'name'), True),
        )

    def __str__(self):
        return 'Uso del CFDI: {} ({})'.format(self.name, self.key)

    @classmethod
    def get_id(self, key):
        if key is None:
            return
        return SATUsoCfdi.get(SATUsoCfdi.key==key).id

    @classmethod
    def get_key(self, id):
        if id is None:
            return
        return SATUsoCfdi.get(SATUsoCfdi.id==id).key

    @classmethod
    def get_activos(cls):
        rows = (SATUsoCfdi
            .select(
                SATUsoCfdi.key.alias('id'),
                SATUsoCfdi.name.alias('value'),
                SATUsoCfdi.fisica,
                SATUsoCfdi.moral,
            )
            .where(SATUsoCfdi.activo==True)
            .dicts()
        )
        return tuple(rows)


class TipoCambio(BaseModel):
    dia = DateField(default=util.now)
    moneda = ForeignKeyField(SATMonedas)
    tipo_cambio = DecimalField(max_digits=15, decimal_places=6, auto_round=True)

    class Meta:
        order_by = ('-dia',)


class Addendas(BaseModel):
    nombre = TextField(unique=True)
    addenda = TextField()

    class Meta:
        order_by = ('nombre',)


class Socios(BaseModel):
    tipo_persona = IntegerField(default=1)
    rfc = TextField(index=True)
    nombre = TextField(index=True)
    slug = TextField(default='')
    nombre_comercial = TextField(index=True, default='')
    calle = TextField(default='')
    no_exterior = TextField(default='')
    no_interior = TextField(default='')
    colonia = TextField(default='')
    municipio = TextField(default='')
    estado = TextField(default='')
    pais = TextField(default='')
    codigo_postal = TextField(default='')
    notas = TextField(default='')
    telefonos = TextField(default='')
    es_activo = BooleanField(default=True)
    es_ong = BooleanField(default=False)
    fecha_alta = DateField(default=util.now)
    dias_pago = IntegerField(default=0)
    dias_habiles = BooleanField(default=False)
    es_cliente = BooleanField(default=False)
    es_proveedor = BooleanField(default=False)
    cuenta_cliente = TextField(default='')
    cuenta_proveedor = TextField(default='')
    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)
    condicion_pago = ForeignKeyField(CondicionesPago, null=True)
    addenda = ForeignKeyField(Addendas, null=True)
    uso_cfdi = ForeignKeyField(SATUsoCfdi, null=True)
    tags = ManyToManyField(Tags, related_name='socios_tags')

    def __str__(self):
        t = '{} ({})'
        return t.format(self.nombre, self.rfc)

    class Meta:
        order_by = ('nombre',)
        indexes = (
            (('rfc', 'slug'), True),
        )

    def _clean(self, values):
        fields = util.clean(values)
        fields['rfc'] = fields['rfc'].upper()
        fields['nombre'] = util.spaces(fields['nombre'])
        fields['slug'] = util.to_slug(fields['nombre'])
        uso_cfdi = fields.pop('uso_cfdi_socio', None)
        fields['uso_cfdi'] = SATUsoCfdi.get_id(uso_cfdi)
        fields['condicion_pago'] = \
            CondicionesPago.get_or(fields.get('condicion_pago', None))

        fb = ('dias_habiles', 'es_activo', 'es_cliente',
            'es_proveedor', 'es_ong')
        for name in fb:
            fields[name] = bool(fields[name].replace('0', ''))
        return fields

    @classmethod
    def get_(cls, 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'] = SATUsoCfdi.get_key(row.pop('uso_cfdi'))
            if not row['condicion_pago'] is None:
                row['condicion_pago'] = \
                    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,
                Socios.rfc,
                Socios.nombre,
                Socios.saldo_cliente)
            .dicts()
        )
        return {'pos': 0, 'total_count': total, 'data': tuple(rows)}

    @classmethod
    def get_by_client(cls, values):
        id = int(values.get('id', 0))
        if id:
            row = (Socios
                .select(
                    Socios.id, Socios.nombre, Socios.rfc,
                    SATFormaPago.key.alias('forma_pago'),
                    SATUsoCfdi.key.alias('uso_cfdi'))
                .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios)
                .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios)
                .where((Socios.id==id) & (Socios.es_cliente==True))
                .dicts()
            )
            if len(row):
                return {'ok': True, 'row': row[0]}
            return {'ok': False}

        name = values.get('name', '')
        if name:
            rows = (Socios
                .select(Socios.id, Socios.nombre, Socios.rfc,
                    SATFormaPago.key.alias('forma_pago'),
                    SATUsoCfdi.key.alias('uso_cfdi'))
                .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios)
                .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios)
                .where((Socios.es_cliente==True) &
                    (Socios.rfc.contains(name) |
                    Socios.nombre.contains(name)))
                .dicts())
            return tuple(rows)
        return {'ok': False}

    @classmethod
    def add(cls, values):
        fields = cls._clean(cls, values)
        try:
            obj = Socios.create(**fields)
        except IntegrityError:
            msg = 'Ya existe el RFC y Razón Social'
            data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
            return data

        #~ ToDo Agregar tags

        row = {
            'id': obj.id,
            'rfc': obj.rfc,
            'nombre': obj.nombre,
            'saldo_cliente': obj.saldo_cliente,
        }
        data = {'ok': True, 'row': row, 'new': True}
        return data

    @classmethod
    def actualizar(cls, values, id):
        fields = cls._clean(cls, values)
        try:
            q = Socios.update(**fields).where(Socios.id==id)
            q.execute()
        except IntegrityError:
            msg = 'Ya existe el RFC y Razón Social'
            data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
            return data

        row = {
            'id': id,
            'rfc': fields['rfc'],
            'nombre': fields['nombre'],
        }
        data = {'ok': True, 'row': row, 'new': False}
        return data

    @classmethod
    def remove(cls, id):
        count = (Facturas
            .select(fn.COUNT(Facturas.id)).join(Socios)
            .where(Socios.id==id)
            .count())
        if count:
            return False

        q = Socios.delete().where(Socios.id==id)
        return bool(q.execute())


class Almacenes(BaseModel):
    nombre = TextField(default='')
    ubicacion = TextField(default='')

    class Meta:
        order_by = ('nombre',)


class Productos(BaseModel):
    almacen = ForeignKeyField(Almacenes, null=True)
    categoria = ForeignKeyField(Categorias, null=True)
    clave = TextField(unique=True, index=True)
    clave_sat = TextField(default='')
    descripcion = TextField(index=True)
    unidad = ForeignKeyField(SATUnidades)
    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 = 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)
    impuestos = ManyToManyField(SATImpuestos, related_name='productos')
    tags = ManyToManyField(Tags, related_name='productos_tags')

    class Meta:
        order_by = ('descripcion',)

    @classmethod
    def next_key(cls):
        value = (Productos
            .select(fn.Max(Productos.id).alias('fm'))
            .order_by(SQL('fm'))
            .scalar())
        if value is None:
            value = 1
        else:
            value += 1
        return {'value': value}

    @classmethod
    def get_by(cls, values):
        id = int(values.get('id', 0))
        if id:
            row = (Productos
                .select(
                    Productos.id,
                    Productos.clave,
                    Productos.clave_sat,
                    Productos.descripcion,
                    SATUnidades.name.alias('unidad'),
                    Productos.valor_unitario,
                    Productos.descuento)
                .join(SATUnidades).switch(Productos)
                .where(Productos.id==id).dicts())
            if len(row):
                model_pt = Productos.impuestos.get_through_model()
                taxes = tuple(model_pt
                    .select(
                        model_pt.productos_id.alias('product'),
                        model_pt.satimpuestos_id.alias('tax'))
                    .where(model_pt.productos_id==id).dicts())
                return {'ok': True, 'row': row[0], 'taxes': taxes}
            return {'ok': False}

        name = values.get('name', '')
        if name:
            rows = (Productos
                .select(
                    Productos.id,
                    Productos.clave,
                    Productos.clave_sat,
                    Productos.descripcion,
                    SATUnidades.name.alias('unidad'),
                    Productos.valor_unitario)
                .join(SATUnidades)
                .switch(Productos)
                .where(Productos.descripcion.contains(name))
                .dicts()
            )
            return tuple(rows)
        return {'ok': False}

    @classmethod
    def get_(cls, values):
        if values:
            id = int(values['id'])
            row = (Productos
                .select(
                    Productos.id,
                    Productos.es_activo.alias('es_activo_producto'),
                    Productos.categoria,
                    Productos.clave,
                    Productos.clave_sat,
                    Productos.descripcion,
                    Productos.unidad,
                    Productos.valor_unitario,
                )
                .where(Productos.id==id).dicts()[0]
            )
            obj = Productos.get(Productos.id==id)
            taxes = [row.id for row in obj.impuestos]
            return {'row': row, 'taxes': taxes}

        rows = (Productos
            .select(
                Productos.id,
                Productos.clave,
                Productos.descripcion,
                SATUnidades.name.alias('unidad'),
                Productos.valor_unitario)
            .join(SATUnidades)
            .dicts()
        )
        return {'ok': True, 'rows': tuple(rows)}

    def _clean(self, values):
        taxes = util.loads(values.pop('taxes'))
        fields = util.clean(values)

        fields['es_activo'] = fields.pop('es_activo_producto')
        fields['descripcion'] = util.spaces(fields['descripcion'])
        fields['unidad'] = int(fields['unidad'])
        fields['valor_unitario'] = fields['valor_unitario'].replace(
            '$', '').replace(',', '')

        fb = ('es_activo', 'inventario')
        for name in fb:
            fields[name] = bool(fields[name].replace('0', ''))

        return fields, taxes

    @classmethod
    def add(cls, values):
        fields, taxes = cls._clean(cls, values)

        if Productos.select().where(Productos.clave==fields['clave']).exists():
            msg = 'Clave ya existe'
            return {'ok': False, 'msg': msg}

        obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes))

        with database_proxy.transaction():
            obj = Productos.create(**fields)
            obj.impuestos = obj_taxes
            row = {
                'id': obj.id,
                'clave': obj.clave,
                'descripcion': obj.descripcion,
                'unidad': obj.unidad.name,
                'valor_unitario': obj.valor_unitario,
            }
        data = {'ok': True, 'row': row, 'new': True}
        return data

    @classmethod
    def actualizar(cls, values, id):
        fields, taxes = cls._clean(cls, values)
        obj_taxes = SATImpuestos.select().where(SATImpuestos.id.in_(taxes))
        with database_proxy.transaction():
            q = Productos.update(**fields).where(Productos.id==id)
            try:
                q.execute()
            except IntegrityError:
                msg = 'Ya existe un producto con esta clave'
                data = {'ok': False, 'row': {}, 'new': False, 'msg': msg}
                return data

            obj = Productos.get(Productos.id==id)
            obj.impuestos = obj_taxes
            row = {
                'id': obj.id,
                'clave': obj.clave,
                'descripcion': obj.descripcion,
                'unidad': obj.unidad.name,
                'valor_unitario': obj.valor_unitario,
            }
        data = {'ok': True, 'row': row, 'new': False}
        return data

    @classmethod
    def remove(cls, id):
        count = (FacturasDetalle
            .select(fn.COUNT(FacturasDetalle.id)).join(Productos)
            .where(Productos.id==id)
            .count()
        )
        if count:
            return False

        with database_proxy.transaction():
            obj = Productos.get(Productos.id==id)
            obj.impuestos.clear()
            obj.tags.clear()
            q = Productos.delete().where(Productos.id==id)
            return bool(q.execute())


class Facturas(BaseModel):
    cliente = ForeignKeyField(Socios)
    version = TextField(default=CURRENT_CFDI)
    serie = TextField(default='')
    folio = IntegerField(default=0)
    fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
    fecha_timbrado = DateTimeField(null=True)
    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, max_digits=15, 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(
        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)
    xml = TextField(default='')
    uuid = UUIDField(null=True)
    estatus = TextField(default='Guardada')
    estatus_sat = TextField(default='Vigente')
    regimen_fiscal = TextField(default='')
    notas = TextField(default='')
    saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    pagada = BooleanField(default=False)
    cancelada = BooleanField(default=False)
    fecha_cancelacion = DateTimeField(null=True)
    acuse = TextField(default='')
    donativo = BooleanField(default=False)
    anticipo = BooleanField(default=False)
    egreso_anticipo = BooleanField(default=False)
    tipo_relacion = TextField(default='')
    error = TextField(default='')

    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.alias('year'))
            .group_by(Facturas.fecha.year)
            .order_by(Facturas.fecha.year)
        )
        if not rows is None:
            data += [{'id': int(r.year), 'value': int(r.year)} for r in rows]
        return tuple(data)

    @classmethod
    def get_xml(cls, id):
        obj = Facturas.get(Facturas.id==id)
        name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
        return obj.xml, name

    #~ Revisar
    def _get_data_cfdi_to_pdf(self, xml, cancel, version):
        pre_nomina = PRE['NOMINA'][version]

        data['nomina'] = {}
        node = doc.find('{}Complemento/{}Nomina'.format(pre, pre_nomina))
        if not node is None:
            data['nomina']['nomina'] = node.attrib.copy()
            subnode = node.find('{}Emisor'.format(pre_nomina))
            if not subnode is None:
                data['emisor'].update(subnode.attrib.copy())
            subnode = node.find('{}Receptor'.format(pre_nomina))
            data['receptor'].update(subnode.attrib.copy())

            subnode = node.find('{}Percepciones'.format(pre_nomina))
            data['nomina']['percepciones'] = subnode.attrib.copy()
            detalle = []
            for n in subnode.getchildren():
                if 'SeparacionIndemnizacion' in n.tag:
                    continue
                detalle.append(n.attrib.copy())
            data['nomina']['percepciones']['detalle'] = detalle

            data['nomina']['deducciones'] = None
            subnode = node.find('{}Deducciones'.format(pre_nomina))
            if not subnode is None:
                data['nomina']['deducciones'] = subnode.attrib.copy()
                detalle = []
                for n in subnode.getchildren():
                    detalle.append(n.attrib.copy())
                data['nomina']['deducciones']['detalle'] = detalle

            data['nomina']['incapacidades'] = None
            subnode = node.find('{}Incapacidades'.format(pre_nomina))
            if not subnode is None:
                detalle = []
                for n in subnode.getchildren():
                    detalle.append(n.attrib.copy())
                data['nomina']['incapacidades'] = detalle

            data['nomina']['otrospagos'] = None
            subnode = node.find('{}OtrosPagos'.format(pre_nomina))
            if not subnode is None:
                data['nomina']['otrospagos'] = subnode.attrib.copy()
                detalle = []
                for n in subnode.getchildren():
                    detalle.append(n.attrib.copy())
                    ns = n.find('{}SubsidioAlEmpleo'.format(pre_nomina))
                    if not ns is None:
                        data['nomina']['otrospagos']['SubsidioCausado'] = ns.attrib['SubsidioCausado']
                data['nomina']['otrospagos']['detalle'] = detalle

        return data

    def _get_not_in_xml(self, invoice, emisor):
        values = {}

        if invoice.version == '3.2':
            return values

        obj = SATRegimenes.get(SATRegimenes.key==invoice.regimen_fiscal)
        values['regimenfiscal'] = str(obj)

        obj = SATUsoCfdi.get(SATUsoCfdi.key==invoice.uso_cfdi)
        values['usocfdi'] = str(obj)

        mp = {
            'PUE': 'Pago en una sola exhibición',
            'PPD': 'Pago en parcialidades o diferido',
        }
        values['metododepago'] = 'Método de Pago: ({}) {}'.format(
            invoice.metodo_pago, mp[invoice.metodo_pago])

        obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago)
        values['formadepago'] = str(obj)

        obj = SATMonedas.get(SATMonedas.key==invoice.moneda)
        values['moneda'] = str(obj)

        if invoice.tipo_relacion:
            obj = SATTipoRelacion.get(SATTipoRelacion.key==invoice.tipo_relacion)
            values['tiporelacion'] = str(obj)

        values['fechadof'] = str(emisor.fecha_dof)

        return values

    @classmethod
    def get_pdf(cls, id, rfc):
        emisor = Emisor.select()[0]
        obj = Facturas.get(Facturas.id==id)
        name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc)
        if obj.uuid is None:
            return b'', name

        values = cls._get_not_in_xml(cls, obj, emisor)
        data = util.get_data_from_xml(obj, values)
        doc = util.to_pdf(data, emisor.rfc)
        return doc, name

    @classmethod
    def get_zip(cls, id, rfc):
        obj = Facturas.get(Facturas.id==id)
        name_zip = '{}{}_{}.zip'.format(obj.serie, obj.folio, obj.cliente.rfc)
        if obj.uuid is None:
            return b'', name_zip

        file_xml = cls.get_xml(id)
        if not file_xml[0]:
            return b'', name_zip

        file_pdf = cls.get_pdf(id, rfc)
        if not file_pdf[0]:
            return b'', name_zip

        file_zip = util.to_zip(file_xml, file_pdf)

        return file_zip, name_zip

    @util.run_in_thread
    def _send(self, id, rfc):
        return Facturas.send(id, rfc)

    @util.run_in_thread
    def _actualizar_saldo_cliente(self, invoice):
        if invoice.tipo_comprobante == 'T':
            return

        if invoice.donativo and invoice.forma_pago == '12':
            return

        importe = invoice.total_mn
        if invoice.tipo_comprobante == 'E':
            importe *= -1

        q = (Socios
            .update(saldo_cliente=Socios.saldo_cliente + importe)
            .where(Socios.id==invoice.cliente.id)
        )
        return bool(q.execute())

    @classmethod
    def send(cls, id, rfc):
        values = Configuracion.get_({'fields': 'correo'})
        if not values:
            msg = 'No esta configurado el servidor de correo de salida'
            return {'ok': False, 'msg': msg}

        obj = Facturas.get(Facturas.id==id)
        if obj.uuid is None:
            msg = 'La factura no esta timbrada'
            return {'ok': False, 'msg': msg}

        if not obj.cliente.correo_facturas:
            msg = 'El cliente no tiene configurado el correo para facturas'
            return {'ok': False, 'msg': msg}

        files = (cls.get_zip(id, rfc),)

        fields = util.make_fields(obj.xml)
        server = {
            'servidor': values['correo_servidor'],
            'puerto': values['correo_puerto'],
            'ssl': bool(int(values['correo_ssl'])),
            'usuario': values['correo_usuario'],
            'contra': values['correo_contra'],
        }
        options = {
            'para': obj.cliente.correo_facturas,
            'copia': values['correo_copia'],
            'asunto': util.make_info_mail(values['correo_asunto'], fields),
            'mensaje': util.make_info_mail(values['correo_mensaje'], fields),
            'files': files,
        }
        data= {
            'server': server,
            'options': options,
        }
        result = util.send_mail(data)
        if not result['ok'] or result['msg']:
            return {'ok': False, 'msg': result['msg']}

        msg = 'Factura enviada correctamente'
        return {'ok': True, 'msg': msg}

    def _get_filter_folios(self, values):
        if not 'folio' in values:
            return ''

        folios = values['folio'].split('-')
        if len(folios) == 1:
            try:
                folio1 = int(folios[0])
            except ValueError:
                return ''

            folio2 = folio1
        else:
            try:
                folio1 = int(folios[0])
                folio2 = int(folios[1])
            except ValueError:
                return ''

        return (Facturas.folio.between(folio1, folio2))

    def _get_por_pagar(self, ids):
        filtros = (
            (Facturas.cancelada==False) &
            (Facturas.uuid.is_null(False)) &
            (Facturas.tipo_comprobante=='I') &
            (Facturas.saldo>0)
        )
        if ids:
            filtros &= (Facturas.id.not_in(ids))

        rows = tuple(Facturas
            .select(
                Facturas.id,
                Facturas.serie,
                Facturas.folio,
                Facturas.uuid,
                Facturas.fecha,
                Facturas.tipo_comprobante,
                Facturas.estatus,
                Socios.nombre.alias('cliente'),
                Facturas.total,
                Facturas.saldo,
            )
            .where(filtros)
            .join(Socios)
            .switch(Facturas)
            .dicts()
        )
        return {'ok': True, 'rows': rows}

        return

    def _get_opt(self, values):
        if values['opt'] == 'porpagar':
            return self._get_por_pagar(self, util.loads(values['ids']))

        cfdis = util.loads(values['cfdis'])

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

        if values['opt'] == 'relacionados':
            folios = self._get_filter_folios(self, values)
            uuid = values.get('uuid', '')
            if uuid:
                f_uuid = (cast(Facturas.uuid, 'text').contains(uuid))
            cliente = (Facturas.cliente == int(values['id_cliente']))
            if cfdis:
                f_ids = (Facturas.id.not_in(cfdis))
            else:
                f_ids = (Facturas.id > 0)

            filters = (fy & fm & cliente & f_ids)
            if folios:
                filters = filters & folios
            elif uuid:
                filters = filters & f_uuid

            if values['anticipo'] == '1':
                filters = filters & (Facturas.anticipo == True)

            rows = tuple(Facturas
                .select(Facturas.id, Facturas.serie, Facturas.folio,
                    Facturas.uuid, Facturas.fecha, Facturas.tipo_comprobante,
                    Facturas.estatus, Facturas.total_mn)
                .where(filters).dicts()
            )

        return {'ok': True, 'rows': rows}

    @classmethod
    def get_(cls, values):
        opt = values.get('opt', '')
        if opt:
            return cls._get_opt(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()
            )
        return {'ok': True, 'rows': rows}

    @classmethod
    def remove(cls, id):
        obj = Facturas.get(Facturas.id==id)
        if obj.uuid:
            return False

        q = FacturasDetalle.delete().where(FacturasDetalle.factura==obj)
        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):
        inicio_serie = Folios.select(
            Folios.inicio).where(Folios.serie==serie).scalar()

        inicio = (Facturas
            .select(fn.Max(Facturas.folio).alias('mf'))
            .where(Facturas.serie==serie)
            .order_by(SQL('mf'))
            .scalar())

        if inicio is None:
            inicio = inicio_serie
        else:
            inicio += 1

        return inicio

    def _calculate_totals(self, invoice, products):
        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None
        total_retenciones = None
        total_iva = 0
        locales_traslados = 0
        locales_retenciones = 0

        for product in products:
            id_product = product.pop('id')
            p = Productos.get(Productos.id==id_product)

            product['unidad'] = p.unidad.key
            product['clave'] = p.clave
            product['clave_sat'] = p.clave_sat

            product['factura'] = invoice.id
            product['producto'] = id_product

            cantidad = float(product['cantidad'])
            valor_unitario = float(product['valor_unitario'])
            descuento = float(product['descuento'])
            precio_final = valor_unitario - descuento
            importe = round(cantidad * precio_final, DECIMALES)

            product['cantidad'] = cantidad
            product['valor_unitario'] = valor_unitario
            product['descuento'] = descuento
            product['precio_final'] = precio_final
            product['importe'] = round(cantidad * valor_unitario, DECIMALES)

            descuento_cfdi += descuento
            subtotal += importe

            FacturasDetalle.create(**product)

            for tax in p.impuestos:
                if tax.id in totals_tax:
                    totals_tax[tax.id].importe += importe
                else:
                    tax.importe = importe
                    totals_tax[tax.id] = tax

        for tax in totals_tax.values():
            if tax.tipo == 'E' or tax.tipo == 'R':
                continue

            import_tax = round(float(tax.tasa) * tax.importe, DECIMALES)
            if tax.key == '000':
                locales_traslados += import_tax
            else:
                total_trasladados = (total_trasladados or 0) + import_tax
            if tax.name == 'IVA':
                total_iva += import_tax

            invoice_tax = {
                'factura': invoice.id,
                'impuesto': tax.id,
                'base': tax.importe,
                'importe': import_tax,
            }
            FacturasImpuestos.create(**invoice_tax)

        for tax in totals_tax.values():
            if tax.tipo == 'E' or tax.tipo == 'T':
                continue
            if tax.tasa == round(Decimal(2/3), 6):
                import_tax = round(float(tax.tasa) * total_iva, DECIMALES)
            else:
                import_tax = round(float(tax.tasa) * tax.importe, DECIMALES)
            if tax.key == '000':
                locales_retenciones += import_tax
            else:
                total_retenciones = (total_retenciones or 0) + import_tax

            invoice_tax = {
                'factura': invoice.id,
                'impuesto': tax.id,
                'base': tax.importe,
                'importe': import_tax,
            }
            FacturasImpuestos.create(**invoice_tax)

        total = subtotal + \
            (total_trasladados or 0) - (total_retenciones or 0) \
            + locales_traslados - locales_retenciones
        total_mn = round(total * invoice.tipo_cambio, DECIMALES)
        data = {
            'subtotal': subtotal + descuento,
            'descuento': descuento_cfdi,
            'total': total,
            'total_mn': total_mn,
            'total_trasladados': total_trasladados,
            'total_retenciones': total_retenciones,
        }
        return data

    def _guardar_relacionados(self, invoice, relacionados):
        for cfdi in relacionados:
            data = {
                'factura': invoice,
                'factura_origen': cfdi,
            }
            FacturasRelacionadas.create(**data)
        return

    def _guardar_ine(self, invoice, valores):
        if not valores:
            return

        data = {
            'factura': invoice,
            'nombre': 'ine',
            'valores': valores,
        }
        FacturasComplementos.create(**data)
        return

    @classmethod
    def add(cls, values):
        productos = util.loads(values.pop('productos'))
        relacionados = util.loads(values.pop('relacionados'))
        ine = values.pop('ine', {})

        emisor = Emisor.select()[0]
        values['folio'] = cls._get_folio(cls, values['serie'])
        values['tipo_cambio'] = float(values['tipo_cambio'])
        values['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
        values['anticipo'] = util.get_bool(values['anticipo'])
        values['donativo'] = util.get_bool(values['donativo'])

        with database_proxy.atomic() as txn:
            obj = Facturas.create(**values)
            totals = cls._calculate_totals(cls, obj, productos)
            cls._guardar_relacionados(cls, obj, relacionados)
            cls._guardar_ine(cls, obj, ine)
            obj.subtotal = totals['subtotal']
            obj.descuento = totals['descuento']
            obj.total_trasladados = totals['total_trasladados']
            obj.total_retenciones = totals['total_retenciones']
            obj.total = totals['total']
            obj.saldo = totals['total']
            obj.total_mn = totals['total_mn']
            obj.save()

        msg = 'Factura guardada correctamente. Enviando a timbrar'
        row = {
            'id': obj.id,
            'serie': obj.serie,
            'folio': obj.folio,
            'uuid': obj.uuid,
            'fecha': obj.fecha,
            'tipo_comprobante': obj.tipo_comprobante,
            'estatus': obj.estatus,
            'total_mn': obj.total_mn,
            'cliente': obj.cliente.nombre,
        }
        data = {'ok': True, 'row': row, 'new': True, 'error': False, 'msg': msg}
        return data

    def _make_xml(self, invoice):
        emisor = Emisor.select()[0]
        certificado = Certificado.select()[0]

        comprobante = {}
        relacionados = {}
        donativo = {}
        complementos = FacturasComplementos.get_(invoice)

        if invoice.donativo:
            donativo['noAutorizacion'] = emisor.autorizacion
            donativo['fechaAutorizacion'] = str(emisor.fecha_autorizacion)

        if invoice.serie:
            comprobante['Serie'] = invoice.serie
        if invoice.condiciones_pago:
            comprobante['CondicionesDePago'] = invoice.condiciones_pago
        if invoice.descuento:
            comprobante['Descuento'] = invoice.descuento

        comprobante['Folio'] = str(invoice.folio)
        comprobante['Fecha'] = invoice.fecha.isoformat()[:19]
        comprobante['FormaPago'] = invoice.forma_pago
        comprobante['NoCertificado'] = certificado.serie
        comprobante['Certificado'] = certificado.cer_txt
        comprobante['SubTotal'] = FORMAT.format(invoice.subtotal)
        comprobante['Moneda'] = invoice.moneda
        comprobante['TipoCambio'] = '1'
        if comprobante['Moneda'] != 'MXN':
            comprobante['TipoCambio'] = FORMAT.format(invoice.tipo_cambio)
        comprobante['Total'] = FORMAT.format(invoice.total)
        comprobante['TipoDeComprobante'] = invoice.tipo_comprobante
        comprobante['MetodoPago'] = invoice.metodo_pago
        comprobante['LugarExpedicion'] = invoice.lugar_expedicion
        if invoice.descuento:
            comprobante['Descuento'] = FORMAT.format(invoice.descuento)

        if invoice.tipo_relacion:
            relacionados = {
                'tipo': invoice.tipo_relacion,
                'cfdis': FacturasRelacionadas.get_(invoice),
            }

        emisor = {
            'Rfc': emisor.rfc,
            'Nombre': emisor.nombre,
            'RegimenFiscal': invoice.regimen_fiscal,
        }

        receptor = {
            'Rfc': invoice.cliente.rfc,
            'Nombre': invoice.cliente.nombre,
            'UsoCFDI': invoice.uso_cfdi,
        }

        conceptos = []
        rows = FacturasDetalle.select().where(FacturasDetalle.factura==invoice)
        for row in rows:
            concepto = {
                'ClaveProdServ': row.producto.clave_sat,
                'NoIdentificacion': row.producto.clave,
                'Cantidad': FORMAT.format(row.cantidad),
                'ClaveUnidad': row.producto.unidad.key,
                'Unidad': row.producto.unidad.name,
                'Descripcion': row.descripcion,
                'ValorUnitario': FORMAT.format(row.valor_unitario),
                'Importe': FORMAT.format(row.importe),
            }
            if row.descuento:
                concepto['Descuento'] = FORMAT.format(row.descuento)

            taxes = {}
            traslados = []
            retenciones = []

            for impuesto in row.producto.impuestos:
                if impuesto.tipo == 'E':
                    continue

                if impuesto.key == '000':
                    continue

                base = row.importe - row.descuento
                import_tax = round(impuesto.tasa * base, DECIMALES)
                tipo_factor = 'Tasa'
                if impuesto.factor != 'T':
                    tipo_factor = 'Cuota'
                tax = {
                    "Base": FORMAT.format(base),
                    "Impuesto": impuesto.key,
                    "TipoFactor": tipo_factor,
                    "TasaOCuota": str(impuesto.tasa),
                    "Importe": FORMAT.format(import_tax),
                }
                if impuesto.tipo == 'T':
                    traslados.append(tax)
                else:
                    retenciones.append(tax)

            if traslados:
                taxes['traslados'] = traslados
            if retenciones:
                taxes['retenciones'] = retenciones
            concepto['impuestos'] = taxes
            conceptos.append(concepto)

        impuestos = {}
        traslados = []
        retenciones = []
        total_locales_trasladados = 0
        total_locales_retenciones = 0
        locales_trasladados = []
        locales_retenciones = []
        if not invoice.total_trasladados is None:
            impuestos['TotalImpuestosTrasladados'] = \
                FORMAT.format(invoice.total_trasladados)
        if not invoice.total_retenciones is None:
            impuestos['TotalImpuestosRetenidos'] = \
                FORMAT.format(invoice.total_retenciones)

        taxes = (FacturasImpuestos
                .select()
                .where(FacturasImpuestos.factura==invoice))
        for tax in taxes:
            if tax.impuesto.key == '000':
                tasa = str(round(tax.impuesto.tasa * 100, 2))
                simporte = FORMAT.format(tax.importe)
                if tax.impuesto.tipo == 'T':
                    traslado = {
                        'ImpLocTrasladado': tax.impuesto.name,
                        'TasadeTraslado': tasa,
                        'Importe': simporte,
                    }
                    locales_trasladados.append(traslado)
                    total_locales_trasladados += tax.importe
                else:
                    retencion = {
                        'ImpLocRetenido': tax.impuesto.name,
                        'TasadeRetencion': tasa,
                        'Importe': simporte,
                    }
                    locales_retenciones.append(retencion)
                    total_locales_retenciones += tax.importe
                continue

            tipo_factor = 'Tasa'
            if tax.impuesto.factor != 'T':
                tipo_factor = 'Cuota'
            if tax.impuesto.tipo == 'T':
                traslado = {
                    "Impuesto": tax.impuesto.key,
                    "TipoFactor": tipo_factor,
                    "TasaOCuota": str(tax.impuesto.tasa),
                    "Importe": FORMAT.format(tax.importe),
                }
                traslados.append(traslado)
            else:
                retencion = {
                    "Impuesto": tax.impuesto.key,
                    "Importe": FORMAT.format(tax.importe),
                }
                retenciones.append(retencion)

        impuestos['traslados'] = traslados
        impuestos['retenciones'] = retenciones
        impuestos['total_locales_trasladados'] = '0.00'
        if total_locales_trasladados:
            impuestos['total_locales_trasladados'] = \
                FORMAT.format(total_locales_trasladados)
        impuestos['total_locales_retenciones'] = '0.00'
        if total_locales_retenciones:
            impuestos['total_locales_retenciones'] = \
                FORMAT.format(total_locales_retenciones)
        impuestos['locales_trasladados'] = locales_trasladados
        impuestos['locales_retenciones'] = locales_retenciones

        data = {
            'comprobante': comprobante,
            'relacionados': relacionados,
            'emisor': emisor,
            'receptor': receptor,
            'conceptos': conceptos,
            'impuestos': impuestos,
            'donativo': donativo,
            'complementos': complementos,
        }
        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 anticipo_egreso(cls, id):
        origen = Facturas.get(Facturas.id == id)
        relacionadas = (FacturasRelacionadas
            .select(FacturasRelacionadas.factura_origen)
            .where(FacturasRelacionadas.factura==origen))
        conceptos = (FacturasDetalle
            .select()
            .where(FacturasDetalle.factura==origen))
        impuestos = (FacturasImpuestos
            .select()
            .where(FacturasImpuestos.factura==origen))

        #~ egreso_anticipo = BooleanField(default=False)

        serie = Folios.get_egreso(origen.serie)
        nueva = {
            'cliente': origen.cliente,
            'tipo_comprobante': 'E',
            'forma_pago': '30',
            'serie': serie,
            'folio': cls._get_folio(cls, serie),
            'tipo_relacion': '07',
            'pagada': True,
            'lugar_expedicion': origen.lugar_expedicion,
            'uso_cfdi': origen.uso_cfdi,
            'moneda': origen.moneda,
            'tipo_cambio': origen.tipo_cambio,
            'regimen_fiscal': origen.regimen_fiscal,
            'subtotal': origen.subtotal,
            'total': origen.total,
            'total_trasladados': origen.total_trasladados,
            'total_retenciones': origen.total_retenciones,
        }
        return

    @classmethod
    def timbrar(cls, id):
        obj = Facturas.get(Facturas.id == id)
        obj.xml = cls._make_xml(cls, obj)
        obj.estatus = 'Generada'
        obj.save()

        enviar_correo = util.get_bool(Configuracion.get_('correo_directo'))
        auth = Emisor.get_auth()

        anticipo = False
        msg = 'Factura timbrada correctamente'
        result = util.timbra_xml(obj.xml, auth)
        if result['ok']:
            obj.xml = result['xml']
            obj.uuid = result['uuid']
            obj.fecha_timbrado = result['fecha']
            obj.estatus = 'Timbrada'
            obj.error = ''
            obj.save()
            row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
            if enviar_correo:
                cls._send(cls, id, auth['RFC'])
            if obj.tipo_comprobante == 'I' and obj.tipo_relacion == '07':
                anticipo = True
            cls._actualizar_saldo_cliente(cls, obj)
        else:
            msg = result['error']
            obj.estatus = 'Error'
            obj.error = msg
            obj.save()
            row = {'estatus': 'Error'}

        result = {
            'ok': result['ok'],
            'msg': msg,
            'row': row,
            'anticipo': anticipo
        }

        return result


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

    @classmethod
    def enviar(cls, id):
        values = Configuracion.get_({'fields': 'correo'})
        if not values:
            msg = 'No esta configurado el servidor de correo de salida'
            return {'ok': False, 'msg': msg}

        obj = PreFacturas.get(PreFacturas.id==id)
        if not obj.cliente.correo_facturas:
            msg = 'El cliente no tiene configurado el correo para facturas'
            return {'ok': False, 'msg': msg}

        files = (cls.get_pdf(id),)

        invoice = PreFacturas.select().where(PreFacturas.id==id).dicts()[0]
        fields = {
            'receptor_nombre': obj.cliente.nombre,
            'receptor_rfc': obj.cliente.rfc,
        }
        fields.update(invoice)

        asunto = 'Enviamos la prefactura: PRE-{}'.format(obj.folio)
        server = {
            'servidor': values['correo_servidor'],
            'puerto': values['correo_puerto'],
            'ssl': bool(int(values['correo_ssl'])),
            'usuario': values['correo_usuario'],
            'contra': values['correo_contra'],
        }
        options = {
            'para': obj.cliente.correo_facturas,
            'copia': values['correo_copia'],
            'asunto': asunto,
            'mensaje': util.make_info_mail(values['correo_mensaje'], fields),
            'files': files,
        }
        data= {
            'server': server,
            'options': options,
        }
        result = util.send_mail(data)
        if not result['ok'] or result['msg']:
            return {'ok': False, 'msg': result['msg']}

        msg = 'Pre Factura enviada correctamente'
        return {'ok': True, 'msg': msg}

    def _get_info_to_pdf(self, id):
        data = {}
        obj = PreFacturas.select().where(PreFacturas.id==id).dicts()[0]
        regimen = SATRegimenes.get(SATRegimenes.key==obj['regimen_fiscal'])
        usocfdi = SATUsoCfdi.get(SATUsoCfdi.key==obj['uso_cfdi'])
        formapago = SATFormaPago.get(SATFormaPago.key==obj['forma_pago'])
        moneda = SATMonedas.get(SATMonedas.key==obj['moneda'])

        emisor  = util.get_dict(Emisor.select().dicts()[0])
        emisor['nointerior'] = emisor['no_interior']
        emisor['noexterior'] = emisor['no_exterior']
        emisor['codigopostal'] = emisor['codigo_postal']
        emisor['regimenfiscal'] = str(regimen)

        receptor  = Socios.select().where(Socios.id==obj['cliente']).dicts()[0]
        receptor['usocfdi'] = str(usocfdi)
        receptor['nointerior'] = receptor['no_interior']
        receptor['noexterior'] = receptor['no_exterior']
        receptor['codigopostal'] = receptor['codigo_postal']

        data['es_pre'] = True
        data['cancelada'] = False

        tipos = {
            'I': 'ingreso',
            'E': 'egreso',
            'T': 'traslado',
        }
        mp = {
            'PUE': 'Pago en una sola exhibición',
            'PPD': 'Pago en parcialidades o diferido',
        }

        data['comprobante'] = obj
        data['comprobante']['version'] = CURRENT_CFDI
        data['comprobante']['folio'] = '{}-{}'.format(
            data['comprobante']['serie'], data['comprobante']['folio'])
        data['comprobante']['fecha'] = str(data['comprobante']['fecha'])
        data['comprobante']['tipodecomprobante'] = tipos.get(
            data['comprobante']['tipo_comprobante'])
        data['comprobante']['lugarexpedicion'] = \
            'C.P. de Expedición: {}'.format(
                data['comprobante']['lugar_expedicion'])
        data['comprobante']['metododepago'] = 'Método de Pago: ({}) {}'.format(
            obj['metodo_pago'], mp[obj['metodo_pago']])
        data['comprobante']['formadepago'] = str(formapago)
        data['comprobante']['condicionesdepago'] = \
            data['comprobante']['condiciones_pago']
        data['comprobante']['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format(
            data['comprobante']['tipo_cambio'])
        data['comprobante']['totalenletras'] = util.to_letters(
            data['comprobante']['total'], data['comprobante']['moneda'])
        data['comprobante']['moneda'] = str(moneda)

        data['emisor'] = emisor
        data['receptor'] = receptor

        data['conceptos'] = PreFacturasDetalle.get_(id)
        data['totales'] = {}
        data['totales']['moneda'] = data['comprobante']['moneda']
        data['totales']['subtotal'] = str(data['comprobante']['subtotal'])
        data['totales']['total'] = str(data['comprobante']['total'])

        if obj['descuento']:
            data['totales']['descuento'] = float(obj['descuento'])

        taxes = PreFacturasImpuestos.get_(id)
        data['totales']['traslados'] = taxes['traslados']
        data['totales']['retenciones'] = taxes['retenciones']
        data['totales']['taxlocales'] = taxes['taxlocales']
        data['timbre'] = {}

        return data

    @classmethod
    def get_pdf(cls, id):
        obj = PreFacturas.get(PreFacturas.id==id)
        name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc)
        data = cls._get_info_to_pdf(cls, id)
        doc = util.to_pdf(data)
        return doc, name

    @classmethod
    def remove(cls, id):
        obj = PreFacturas.get(PreFacturas.id==id)

        q = PreFacturasDetalle.delete().where(
            PreFacturasDetalle.factura==obj)
        q.execute()
        q = PreFacturasImpuestos.delete().where(
            PreFacturasImpuestos.factura==obj)
        q.execute()
        q = PreFacturasRelacionadas.delete().where(
            PreFacturasRelacionadas.factura==obj)
        q.execute()
        return bool(obj.delete_instance())

    @classmethod
    def filter_years(cls):
        data = [{'id': -1, 'value': 'Todos'}]
        rows = (PreFacturas
            .select(PreFacturas.fecha.year)
            .group_by(PreFacturas.fecha.year)
            .order_by(PreFacturas.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_(cls, values):
        if values['year'] == '-1':
            fy = (PreFacturas.fecha.year > 0)
        else:
            fy = (PreFacturas.fecha.year == int(values['year']))
        if values['month'] == '-1':
            fm = (PreFacturas.fecha.month > 0)
        else:
            fm = (PreFacturas.fecha.month == int(values['month']))
        filters = (fy & fm)

        rows = tuple(PreFacturas
            .select(
                PreFacturas.id,
                PreFacturas.folio,
                PreFacturas.fecha,
                PreFacturas.tipo_comprobante,
                PreFacturas.total_mn,
                Socios.nombre.alias('cliente'))
            .where(filters)
            .join(Socios)
            .switch(PreFacturas).dicts()
            )
        return {'ok': True, 'rows': rows}

    def _get_folio(self, serie):
        inicio = (PreFacturas
            .select(fn.Max(PreFacturas.folio).alias('mf'))
            .where(PreFacturas.serie==serie)
            .order_by(SQL('mf'))
            .scalar())

        if inicio is None:
            inicio = 1
        else:
            inicio += 1

        return inicio

    def _calculate_totals(self, invoice, products):
        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None
        total_retenciones = None
        total_iva = 0

        for product in products:
            id_product = product.pop('id')
            p = Productos.get(Productos.id==id_product)

            product['unidad'] = p.unidad.key
            product['clave'] = p.clave
            product['clave_sat'] = p.clave_sat

            product['factura'] = invoice.id
            product['producto'] = id_product

            cantidad = float(product['cantidad'])
            valor_unitario = float(product['valor_unitario'])
            descuento = float(product['descuento'])
            precio_final = valor_unitario - descuento
            importe = round(cantidad * precio_final, DECIMALES)

            product['cantidad'] = cantidad
            product['valor_unitario'] = valor_unitario
            product['descuento'] = descuento
            product['precio_final'] = precio_final
            product['importe'] = round(cantidad * valor_unitario, DECIMALES)

            descuento_cfdi += descuento
            subtotal += importe

            PreFacturasDetalle.create(**product)
            for tax in p.impuestos:
                if tax.id in totals_tax:
                    totals_tax[tax.id].importe += importe
                else:
                    tax.importe = importe
                    totals_tax[tax.id] = tax

        for tax in totals_tax.values():
            if tax.tipo == 'E' or tax.tipo == 'R':
                continue
            import_tax = round(float(tax.tasa) * tax.importe, DECIMALES)
            total_trasladados = (total_trasladados or 0) + import_tax
            if tax.name == 'IVA':
                total_iva += import_tax

            invoice_tax = {
                'factura': invoice.id,
                'impuesto': tax.id,
                'base': tax.importe,
                'importe': import_tax,
            }
            PreFacturasImpuestos.create(**invoice_tax)

        for tax in totals_tax.values():
            if tax.tipo == 'E' or tax.tipo == 'T':
                continue
            if tax.tasa == round(Decimal(2/3), 6):
                import_tax = round(float(tax.tasa) * total_iva, DECIMALES)
            else:
                import_tax = round(float(tax.tasa) * tax.importe, DECIMALES)
            total_retenciones = (total_retenciones or 0) + import_tax

            invoice_tax = {
                'factura': invoice.id,
                'impuesto': tax['id'],
                'base': tax['importe'],
                'importe': import_tax,
            }
            PreFacturasImpuestos.create(**invoice_tax)

        total = subtotal + (total_trasladados or 0) - (total_retenciones or 0)
        total_mn = round(total * invoice.tipo_cambio, DECIMALES)
        data = {
            'subtotal': subtotal + descuento,
            'descuento': descuento_cfdi,
            'total': total,
            'total_mn': total_mn,
            'total_trasladados': total_trasladados,
            'total_retenciones': total_retenciones,
        }
        return data

    @classmethod
    def add(cls, values):
        productos = util.loads(values.pop('productos'))

        emisor = Emisor.select()[0]
        values['serie'] = 'PRE'
        values['folio'] = cls._get_folio(cls, values['serie'])
        values['tipo_cambio'] = float(values['tipo_cambio'])
        values['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal

        with database_proxy.atomic() as txn:
            obj = PreFacturas.create(**values)
            totals = cls._calculate_totals(cls, obj, productos)
            obj.subtotal = totals['subtotal']
            obj.descuento = totals['descuento']
            obj.total_trasladados = totals['total_trasladados']
            obj.total_retenciones = totals['total_retenciones']
            obj.total = totals['total']
            obj.total_mn = totals['total_mn']
            obj.save()

        msg = 'Factura guardada correctamente'
        row = {
            'id': obj.id,
            'folio': obj.folio,
            'fecha': obj.fecha,
            'tipo_comprobante': obj.tipo_comprobante,
            'total_mn': obj.total_mn,
            'cliente': obj.cliente.nombre,
        }
        data = {'ok': True, 'row': row, 'new': True, 'error': False, 'msg': msg}
        return data


class FacturasRelacionadas(BaseModel):
    factura = ForeignKeyField(Facturas, related_name='original')
    factura_origen = ForeignKeyField(Facturas, related_name='relacion')

    class Meta:
        order_by = ('factura',)

    @classmethod
    def get_(cls, invoice):
        query = (FacturasRelacionadas
            .select()
            .where(FacturasRelacionadas.factura==invoice)
        )
        return [str(r.factura_origen.uuid) for r in query]


class FacturasComplementos(BaseModel):
    factura = ForeignKeyField(Facturas)
    nombre = TextField(default='')
    valores = TextField(default='')

    class Meta:
        order_by = ('factura',)

    @classmethod
    def get_(cls, factura):
        query = (FacturasComplementos
            .select()
            .where(FacturasComplementos.factura==factura)
        )
        return {r.nombre: util.loads(r.valores) for r in query}


# ~ class CfdiPagosFacturas(BaseModel):
    # ~ pago = ForeignKeyField(CfdiPagos)
    # ~ 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 = ('pago',)
        # ~ indexes = (
            # ~ (('pago', 'factura', 'numero'), 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, 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='')
    clave_sat = TextField(default='')
    categoria = TextField(default='')
    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 PreFacturasDetalle(BaseModel):
    factura = ForeignKeyField(PreFacturas)
    producto = ForeignKeyField(Productos, null=True)
    descripcion = TextField(default='')
    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',)

    def _get_impuestos(self, id):
        model_pt = Productos.impuestos.get_through_model()
        impuestos = tuple(model_pt
            .select(
                model_pt.productos_id.alias('product'),
                model_pt.satimpuestos_id.alias('tax'))
            .where(model_pt.productos_id==id).dicts())
        return impuestos

    @classmethod
    def facturar(cls, id):
        data = []

        q = PreFacturas.select(PreFacturas.cliente).where(PreFacturas.id==id)[0]
        receptor = {
            'id': q.cliente.id,
            'nombre': q.cliente.nombre,
            'rfc': q.cliente.rfc,
            'forma_pago': q.cliente.forma_pago.key,
            'uso_cfdi': q.cliente.uso_cfdi.key,
        }

        productos = PreFacturasDetalle.select().where(
            PreFacturasDetalle.factura==id)

        for p in productos:
            row = {'id': p.producto.id}
            row['clave'] = p.producto.clave
            row['descripcion'] = p.descripcion
            row['unidad'] = p.producto.unidad.name
            row['cantidad'] = p.cantidad
            row['valor_unitario'] = p.valor_unitario
            row['descuento'] = p.descuento
            pf = p.valor_unitario - p.descuento
            row['importe'] = round(pf * p.cantidad, DECIMALES)
            impuestos = cls._get_impuestos(cls, row['id'])
            data.append({'row': row, 'taxes': impuestos})

        return {'rows': data, 'receptor': receptor}

    @classmethod
    def get_(cls, id):
        data = []

        productos = PreFacturasDetalle.select().where(
            PreFacturasDetalle.factura==id)

        for p in productos:
            producto = {}
            producto['noidentificacion'] = '{}\n(SAT {})'.format(
                p.producto.clave, p.producto.clave_sat)
            producto['descripcion'] = p.descripcion
            producto['unidad'] = '{}\n({})'.format(
                p.producto.unidad.name, p.producto.unidad.key)
            producto['cantidad'] = str(p.cantidad)
            producto['valorunitario'] = str(p.valor_unitario)
            producto['importe'] = str(p.importe)
            data.append(producto)

        return data


class FacturasImpuestos(BaseModel):
    factura = ForeignKeyField(Facturas)
    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',)
        indexes = (
            (('factura', 'impuesto'), True),
        )


class FacturasPagos(BaseModel):
    movimiento = ForeignKeyField(MovimientosBanco)
    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)
    # ~ cancelado = BooleanField(default=False)

    class Meta:
        order_by = ('factura',)
        indexes = (
            (('movimiento', 'factura', 'numero'), True),
        )

    def _movimiento_anterior(self, mov, id):
        query = (FacturasPagos
            .select()
            .where(FacturasPagos.factura==id)
        )
        if len(query):
            return query[-1], len(query) + 1
        else:
            return None, 1

    def _actualizar_saldo_cliente(self, cliente, importe):
        q = (Socios
            .update(saldo_cliente=Socios.saldo_cliente + importe)
            .where(Socios.id==cliente.id)
        )
        return bool(q.execute())

    def _actualizar_saldos(self, factura, saldo_anterior):
        query = (FacturasPagos
            .select()
            .where(FacturasPagos.factura==factura)
        )
        saldo = saldo_anterior
        for i, row in enumerate(query):
            if not saldo_anterior:
                saldo_anterior = row.saldo_anterior
            row.numero = i + 1
            row.saldo_anterior = saldo_anterior
            row.saldo = saldo_anterior - row.importe
            row.save()
            saldo_anterior = row.saldo
            saldo = row.saldo

        factura.saldo = saldo
        factura.pagada = False
        factura.save()
        return

    @classmethod
    def cancelar(cls, mov):
        query = (FacturasPagos
            .select()
            .where(FacturasPagos.movimiento==mov)
        )
        for row in query:
            cls._actualizar_saldo_cliente(cls, row.factura.cliente, row.importe)
            factura = row.factura
            saldo_anterior = 0
            if row.numero == 1:
                saldo_anterior = row.saldo_anterior
            row.delete_instance()
            cls._actualizar_saldos(cls, factura, saldo_anterior)
        return

    @classmethod
    def add(cls, mov, ids):
        for i, importe in ids.items():
            fac = Facturas.get(Facturas.id==int(i))
            mov_ant, numero = cls._movimiento_anterior(cls, mov, fac)
            nuevo = {
                'movimiento': mov,
                'factura': fac,
                'numero': numero,
                'importe': importe,
            }
            if mov_ant is None:
                nuevo['saldo_anterior'] = float(fac.saldo)
            else:
                nuevo['saldo_anterior'] = float(mov_ant.saldo)
            nuevo['saldo'] = nuevo['saldo_anterior'] - importe
            FacturasPagos.create(**nuevo)
            fac.saldo = nuevo['saldo']
            if nuevo['saldo'] == 0:
                fac.pagada = True
            fac.save()
            cls._actualizar_saldo_cliente(cls, fac.cliente, importe * -1)
        return


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',)
        indexes = (
            (('factura', 'impuesto'), True),
        )

    @classmethod
    def get_(cls, id):
        data = {
            'traslados': [],
            'retenciones': [],
            'taxlocales': [],
        }

        taxes = PreFacturasImpuestos.select().where(
            PreFacturasImpuestos.factura==id)

        for tax in taxes:
            if tax.impuesto.tipo == 'T':
                title = 'Traslado {} {}'.format(
                    tax.impuesto.name, str(tax.impuesto.tasa))
                data['traslados'].append((title, str(tax.importe)))
            elif tax.impuesto.tipo == 'R':
                title = 'Retención {} {}'.format(
                    tax.impuesto.name, str(tax.impuesto.tasa))
                data['retenciones'].append((title, str(tax.importe)))

        return data


def authenticate(args):
    respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''}
    values = util.get_con(args['rfc'])
    if not values:
        return respuesta

    conectar(values)
    try:
        obj = Usuarios.get(usuario=args['usuario'], es_activo=True)
    except Usuarios.DoesNotExist:
        return respuesta

    if not obj.contraseña.check_password(args['contra']):
        return respuesta

    obj.ultimo_ingreso = util.now()
    obj.save()
    respuesta['msg'] = ''
    respuesta['login'] = True
    respuesta['user'] = str(obj)
    respuesta['super'] = obj.es_superusuario
    #~ respuesta['admin'] = obj.es_superusuario or obj.es_admin
    return respuesta, obj


def get_cp(cp):
    con = sqlite3.connect(PATH_CP)
    cursor = con.cursor()
    sql = """
        SELECT colonia, municipio, estado
        FROM colonias, municipios, estados
        WHERE colonias.id_municipio=municipios.id
            AND municipios.id_estado=estados.id
            AND cp=?
        ORDER BY colonia"""
    cursor.execute(sql, (cp,))
    rows = cursor.fetchall()
    cursor.close()
    con.close()

    data = {}
    if rows:
        data = {
            'estado': rows[0][2],
            'municipio': rows[0][1],
        }
        if len(rows) == 1:
            data['colonia'] = rows[0][0]
        else:
            data['colonia'] = [r[0] for r in rows]
    return data


def get_sat_key(key):
    return util.get_sat_key('productos', key)


def get_sat_unidades(key):
    return util.get_sat_unidades(key)

def get_sat_productos(key):
    return util.get_sat_productos(key)


def test_correo(values):
    server = {
        'servidor': values['correo_servidor'],
        'puerto': values['correo_puerto'],
        'ssl': bool(values['correo_ssl'].replace('0', '')),
        'usuario': values['correo_usuario'],
        'contra': values['correo_contra'],
    }
    options = {
        'para': values['correo_usuario'],
        'copia': values['correo_copia'],
        'asunto': values['correo_asunto'],
        'mensaje': values['correo_mensaje'].replace('\n', '<br/>'),
        'files': [],
    }
    data= {
        'server': server,
        'options': options,
    }
    return util.send_mail(data)


def _init_values(rfc):
    data = (
        {'clave': 'version', 'valor': VERSION},
        {'clave': 'migracion', 'valor': '0'},
        {'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:
            with database_proxy.atomic() as txn:
                Configuracion.create(**row)
        except IntegrityError:
            pass

    if not Certificado.select().count():
        Certificado.create(rfc=rfc)

    log.info('Valores iniciales insertados...')
    return


def _crear_tablas(rfc):
    tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion,
        Folios, Registro,
        Emisor, Facturas, FacturasDetalle, FacturasImpuestos, FacturasPagos,
        FacturasRelacionadas, FacturasComplementos, Almacenes, Productos,
        PreFacturas, PreFacturasDetalle, PreFacturasImpuestos,
        PreFacturasRelacionadas,
        SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes,
        SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos,
        Socios, Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco,
        CfdiPagos,
        Emisor.regimenes.get_through_model(),
        Socios.tags.get_through_model(),
        Productos.impuestos.get_through_model(),
        Productos.tags.get_through_model(),
    ]
    log.info('Creando tablas...')
    database_proxy.create_tables(tablas, True)
    log.info('Tablas creadas correctamente...')

    usuarios = (
        {'usuario': 'superadmin', 'contraseña': DEFAULT_PASSWORD,
            'es_superusuario': True},
        {'usuario': 'admin', 'contraseña': DEFAULT_PASSWORD, 'es_admin': True},
    )

    for usuario in usuarios:
        try:
            with database_proxy.atomic() as txn:
                Usuarios.create(**usuario)
                log.info('Usuario creado correctamente...')
        except IntegrityError:
            log.info('Usuario ya existe...')

    _init_values(rfc)
    _importar_valores('', rfc)

    return True


def migrate_tables():
    connect()
    log.info('Tablas migradas correctamente...')
    return


def _agregar_superusuario():
    args = util.get_con()
    if not args:
        return

    conectar(args)
    usuario = input('Introduce el nuevo nombre para el superusuario: ').strip()
    if not usuario:
        msg = 'El nombre de usuario es requerido'
        log.erro(msg)
        return
    ok, contraseña = util.get_pass()
    if not ok:
        log.error(contraseña)
        return
    try:
        obj = Usuarios.create(
            usuario=usuario, contraseña=contraseña, es_superusuario=True)
    except IntegrityError:
        msg = 'El usuario ya existe'
        log.error(msg)
        return

    log.info('SuperUsuario creado correctamente...')
    return


def _cambiar_contraseña():
    args = util.get_con()
    if not args:
        return

    conectar(args)
    usuario = input('Introduce el nombre de usuario: ').strip()
    if not usuario:
        msg = 'El nombre de usuario es requerido'
        log.error(msg)
        return

    try:
        obj = Usuarios.get(usuario=usuario)
    except Usuarios.DoesNotExist:
        msg = 'El usuario no existe'
        log.error(msg)
        return

    ok, contraseña = util.get_pass()
    if not ok:
        log.error(contraseña)
        return

    obj.contraseña = contraseña
    obj.save()

    log.info('Contraseña cambiada correctamente...')
    return


def _add_emisor(rfc, args):
    util._valid_db_companies()
    con = sqlite3.connect(COMPANIES)
    cursor = con.cursor()
    sql = """
        INSERT INTO names
        VALUES (?, ?)"""
    try:
        cursor.execute(sql, (rfc, args))
    except sqlite3.IntegrityError as e:
        log.error(e)
        return False

    con.commit()
    cursor.close()
    con.close()
    return True


def _delete_emisor(rfc):
    util._valid_db_companies()
    con = sqlite3.connect(COMPANIES)
    cursor = con.cursor()
    sql = """
        DELETE FROM names
        WHERE rfc = ?"""
    try:
        cursor.execute(sql, (rfc,))
    except Exception as e:
        log.error(e)
        return False

    con.commit()
    cursor.close()
    con.close()
    return True


def _iniciar_bd():
    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)
    if _crear_tablas(rfc):
        return

    log.error('No se pudieron crear las tablas')
    return


def _agregar_rfc():
    rfc = input('Introduce el nuevo RFC: ').strip().upper()
    if not rfc:
        msg = 'El RFC es requerido'
        log.error(msg)
        return

    datos = input('Introduce los datos de conexión: ').strip()
    if not datos:
        msg = 'Los datos de conexión son requeridos'
        log.error(msg)
        return

    opt = util.parse_con(datos)
    if not opt:
        log.error('Datos de conexión incompletos')
        return

    args = opt.copy()
    if conectar(args):
        if _add_emisor(rfc, util.dumps(opt)) and _crear_tablas(rfc):
            log.info('RFC agregado correctamente...')
            return

    log.error('No se pudo agregar el RFC')
    return


def _borrar_rfc():
    rfc = input('Introduce el RFC a borrar: ').strip().upper()
    if not rfc:
        msg = 'El RFC es requerido'
        log.error(msg)
        return

    confirm = input('¿Estás seguro de borrar el RFC?')

    if _delete_emisor(rfc):
        log.info('RFC borrado correctamente...')
        return

    log.error('No se pudo borrar el RFC')
    return


def _listar_rfc():
    data = util.get_rfcs()
    for row in data:
        msg = 'RFC: {}\n\t{}'.format(row[0], row[1])
        log.info(msg)
    return

def _importar_valores(archivo='', rfc=''):
    if not rfc:
        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)

    if not archivo:
        archivo = INIT_VALUES

    log.info('Importando datos...')
    regimen = ''
    rows = util.loads(open(archivo, 'r').read())
    for row in rows:
        log.info('\tImportando tabla: {}'.format(row['tabla']))
        table = globals()[row['tabla']]
        for r in row['datos']:
            try:
                with database_proxy.atomic() as txn:
                    table.create(**r)
            except IntegrityError:
                pass

    log.info('Importación terminada...')
    return


def _importar_socios(rows):
    log.info('\tImportando Clientes...')
    for row in rows:
        try:
            with database_proxy.atomic() as txn:
                Socios.create(**row)
        except IntegrityError:
            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:
        try:
            detalles = row.pop('detalles')
            impuestos = row.pop('impuestos')
            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
                    FacturasDetalle.create(**detalle)
                for impuesto in impuestos:
                    imp = SATImpuestos.get(**impuesto['filtro'])
                    new = {
                        'factura': obj,
                        'impuesto': imp,
                        'importe': impuesto['importe'],
                    }
                    FacturasImpuestos.create(**new)
        except IntegrityError as 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_bdfl():
    try:
        emisor = Emisor.select()[0]
    except IndexError:
        msg = 'Configura primero al emisor'
        return {'ok': False, 'msg': msg}

    name = '{}.sqlite'.format(emisor.rfc.lower())
    path = util._join('/tmp', name)

    log.info('Importando datos...')
    app = util.ImportFacturaLibre(path, emisor.rfc)
    if not app.is_connect:
        msg = app._error
        log.error('\t{}'.format(msg))
        return {'ok': False, 'msg': msg}

    data = app.import_data()

    _importar_socios(data['Socios'])
    _importar_facturas(data['Facturas'])
    _importar_categorias(data['Categorias'])

    msg = 'Importación terminada...'
    log.info(msg)

    return {'ok': True, 'msg': msg}


def _importar_factura_libre(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

    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'
help_superuser = 'Crea un nuevo super usuario'
help_change_pass = 'Cambia la contraseña a un usuario'
help_rfc = 'Agrega un nuevo RFC'
help_br = 'Elimina un RFC'
help_lr = 'Listar RFCs'

@click.command(context_settings=CONTEXT_SETTINGS)
@click.option('-bd', '--iniciar-bd',help=help_create_tables,
    is_flag=True, default=False)
@click.option('-m', '--migrar-bd', help=help_migrate_db,
    is_flag=True, default=False)
@click.option('-ns', '--nuevo-superusuario', help=help_superuser,
    is_flag=True, default=False)
@click.option('-cc', '--cambiar-contraseña', help=help_change_pass,
    is_flag=True, default=False)
@click.option('-rfc', '--rfc', help=help_rfc, is_flag=True, default=False)
@click.option('-br', '--borrar-rfc', help=help_br, is_flag=True, default=False)
@click.option('-lr', '--listar-rfc', help=help_lr, is_flag=True, default=False)
@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, test,
    generar_archivo_productos, importar_productos):

    opt = locals()

    if opt['test']:
        _test()
        sys.exit(0)

    if opt['iniciar_bd']:
        _iniciar_bd()
        sys.exit(0)

    if opt['migrar_bd']:
        migrate_tables()
        sys.exit(0)

    if opt['nuevo_superusuario']:
        _agregar_superusuario()
        sys.exit(0)

    if opt['cambiar_contraseña']:
        _cambiar_contraseña()
        sys.exit(0)

    if opt['rfc']:
        _agregar_rfc()
        sys.exit(0)

    if opt['borrar_rfc']:
        _borrar_rfc()
        sys.exit(0)

    if opt['listar_rfc']:
        _listar_rfc()
        sys.exit(0)

    if opt['importar_valores']:
        if not opt['archivo']:
            msg = 'Falta la ruta del archivo importar'
            raise click.ClickException(msg)
        if not util.is_file(opt['archivo']):
            msg = 'No es un archivo'
            raise click.ClickException(msg)

        _importar_valores(opt['archivo'])
        sys.exit(0)

    if opt['factura_libre']:
        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)

        _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


if __name__ == '__main__':
    main()

