#!/usr/bin/env python3

# ~ Empresa Libre
# ~ Copyright (C) 2016-2021  Mauricio Baeza Servin (public@correolibre.net)
# ~
# ~ This program is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.
# ~
# ~ This program is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# ~ GNU General Public License for more details.
# ~
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.


import argparse
from decimal import Decimal
import sqlite3
from peewee import *
from playhouse.fields import PasswordField, ManyToManyField
from playhouse.shortcuts import case, SQL, cast
from psycopg2.errors import UniqueViolation


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, COMPANIES, VERSION, PATH_CP, PRE, \
    INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \
    CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, \
    DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \
    DEFAULT_CFDIPAY, CURRENCY_MN

# ~ v2
from controllers import utils
from settings import (
    DEBUG,
    CANCEL_VERSION,
    CARTA_PORTE,
    DEFAULT_GLOBAL,
    DB_COMPANIES,
    EXT,
    IS_MV,
    MXN,
    PATHS,
    PRE_DEFAULT,
    URL,
    VALUES_PDF,
    VERSION as VERSION_EMPRESA_LIBRE,
    RESICO,
    RFCS,
)

FORMAT = '{0:.2f}'
FORMAT3 = '{0:.3f}'
FORMAT4 = '{0:.4f}'
FORMAT6 = '{0:.6f}'
FORMAT_TAX = FORMAT4
FORMAT_PRECIO = FORMAT4
# ~ RFC_PUBLICO = 'XAXX010101000'
RFC_EXTRANJERO = 'XEXX010101000'


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


def conectar(opt):
    db = {
        'sqlite': SqliteDatabase,
        'postgres': PostgresqlDatabase,
    }
    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

    opt['host'] = opt.get('host', 'localhost')
    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


class UploadFile(object):

    def _read_productsadd(self, result):
        emisor = result['data']['emisor']
        rfc = emisor['rfc']

        where = ((Socios.rfc == rfc) & (Socios.es_proveedor == True))
        try:
            partner = Socios.select(Socios.id).where(where).get()
            emisor['id'] = partner.id
        except Socios.DoesNotExist:
            emisor['id'] = 0

        result['data']['emisor'] = emisor

        return result

    @classmethod
    def read(cls, rfc, opt, file_obj):
        result = utils.upload_file(rfc, opt, file_obj)
        method = f'_read_{opt}'
        if hasattr(cls, method):
            return getattr(cls, method)(cls, result)

        return result


def upload_file(rfc, opt, file_obj):
    if opt == 'cfdixml':
        sxml = file_obj.file.read().decode()
        xml = util.parse_xml(sxml)
        # ~ sxml = util.to_pretty_xml(data)
        if xml is None:
            return {'status': 'error'}
        else:
            return Facturas.import_cfdi(xml, sxml)

    if opt == 'emisorlogo':
        return Emisor.save_logo(file_obj)

    # ~ v2
    names = ('productsadd', '_3.3_cp_2.0.ods')
    if opt in names:
        result = UploadFile.read(rfc, opt, file_obj)
        return result

    result = util.upload_file(rfc, opt, file_obj)
    if result['ok']:
        names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods', 'ceods')
        if not opt in names:
            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.get(Certificado.es_fiel==False)
    except Exception as e:
        return {'ok': False, 'msg': msg}

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

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

    auth = Configuracion.get_({'fields': 'pac_auth'})
    if not auth:
        msg = 'Es necesario configurar los datos de timbrado del PAC'
        return {'ok': False, 'msg': msg}

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

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



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


def import_invoice():
    log.info('Importando factura...')
    emisor = Emisor.select()[0]
    rows, msg = util.import_invoice(emisor.rfc)
    if not rows:
        return {'ok': False, 'msg': msg}

    products = []
    for i, row in enumerate(rows):
        try:
            if not isinstance(row[0], str):
                msg = 'Fila: {} - La clave debe ser TEXTO'.format(i+1)
                return {'ok': False, 'msg': msg}

            obj = Productos.get(Productos.clave==row[0])

            if isinstance(row[2], str):
                msg = 'El Precio Unitario debe ser un número, debe ser 0.00, si no quieres cambiarlo'
                return {'ok': False, 'msg': msg}

            vu = round(row[2], 2)
            if vu <= 0:
                vu = float(obj.valor_unitario)

            if isinstance(row[3], str):
                msg = 'El descuento debe ser un número, debe ser 0.00, si no tiene'
                return {'ok': False, 'msg': msg}

            descuento = round(row[3], 2)
            cant = round(row[4], 2)
            description = row[1].strip()
            pedimento = row[5].strip()
            pf =  vu - descuento
            p = {
                'id_product': obj.id,
                'delete': '-',
                'clave': obj.clave,
                'clave_sat': obj.clave_sat,
                'descripcion': description or obj.descripcion,
                'pedimento': pedimento,
                'unidad': obj.unidad.id,
                'cantidad': cant,
                'valor_unitario': vu,
                'descuento': descuento,
                'importe': round(pf * cant, DECIMALES),
                'taxes': _get_taxes_product(obj.id),
            }
            products.append(p)
        except Productos.DoesNotExist:
            pass

    log.info('Factura importada...')
    return {'ok': True, 'rows': tuple(products)}


def import_ceods():
    log.info('Importando plantilla...')
    emisor = Emisor.select()[0]
    data, msg = util.import_ceods(emisor.rfc)

    if not data:
        return {'ok': False, 'msg': msg}

    log.info('Plantilla importada...')
    return {'ok': True, 'data': data}


def get_doc(type_doc, id, rfc):
    types = {
        'xml': 'application/xml',
        'xmlpago': 'application/xml',
        'nomxml': 'application/xml',
        'ods': 'application/octet-stream',
        'zip': 'application/octet-stream',
        'nomlog': 'application/txt',
        'html': 'text/html',
    }
    content_type = types.get(type_doc, 'application/pdf')
    if type_doc == 'xml':
        data, file_name = Facturas.get_xml(id)
    elif type_doc == 'pdf':
        data, file_name = Facturas.get_pdf(id, rfc)
    elif type_doc == 'html':
        data, file_name = Facturas.get_html(id)
    elif type_doc == 'ods':
        data, file_name = Facturas.get_ods(id, rfc)
    elif type_doc == 'zip':
        data, file_name = Facturas.get_zip(id, rfc)
    elif type_doc == 'pre':
        data, file_name = PreFacturas.get_pdf(id)
    elif type_doc == 'tpdf':
        data, file_name = Tickets.get_pdf(id)
    elif type_doc == 'nomxml':
        data, file_name = CfdiNomina.get_xml(id)
    elif type_doc == 'nomlog':
        data, file_name = util.get_log('nomina')
    elif type_doc == 'nompdf':
        data, file_name = CfdiNomina.get_pdf(id, rfc)
    elif type_doc == 'xmlpago':
        data, file_name = CfdiPagos.get_file_xml(id)
    elif type_doc == 'pdfpago':
        data, file_name = CfdiPagos.get_file_pdf(id)

    return data, file_name, content_type


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

    punto_de_venta = Configuracion.get_bool('chk_usar_punto_de_venta')

    nomina = Configuracion.get_bool('chk_usar_nomina')
    if not user.es_admin:
        nomina = False

    data = {
        'empresa': get_title_app(3),
        'punto_de_venta': punto_de_venta,
        'escuela': False,
        'nomina': nomina,
        'timbres': 0,
        'decimales_precios': DECIMALES,
        'pagos': Configuracion.get_bool('chk_config_pagos'),
        'pays_data_bank': Configuracion.get_bool('chk_cfg_pays_data_bank'),
        'show_filter_by_day': Configuracion.get_bool('chk_config_show_filter_by_day'),
    }
    dp = Configuracion.get_bool('chk_config_decimales_precios')
    if dp:
        data['decimales_precios'] = DECIMALES_PRECIOS

    if not obj is None:
        titulo = '{} - <b><font color="#610B0B">{}</font></b>'
        data['empresa'] = titulo.format(data['empresa'], obj.nombre)
        data['escuela'] = obj.es_escuela
        data['timbres'] = Emisor.get_timbres()
        data['multi_currency'] = SATMonedas.get_multi_currency()

    return data


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

    conf = {
        'cfdi_donativo': obj.es_ong,
        'cfdi_anticipo': Configuracion.get_('chk_config_anticipo'),
        'cfdi_ine': Configuracion.get_bool('chk_config_ine'),
        'cfdi_edu': Configuracion.get_bool('chk_config_edu'),
        'cfdi_carta_porte': Configuracion.get_bool('chk_config_carta_porte'),
        'cfdi_comercioe': Configuracion.get_bool('chk_config_comercio_exterior'),
        'cfdi_divisas': Configuracion.get_bool('chk_config_divisas'),
        'cfdi_metodo_pago': Configuracion.get_bool('chk_config_ocultar_metodo_pago'),
        'cfdi_condicion_pago': Configuracion.get_bool('chk_config_ocultar_condiciones_pago'),
        'cfdi_open_pdf': Configuracion.get_bool('chk_config_open_pdf'),
        'cfdi_show_pedimento': Configuracion.get_bool('chk_config_show_pedimento'),
        'cfdi_tax_locales': Configuracion.get_bool('chk_config_tax_locales'),
        'cfdi_tax_decimals': Configuracion.get_bool('chk_config_tax_decimals'),
        'cfdi_with_taxes': Configuracion.get_bool('chk_config_price_with_taxes_in_invoice'),
        'cfdi_add_same_product': Configuracion.get_bool('chk_config_add_same_product'),
        'cfdi_tax_locales_truncate': Configuracion.get_bool('chk_config_tax_locales_truncate'),
        'cfdi_folio_custom': Configuracion.get_bool('chk_folio_custom'),
        'cfdi_leyendasfiscales': Configuracion.get_bool('chk_config_leyendas_fiscales'),
        'cfdi_show_total_cant': Configuracion.get_bool('chk_config_show_total_cant'),
        'cfdi_change_date_invoice': Configuracion.get_bool('chk_config_change_date_invoice'),
    }

    return conf


def config_ticket():
    conf = {
        'open_pdf': Configuracion.get_bool('chk_ticket_pdf_show'),
        'direct_print': Configuracion.get_bool('chk_ticket_direct_print'),
        'edit_cant': Configuracion.get_bool('chk_ticket_edit_cant'),
        'total_up': Configuracion.get_bool('chk_ticket_total_up'),
    }
    return conf


class Configuracion(BaseModel):
    clave = TextField(unique=True)
    valor = TextField(default='')

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

    def __str__(self):
        return '{} = {}'.format(self.clave, self.valor)

    def _clean_email(self, values={}):
        fields = (
            'correo_asunto',
            'correo_confirmacion',
            'correo_contra',
            'correo_directo',
            'correo_mensaje',
            'correo_puerto',
            'correo_servidor',
            'correo_ssl',
            'correo_usuario',
        )

        q = (Configuracion
            .update(**{'valor': ''})
            .where(Configuracion.clave.in_(fields))
        )
        result = q.execute()
        msg = 'Configuración guardada correctamente'
        if not result:
            msg = 'No se pudo guardar la configuración'
        return {'ok': result, 'msg': msg}

    def _save_mail(self, values):
        rfc = Emisor.select()[0].rfc
        values['correo_contra'] = utils.encrypt(values['correo_contra'], rfc)

        for k, v in values.items():
            obj, created = Configuracion.get_or_create(clave=k)
            obj.valor = v
            obj.save()

        return {'ok': True}

    @classmethod
    def get_bool(cls, key):
        data = (Configuracion
            .select(Configuracion.valor)
            .where(Configuracion.clave == key)
        )
        if data and data[0].valor == '1':
            return True
        return False

    def _get_partners(self, args={}):
        fields = (
            'chk_config_change_balance_partner',
        )
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: util.get_bool(r.valor) for r in data}
        return values


    def _get_admin_products(self, args={}):
        fields = (
            'chk_config_cuenta_predial',
            'chk_config_codigo_barras',
            'chk_config_precio_con_impuestos',
            'chk_llevar_inventario',
            'chk_multi_stock',
            # ~ 'chk_use_packing',
        )
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: util.get_bool(r.valor) for r in data}
        if not 'chk_multi_stock' in values:
            values['chk_multi_stock'] = False
        return values

    def _get_main_products(self, args={}):
        fields = (
            'chk_config_cuenta_predial',
            'chk_config_codigo_barras',
            'chk_config_precio_con_impuestos',
            'chk_llevar_inventario',
            # ~ 'chk_use_packing',
            'chk_multi_stock',
        )
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: r.valor for r in data}
        values['default_tax'] = SATImpuestos.select()[0].id
        values['default_unidad'] = SATUnidades.get_default()
        return values

    def _get_complements(self, args={}):
        fields = (
            'chk_config_ine',
            'chk_config_edu',
            'chk_config_carta_porte',
            'chk_config_comercio_exterior',
            'chk_config_pagos',
            'chk_config_divisas',
            'chk_cfg_pays_data_bank',
            'chk_usar_nomina',
            'chk_config_leyendas_fiscales',
        )
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: util.get_bool(r.valor) for r in data}

        fields = (
            'txt_ticket_printer',
            'txt_config_nomina_serie',
            'txt_config_nomina_folio',
            'txt_config_cfdipay_serie',
            'txt_config_cfdipay_folio',
        )
        for f in fields:
            values[f] = Configuracion.get_(f)

        return values

    def _get_folios(self, args={}):
        fields = (
            'chk_folio_custom',
        )
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: util.get_bool(r.valor) for r in data}
        return values

    def _get_correo(self, args={}):
        fields = ('correo_servidor', 'correo_puerto',
            'correo_ssl', 'correo_starttls',
            'correo_usuario', 'correo_copia', 'correo_asunto',
            'correo_mensaje', 'correo_directo', 'correo_confirmacion')
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: r.valor for r in data}
        return values

    def _get_admin_config_users(self, args={}):
        fields = (
            'chk_users_notify_access',
        )
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        values = {r.clave: util.get_bool(r.valor) for r in data}
        return values

    @classmethod
    def get_value(cls, key, default=''):
        value = default
        data = (Configuracion
            .select(Configuracion.valor)
            .where(Configuracion.clave == key)
        )
        if data:
            value = data[0].valor
        return value

    def _get_pac(cls, pac):
        user_field = f'user_timbrado_{pac}'
        token_field = f'token_timbrado_{pac}'
        fields = (user_field, token_field)
        data = (Configuracion
            .select()
            .where(Configuracion.clave.in_(fields))
        )
        data = {r.clave: r.valor for r in data}
        values = {
            'user_timbrado': data.get(user_field, ''),
            'token_timbrado': data.get(token_field, ''),
        }
        return values

    def _get_pac_auth(cls, args={}):
        pac = cls.get_('lst_pac').lower()
        user = cls.get_(f'user_timbrado_{pac}')
        token = cls.get_(f'token_timbrado_{pac}')
        data = {}
        if pac and user and token:
            data['pac'] = pac
            data['user'] = user
            data['pass'] = token
        return data

    def _get_auth_by_pac(cls, args):
        pac = args['pac']
        user = cls.get_(f'user_timbrado_{pac}')
        token = cls.get_(f'token_timbrado_{pac}')
        data = {}
        if pac and user and token:
            data['pac'] = pac
            data['user'] = user
            data['pass'] = token
        return data

    def _get_admin_sucursales(cls, args):
        values = {
            'folios': Folios.get_for_sucursales(),
            'warehouse': Almacenes.get_for_sucursales(),
            'multi_stock': Configuracion.get_bool('chk_multi_stock')
        }
        return values

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

        options = ('partners',
            'admin_products',
            'main_products',
            'complements',
            'folios',
            'correo',
            'admin_config_users',
            'pac_auth',
            'auth_by_pac',
            'admin_sucursales',
        )
        opt = keys['fields']

        if opt in options:
            return getattr(cls, f'_get_{opt}')(cls, keys)

        if opt == 'pac':
            return cls._get_pac(cls, keys['pac'])

        if keys['fields'] == 'configtemplates':
            try:
                emisor = Emisor.select()[0]
                is_ong = emisor.es_ong
            except IndexError:
                is_ong = False

            values = {'txt_plantilla_donataria': is_ong}
            fields = (
                ('chk_usar_punto_de_venta', 'txt_plantilla_ticket'),
            )

            for s, key in fields:
                value = util.get_bool(Configuracion.get_(s))
                values[key] = value

            return values

        if keys['fields'] == 'configotros':
            fields = (
                'chk_config_ocultar_metodo_pago',
                'chk_config_ocultar_condiciones_pago',
                'chk_config_send_zip',
                'chk_config_open_pdf',
                'chk_config_show_pedimento',
                'chk_config_tax_locales',
                'chk_config_tax_decimals',
                'chk_config_price_with_taxes_in_invoice',
                'chk_config_add_same_product',
                'chk_config_tax_locales_truncate',
                'chk_config_decimales_precios',
                'chk_config_user_show_doc',
                'chk_config_anticipo',
                'chk_usar_punto_de_venta',
                'chk_ticket_pdf_show',
                'chk_ticket_direct_print',
                'chk_ticket_edit_cant',
                'chk_ticket_total_up',
                'chk_ticket_user_show_doc',
                'chk_config_invoice_by_ticket',
                'chk_config_show_total_cant',
                'chk_cancel_invoices_by_admin',
                'chk_config_change_date_invoice',
                'chk_cancel_tickets_by_admin',
                'chk_config_show_filter_by_day',
            )
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )
            values = {r.clave: util.get_bool(r.valor) for r in data}
            fields = (
                ('lst_pac', 'comercio'),
            )
            for k, d in fields:
                values[k] = Configuracion.get_value(k, d)
            return values

        if 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_html',
                'txt_plantilla_factura_css',
                'txt_plantilla_factura_json',
                'txt_plantilla_nomina1233',
                'txt_plantilla_pagos10',
                'txt_plantilla_ticket',
                'txt_plantilla_donataria',
                'make_pdf_from',
            )
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )
        elif keys['fields'] == 'timbrar':
            fields = (
                'chk_config_ocultar_metodo_pago',
                'chk_config_ocultar_condiciones_pago',
                'chk_config_anticipo',
                'chk_config_ine',
                'chk_config_open_pdf',
            )
            data = (Configuracion
                .select()
                .where(Configuracion.clave.in_(fields))
            )

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

    def _save_pac(cls, values):
        pac = values['lst_pac']
        user = values['user_timbrado']
        token = values['token_timbrado']

        data = {
            'lst_pac': pac,
            f'user_timbrado_{pac}': user,
            f'token_timbrado_{pac}': token,
        }

        for k, v in data.items():
            obj, _ = Configuracion.get_or_create(clave=k)
            obj.valor = v
            obj.save()

        return {'ok': True}

    @classmethod
    def add(cls, values):
        opt = values.pop('opt', '')
        if opt:
            return getattr(cls, f'_{opt}')(cls, values)

        # ~ print (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)}

    @classmethod
    def remove(cls, key):
        q = Configuracion.delete().where(Configuracion.clave==key)
        return bool(q.execute())


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

    class Meta:
        order_by = ('tag',)


class TipoDireccion(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoTitulo(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoTelefono(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoCorreo(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoPariente(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoResponsable(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoMovimientoAlumno(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class TipoMovimientoAlmacen(BaseModel):
    nombre = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)

    def __str__(self):
        return self.nombre


class Roles(BaseModel):
    nombre = TextField(default='')

    class Meta:
        order_by = ('nombre',)


class Permisos(BaseModel):
    rol = ForeignKeyField(Roles)
    modulo = TextField(default='')
    ver = BooleanField(default=False)
    agregar = BooleanField(default=False)
    editar = BooleanField(default=False)
    eliminar = BooleanField(default=False)
    especiales = TextField(default='')

    class Meta:
        order_by = ('rol', 'modulo')
        indexes = (
            (('rol', 'modulo'), True),
        )


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_by_key(cls, key):
        return SATRegimenes.get(SATRegimenes.key==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)

    @classmethod
    def _get_actives(cls, filters, user):
        where = ((SATRegimenes.activo==True) & (SATRegimenes.fisica==True))
        if (filters['morales']=='true'):
            where = ((SATRegimenes.activo==True) & (SATRegimenes.moral==True))

        rows = (SATRegimenes
            .select(
                SATRegimenes.id,
                fn.Concat("[", SATRegimenes.key, "] ", SATRegimenes.name).alias('value')
            )
            .where(where)
            .dicts()
        )
        return tuple(rows)

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

    @classmethod
    def _get_all(cls, filters, user):
        rows = SATRegimenes.select().dicts()
        return tuple(rows)

    @classmethod
    def get_data(cls, filters, user):
        opt = filters['opt']
        return getattr(cls, f'_get_{opt}')(filters, user)


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='')
    registro_patronal = TextField(default='')
    regimenes = ManyToManyField(SATRegimenes, related_name='emisores')
    hours = IntegerField(default=0)

    class Meta:
        order_by = ('nombre',)

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

    @classmethod
    def save_logo(cls, file_obj):
        result = {'status': 'error', 'ok': False}
        name = file_obj.filename.split('.')
        if name[-1].lower() != EXT['PNG']:
            return result

        emisor = Emisor.select()[0]
        rfc = emisor.rfc.lower()
        name = f'{rfc}.png'
        path = util._join(PATHS['LOGOS'], name)
        if util.save_file(path, file_obj.file.read()):
            emisor.logo = file_obj.filename
            emisor.save()
            result =  {'status': 'server', 'name': file_obj.filename, 'ok': True}

        return result

    @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_curp': obj.curp,
                '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,
                'emisor_logo': obj.logo,
                '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,
                'token_soporte': obj.token_soporte,
                'emisor_registro_patronal': obj.registro_patronal,
                'regimenes': [row.id for row in obj.regimenes],
                'emisor_hours': obj.hours
            }
        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,
                'REPO': obj.token_soporte,
            }
            return data
        except:
            return {}

    @classmethod
    def get_timbres(cls):
        try:
            obj = Emisor.select()[0]
        except IndexError:
            return 's/e'

        auth = Configuracion.get_({'fields': 'pac_auth'})
        result = utils.get_client_balance(auth, obj.rfc)
        return result

    @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['curp'] = fields.pop('emisor_curp', '')
        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', ''))
        ong_auth = fields.pop('ong_autorizacion', '')
        ong_date = fields.pop('ong_fecha', None)
        ong_date_dof = fields.pop('ong_fecha_dof', None)
        fields['autorizacion'] = ong_auth
        fields['fecha_autorizacion'] = ong_date
        fields['fecha_dof'] = ong_date_dof

        fields['correo_timbrado'] = fields.pop('correo_timbrado', '')
        fields['token_timbrado'] = fields.pop('token_timbrado', '')
        fields['token_soporte'] = fields.pop('token_soporte', '')
        if len(fields['rfc']) == 12:
            fields['es_moral'] = True
        fields['registro_patronal'] = fields.pop('emisor_registro_patronal', '')
        fields['regimenes'] = SATRegimenes.get_(
            util.loads(fields['regimenes']))
        fields['hours'] = int(fields.pop('emisor_hours', 0))

        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)
    es_fiel = BooleanField(default=False)

    def __str__(self):
        return self.serie

    @classmethod
    def _get_cert(cls, args):
        obj = Certificado.get(Certificado.es_fiel==False)
        data = {
            'cert_rfc': obj.rfc,
            'cert_serie': obj.serie,
            'cert_desde': obj.desde,
            'cert_hasta': obj.hasta,
        }
        return data

    @classmethod
    def get_data(cls, values):
        opt = values['opt']
        return getattr(cls, f'_get_{opt}')(values)

    @classmethod
    def _validate_cert(cls, args):
        msg = 'Certificado guardado correctamente'
        result = {'ok': True, 'msg': msg, 'data': {}}
        cert = utils.get_cert(args)
        if not cert.is_valid:
            result['ok'] = False
            result['msg'] = cert.error
            return result

        obj = Certificado.get(Certificado.es_fiel==False)
        if not DEBUG:
            if obj.rfc != cert.rfc:
                result['ok'] = False
                result['msg'] = 'El RFC del certificado no corresponde.'
                return result

        obj.key = cert._key
        obj.key_enc = cert.key_enc
        obj.cer = cert.cer
        obj.serie = cert.serial_number
        obj.cer_txt = cert.cer_txt
        obj.p12 = utils.encrypt(cert._keyp, cert.serial_number)
        obj.desde = cert.not_before
        obj.hasta = cert.not_after
        obj.save()

        data = {
            'cert_rfc': obj.rfc,
            'cert_serie': obj.serie,
            'cert_desde': obj.desde,
            'cert_hasta': obj.hasta,
        }
        result['data'] = data
        return result

    @classmethod
    def post(cls, values):
        opt = values['opt']
        return getattr(cls, f'_{opt}')(values)


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

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

    @classmethod
    def get_default(cls):
        folio = Folios.select()[0]
        return folio.serie

    @classmethod
    def get_id(cls, serie):
        try:
            obj = Folios.get(Folios.serie==serie)
            return obj.id
        except:
            return Folios.select()[0].id

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

    @classmethod
    def get_for_sucursales(cls):
        rows = (Folios
            .select(
                Folios.serie.alias('id'),
                Folios.serie.alias('value'),
            )
            # ~ .where(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_by_id(cls, id):
        try:
            return Categorias.get(Categorias.id==id)
        except:
            return None

    @classmethod
    def _get_by_name(cls, name):
        try:
            return Categorias.get(Categorias.categoria==name)
        except:
            return None

    @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 SATUnidadesPeso(BaseModel):
    key = TextField(unique=True, index=True)
    name = TextField(default='', index=True)
    activo = BooleanField(default=False)

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

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

    @classmethod
    def _get_all(cls, values, user):
        rows = tuple(SATUnidadesPeso.select().dicts())
        return rows

    @classmethod
    def _get_active(cls, values, user):
        fields = (
            SATUnidadesPeso.key.alias('id'),
            SATUnidadesPeso.name.alias('value'),
        )
        where = (SATUnidadesPeso.activo==True)
        rows = tuple(
            SATUnidadesPeso.select(*fields).where(where).dicts()
        )
        return rows

    @classmethod
    def get_data(cls, values, user):
        opt = values.pop('opt')
        return getattr(cls, f'_get_{opt}')(values, user)

    @classmethod
    def _add(cls, values, user):
        result = True
        try:
            SATUnidadesPeso.create(**values)
        except:
            result = False

        return {'ok': result}

    @classmethod
    def _delete(cls, values, user):
        id = values['id']
        q = SATUnidadesPeso.delete().where(SATUnidadesPeso.id==id)
        result = bool(q.execute())
        response = {'ok': result}
        return response

    @classmethod
    def _update_active(cls, values, user):
        id = values['id']
        update = {'activo': bool(values['activo'])}
        where = (SATUnidadesPeso.id==id)
        q = SATUnidadesPeso.update(**update).where(where)
        result = bool(q.execute())
        response = {'ok': result}
        return response

    @classmethod
    def post(cls, values, user):
        opt = values['opt']
        args = utils.loads(values['values'])
        return getattr(cls, f'_{opt}')(args, user)


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_by_name(self, name):
        try:
            return SATUnidades.get(SATUnidades.name==name)
        except:
            return None

    @classmethod
    def get_key_by_id(self, id):
        obj = SATUnidades.get(SATUnidades.id==id)
        return obj.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_default(cls):
        obj = SATUnidades.select()[0]
        if obj.default:
            return obj.id
        return 0

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

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

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


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_values(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_get_{}'.format(opt))(cls, values)

    def _get_active_by_id(self, values):
        rows = (SATFormaPago
            .select(
                SATFormaPago.id,
                SATFormaPago.name.alias('value'))
            .where(SATFormaPago.activo==True)
            .dicts()
        )
        return tuple(rows)

    def _get_active_by_key(self, values):
        rows = (SATFormaPago
            .select(
                SATFormaPago.key.alias('id'),
                SATFormaPago.name.alias('value'))
            .where(SATFormaPago.activo==True)
            .dicts()
        )
        return tuple(rows)

    @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,
                fn.Concat("[", SATFormaPago.key, "] ", 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_by_key(cls, key):
        return SATMonedas.get(SATMonedas.key==key)

    @classmethod
    def get_multi_currency(cls):
        count_currencies = len(cls.get_activos())
        multi_currency = False
        if count_currencies > 1:
            multi_currency = True
        return multi_currency

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

    @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'], '000'),
            '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)

    def _updaterfc(self, args):
        id = int(args['id'])
        values = util.loads(args['values'])
        msg = util.validate_rfc(values['rfc'])
        if msg:
            return {'ok': False, 'msg': msg}

        q = (SATBancos
            .update(**values)
            .where(SATBancos.id==id))
        result = bool(q.execute())
        msg = 'RFC actualizado correctamente'
        return {'ok': result, 'msg': msg}

    @classmethod
    def post(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_{}'.format(opt))(cls, values)

    @classmethod
    def get_values(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_get_{}'.format(opt))(cls, values)

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

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

    @classmethod
    def get_by_key(cls, key):
        if not key:
            return
        try:
            obj = SATBancos.get(SATBancos.key==key)
            return obj
        except SATBancos.DoesNotExist:
            msg = 'SATBancos no existe: {}'.format(key)
            log.error(msg)
        return

    @classmethod
    def get_by_name(cls, name):
        try:
            obj = SATBancos.get(SATBancos.name==name)
            return obj
        except SATBancos.DoesNotExist:
            msg = 'SATBancos no existe: {}'.format(key)
            log.error(msg)
        return


class SATNivelesEducativos(BaseModel):
    name = TextField(index=True)

    class Meta:
        order_by = ('name',)

    def __str__(self):
        return self.name

    @classmethod
    def get_by(cls):
        rows = SATNivelesEducativos.select(
            SATNivelesEducativos.name).tuples()
        return tuple([r[0] for r in rows])


class NivelesEducativos(BaseModel):
    nombre = TextField()
    autorizacion = TextField(default='')

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

    def __str__(self):
        return '{} ({})'.format(self.nombre, self.autorizacion)

    @classmethod
    def get_all(cls):
        rows = NivelesEducativos.select().dicts()
        return tuple(rows)

    def _add_group(self, obj):
        Grupos.get_or_create(**{'nivel': obj})
        return

    @classmethod
    def add(cls, values):
        try:
            obj = NivelesEducativos.create(**values)
            # Revisar
            cls._add_group(cls, obj)
            result = {'ok': True}
        except IntegrityError:
            msg = 'Nivel Educativo existente'
            result = {'ok': False, 'msg': msg}
        return result

    @classmethod
    def remove(cls, id):
        obj = NivelesEducativos.get(NivelesEducativos.id==int(id))
        q = Grupos.delete().where(Grupos.nivel==obj)
        try:
            q.execute()
        except IntegrityError:
            return False

        q = NivelesEducativos.delete().where(NivelesEducativos.id==int(id))
        return bool(q.execute())


class Grupos(BaseModel):
    nivel = ForeignKeyField(NivelesEducativos)
    grado = TextField(default='')
    nombre = TextField(default='')

    class Meta:
        order_by = ('nivel', 'grado', 'nombre')
        indexes = (
            (('nivel', 'grado', 'nombre'), True),
        )

    def __str__(self):
        return '{} {} {}'.format(self.nivel.nombre, self.grado, self.nombre)

    @classmethod
    def get_by(cls, values):
        rows = (Grupos.select(
            Grupos.id.alias('id'),
            NivelesEducativos.nombre.alias('value'))
            .join(NivelesEducativos)
            .switch(Grupos)
            .dicts()
        )
        return tuple(rows)


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.nombre, self.cuenta[-4:])

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

    @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):
        opt = values.pop('opt', '')
        if opt:
            return getattr(cls, '_get_{}'.format(opt))(cls, values)

        if values['tipo'] == '1':
            rows = (CuentasBanco
                .select()
                .where(CuentasBanco.de_emisor==True, CuentasBanco.activa==True)
                .order_by(CuentasBanco.id)
            )
            if not (len(rows)):
                return {'ok': False}

            first = rows[0]
            rows = [{'id': r.id, 'value': '{} ({})'.format(
                r.nombre, r.cuenta[-4:])} for r in rows]
            data = {
                'ok': True,
                'rows': tuple(rows),
                'moneda': '{} ({})'.format(first.moneda.name, first.moneda.key),
                'key_currency': first.moneda.key,
                'saldo': first.saldo,
            }
            return data

        return {'ok': False}

    def _get_currency(self, values):
        id = int(values['id'])
        account = CuentasBanco.get(CuentasBanco.id==id)
        data = {
            'ok': True,
            'currency': '{} ({})'.format(account.moneda.name, account.moneda.key),
            'key_currency': account.moneda.key,
        }
        return data

    @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, UniqueViolation):
                msg = 'Esta cuenta ya existe'
                return {'ok': False, 'msg': msg}

            nuevo_mov= {
                'opt': 'add',
                'cuenta': obj.id,
                'fecha': fecha_deposito,
                'descripcion': 'Saldo inicial',
                'forma_pago': SATFormaPago.get_by_key('99'),
                'deposito': values['saldo'],
                'saldo': values['saldo'],
            }
            MovimientosBanco.post(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 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 = ('name',)
        indexes = (
            (('key', 'name'), True),
        )

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

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

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

    @classmethod
    def get_all(self):
        rows = SATUsoCfdi.select().dicts()
        return tuple(rows)

    @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'),
                fn.Concat("[", SATUsoCfdi.key, "] ", SATUsoCfdi.name).alias('value'),
                SATUsoCfdi.fisica,
                SATUsoCfdi.moral,
            )
            .where(SATUsoCfdi.activo==True)
            .dicts()
        )
        return tuple(rows)


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATEstados.get(SATEstados.key==key)
            return obj
        except SATEstados.DoesNotExist:
            msg = 'SATEstados no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATOrigenRecurso.get(SATOrigenRecurso.key==key)
            return obj
        except SATOrigenRecurso.DoesNotExist:
            msg = 'SATOrigenRecurso no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATPeriodicidadPago.get(SATPeriodicidadPago.key==key)
            return obj
        except SATPeriodicidadPago.DoesNotExist:
            msg = 'SATPeriodicidadPago no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoContrato.get(SATTipoContrato.key==key)
            return obj
        except SATTipoContrato.DoesNotExist:
            msg = 'SATTipoContrato no existe: {}'.format(key)
            log.error(msg)
        return


class SATTipoDeduccion(BaseModel):
    key = TextField(index=True, unique=True)
    name = TextField(default='', index=True)
    clave = TextField(default='')
    nombre = TextField(default='')
    activo = BooleanField(default=True)

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoDeduccion.get(SATTipoDeduccion.key==key)
            return obj
        except SATTipoDeduccion.DoesNotExist:
            msg = 'SATTipoDeduccion no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoHoras.get(SATTipoHoras.key==key)
            return obj
        except SATTipoHoras.DoesNotExist:
            msg = 'SATTipoHoras no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoIncapacidad.get(SATTipoIncapacidad.key==key)
            return obj
        except SATTipoIncapacidad.DoesNotExist:
            msg = 'SATTipoIncapacidad no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoJornada.get(SATTipoJornada.key==key)
            return obj
        except SATTipoJornada.DoesNotExist:
            msg = 'SATTipoJornada no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoNomina.get(SATTipoNomina.key==key)
            return obj
        except SATTipoNomina.DoesNotExist:
            msg = 'SATTipoNomina no existe: {}'.format(key)
            log.error(msg)
        return


class SATTipoOtroPago(BaseModel):
    key = TextField(index=True, unique=True)
    name = TextField(default='', index=True)
    clave = TextField(default='')
    nombre = TextField(default='')
    activo = BooleanField(default=True)

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

    def __str__(self):
        return 'Tipo de Otro Pago: {} ({})'.format(self.name, self.key)

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoOtroPago.get(SATTipoOtroPago.key==key)
            return obj
        except SATTipoOtroPago.DoesNotExist:
            msg = 'SATTipoOtroPago no existe: {}'.format(key)
            log.error(msg)
        return


class SATTipoPercepcion(BaseModel):
    key = TextField(index=True, unique=True)
    name = TextField(default='', index=True)
    clave = TextField(default='')
    nombre = TextField(default='')
    activo = BooleanField(default=True)

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoPercepcion.get(SATTipoPercepcion.key==key)
            return obj
        except SATTipoPercepcion.DoesNotExist:
            msg = 'SATTipoPercepcion no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATTipoRegimen.get(SATTipoRegimen.key==key)
            return obj
        except SATTipoRegimen.DoesNotExist:
            msg = 'SATTipoRegimen no existe: {}'.format(key)
            log.error(msg)
        return


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

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

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

    @classmethod
    def get_by_key(cls, key):
        try:
            obj = SATRiesgoPuesto.get(SATRiesgoPuesto.key==key)
            return obj
        except SATRiesgoPuesto.DoesNotExist:
            msg = 'SATRiesgoPuesto no existe: {}'.format(key)
            log.error(msg)
        return


class SATLeyendasFiscales(BaseModel):
    texto_leyenda = TextField(index=True)
    norma = TextField(default='')
    disposicion_fiscal = TextField(default='')
    active = BooleanField(default=True)
    default = BooleanField(default=False)

    class Meta:
        order_by = ('texto_leyenda',)
        indexes = (
            (('texto_leyenda', 'norma', 'disposicion_fiscal'), True),
        )

    def _get_all(self, values):
        rows = (SATLeyendasFiscales.select(
            SATLeyendasFiscales.id,
            SQL(" '-' AS delete"),
            SATLeyendasFiscales.texto_leyenda,
            SATLeyendasFiscales.norma,
            SATLeyendasFiscales.disposicion_fiscal)
            .dicts()
        )
        return tuple(rows)

    def _get_active(self, values):
        rows = (SATLeyendasFiscales.select(
            SATLeyendasFiscales.id,
            SATLeyendasFiscales.texto_leyenda,
            SATLeyendasFiscales.norma,
            SATLeyendasFiscales.disposicion_fiscal)
            .where(SATLeyendasFiscales.active==True)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_by_id(cls, ids):
        rows = (SATLeyendasFiscales.select(
            SATLeyendasFiscales.texto_leyenda.alias('textoLeyenda'),
            SATLeyendasFiscales.norma,
            SATLeyendasFiscales.disposicion_fiscal.alias('disposicionFiscal'))
            .where(SATLeyendasFiscales.id.in_(ids))
            .dicts()
        )
        for row in rows:
            if not row['norma']:
                del row['norma']
            if not row['disposicionFiscal']:
                del row['disposicionFiscal']
        return tuple(rows)

    @classmethod
    def get_values(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_get_{}'.format(opt))(cls, values)

    def _new(self, values):
        try:
            obj = SATLeyendasFiscales.create(**values)
            values['id'] = obj.id
            values['delete'] = '-'
            result = {'ok': True, 'row': values}
        except Exception as e:
            result = {'ok': False, 'msg': 'La Leyenda Fiscal ya existe'}
        return result

    @classmethod
    def post(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_{}'.format(opt))(cls, values)

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


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)
    id_fiscal = TextField(default='')
    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='')
    correo_facturasp = 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')
    plantilla = TextField(default='')
    regimen_fiscal = TextField(default='')

    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)

        change_balance = Configuracion.get_bool('chk_config_change_balance_partner')
        balance = fields.pop('partner_balance', '').replace('$', '').replace(',', '')
        if change_balance:
            fields['saldo_cliente'] = round(float(balance), 2)

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

        ft = ('nombre_comercial', 'calle', 'no_exterior', 'no_interior',
            'colonia', 'municipio', 'estado', 'pais', 'codigo_postal', 'notas',
            'telefonos', 'cuenta_cliente', 'cuenta_proveedor', 'web',
            'correo_facturas', 'plantilla', 'id_fiscal')
        for name in ft:
            fields[name] = values.get(name, '')
        fields['correo_facturasp'] = values.pop('partner_email_fp', '')

        if fields['pais'] != 'México':
            fields['pais'] = fields['pais'].upper()

        if 'regimenes' in fields:
            fields['regimenes'] = utils.loads(fields['regimenes'])
            if isinstance(fields['regimenes'], list):
                fields['regimenes'] = tuple(map(int, fields['regimenes']))
            else:
                fields['regimenes'] = (fields['regimenes'],)

        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']))
            row['partner_balance'] = row.pop('saldo_cliente')
            row['partner_email_fp'] = row.pop('correo_facturasp')
            row['regimenes'] = SociosRegimenes.get_by_socio(row['id'])
            return row

        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'),
                    Socios.codigo_postal)
                .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):
                client = row[0]
                client['regimenes'] = SociosRegimenes.get_by_key(client['id'])
                return {'ok': True, 'row': client}
            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'),
                    Socios.codigo_postal)
                .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios)
                .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios)
                .where((Socios.es_cliente==True & Socios.es_activo==True) &
                    (Socios.rfc.contains(name) |
                    Socios.nombre.contains(name)))
                .dicts())
            return tuple(rows)
        return {'ok': False}

    @classmethod
    def add(cls, values):
        accounts = util.loads(values.pop('accounts', '[]'))
        fields = cls._clean(cls, values)
        regimenes = fields.pop('regimenes', ())

        w = ((Socios.rfc==fields['rfc']) & (Socios.slug==fields['slug']))
        if Socios.select().where(w).exists():
            msg = 'Ya existe el RFC y Razón Social'
            data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
            return data

        try:
            obj = Socios.create(**fields)
        except IntegrityError as e:
            msg = 'Ocurrio un error, al dar de alta el emisor'
            data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
            return data

        #~ ToDo Agregar tags

        for account in accounts:
            try:
                bank = SATBancos.get_by_name(account['banco'])
                fields = {
                    'socio': obj,
                    'banco': bank,
                    'cuenta': account['cuenta'],
                    'clabe': account['clabe'],
                }
                o = SociosCuentasBanco.create(**fields)
            except IntegrityError:
                pass

        for regimen in regimenes:
            try:
                fields = {
                    'socio': obj,
                    'regimen': regimen,
                }
                SociosRegimenes.create(**fields)
            except IntegrityError:
                pass

        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)
        fields.pop('accounts', '')
        regimenes = fields.pop('regimenes', ())

        w = (
            (Socios.rfc==fields['rfc']) &
            (Socios.slug==fields['slug']) &
            (Socios.id!=id)
        )
        if Socios.select().where(w).exists():
            msg = 'Ya existe otro emisor con este RFC y Razón Social'
            data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
            return data

        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

        obj = Socios.get(Socios.id==id)
        q = SociosRegimenes.delete().where(SociosRegimenes.socio==id)
        q.execute()
        for regimen in regimenes:
            try:
                fields = {
                    'socio': obj,
                    'regimen': regimen,
                }
                SociosRegimenes.create(**fields)
            except IntegrityError:
                pass

        obj = Socios.get(Socios.id==id)
        row = {
            'id': id,
            'rfc': obj.rfc,
            'nombre': obj.nombre,
            'saldo_cliente': obj.saldo_cliente,
        }
        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 = SociosCuentasBanco.delete().where(SociosCuentasBanco.socio==id)
        q.execute()
        q = SociosRegimenes.delete().where(SociosRegimenes.socio==id)
        q.execute()
        q = Socios.delete().where(Socios.id==id)
        return bool(q.execute())

    @classmethod
    def opt(cls, args):
        opt = args.get('opt', '')
        if opt == 'reset':
            return cls._reset_saldo(cls, args['id'])

        return {'ok': False}

    # ~ v2

    @classmethod
    def post(cls, values):
        opt = values['opt']
        args = values['values']
        return getattr(cls, f'_{opt}')(cls, args)


class SociosCuentasBanco(BaseModel):
    socio = ForeignKeyField(Socios)
    activa = BooleanField(default=True)
    banco = ForeignKeyField(SATBancos)
    cuenta = TextField(default='')
    clabe = TextField(default='')
    moneda = ForeignKeyField(SATMonedas, null=True)

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

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

    def _get_by_name(self, values):
        name = values['name']
        query = (SociosCuentasBanco
            .select()
            .where(SociosCuentasBanco.activa==True, Socios.nombre==name)
            .join(Socios).switch(SociosCuentasBanco)
            )
        rows = tuple([{'id': q.id, 'value': str(q)} for q in query])
        return {'ok': True, 'values': rows}

    def _get_by_partner(self, values):
        id = int(values['id_partner'])
        rows = (SociosCuentasBanco.select(
            SociosCuentasBanco.id,
            SQL(" '-' AS delete"),
            SATBancos.name.alias('banco'),
            SociosCuentasBanco.cuenta,
            SociosCuentasBanco.clabe)
        .join(SATBancos).switch(SociosCuentasBanco)
        .where(SociosCuentasBanco.socio==id)
        .dicts())
        return tuple(rows)

    @classmethod
    def get_values(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_get_{}'.format(opt))(cls, values)

    @classmethod
    def post(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_{}'.format(opt))(cls, values)

    def _new(self, values):
        values = util.loads(values['values'])
        bank = SATBancos.get_by_name(values['banco'])
        fields = {
            'socio': values['id_partner'],
            'banco': bank,
            'cuenta': values['cuenta'],
            'clabe': values['clabe'],
        }
        try:
            obj = SociosCuentasBanco.create(**fields)
        except IntegrityError as e:
            msg = 'Ya existe esta cuenta'
            data = {'ok': False, 'msg': msg}
            return data

        msg = 'Cuenta de banco agregada correctamente'
        data = {'ok': True, 'id': obj.id, 'msg': msg}
        return data

    def _delete(self, values):
        values = util.loads(values['values'])
        id = int(values['id'])
        q = SociosCuentasBanco.delete().where(SociosCuentasBanco.id==id)
        result = bool(q.execute())
        msg = 'Cuenta borrada correctamente'
        return {'ok': result, 'msg': msg}

    @classmethod
    def validate_partner_account(cls, id, invoices):
        id_invoice = int(tuple(invoices.keys())[0])
        account = SociosCuentasBanco.get(SociosCuentasBanco.id==id)
        invoice = Facturas.get(Facturas.id==id_invoice)
        return account.socio == invoice.cliente


class SociosRegimenes(BaseModel):
    socio = ForeignKeyField(Socios)
    regimen = ForeignKeyField(SATRegimenes)

    class Meta:
        indexes = (
            (('socio', 'regimen'), True),
        )

    @classmethod
    def get_by_key(cls, socio, user=None):
        fields = (
            SATRegimenes.key.alias('id'),
            fn.Concat("[", SATRegimenes.key, "] ", SATRegimenes.name).alias('value')
        )
        where = (SociosRegimenes.socio == socio)
        regimenes = (SociosRegimenes
            .select(*fields)
            .where(where)
            .join(SATRegimenes).switch(SociosRegimenes)
            .dicts()
        )
        return tuple(regimenes)

    @classmethod
    def get_by_socio(cls, socio, user=None):
        fields = (SATRegimenes.id,)
        where = (SociosRegimenes.socio == socio)
        regimenes = (SociosRegimenes
            .select(*fields)
            .where(where)
            .join(SATRegimenes).switch(SociosRegimenes)
            .tuples()
        )
        regimenes = [r[0] for r in regimenes]
        return regimenes

    @classmethod
    def get_by_socio_first(cls, socio, user=None):
        fields = (SATRegimenes.key,)
        where = (SociosRegimenes.socio == socio)
        regimenes = (SociosRegimenes
            .select(*fields)
            .where(where)
            .join(SATRegimenes).switch(SociosRegimenes)
            .dicts()
        )
        regimen = ''
        if len(regimenes):
            regimen = regimenes[0]['key']
        return regimen

    @classmethod
    def _get_by_id(cls, filters, user):
        id = int(filters['id'])
        return cls.get_by_key(id)

    @classmethod
    def get_data(cls, filters, user):
        # ~ print('FILERS', filters)
        opt = filters['opt']
        return getattr(cls, f'_get_{opt}')(filters, user)


class Contactos(BaseModel):
    socio = ForeignKeyField(Socios)
    titulo = ForeignKeyField(TipoTitulo)
    foto = TextField(default='')
    nombre = TextField(index=True)
    paterno = TextField(index=True)
    materno = TextField(default='')
    fecha_nacimiento = DateField(null=True)
    notas = TextField(default='')

    class Meta:
        order_by = ('socio', 'nombre')
        indexes = (
            (('socio', 'nombre', 'paterno', 'materno'), True),
        )


class ContactoDirecciones(BaseModel):
    contacto = ForeignKeyField(Contactos)
    tipo = ForeignKeyField(TipoDireccion)
    direccion = TextField()

    class Meta:
        order_by = ('contacto',)
        indexes = (
            (('contacto', 'tipo', 'direccion'), True),
        )


class ContactoTelefonos(BaseModel):
    contacto = ForeignKeyField(Contactos)
    tipo = ForeignKeyField(TipoTelefono)
    telefono = TextField()

    class Meta:
        order_by = ('contacto',)
        indexes = (
            (('contacto', 'tipo', 'telefono'), True),
        )


class ContactoCorreos(BaseModel):
    contacto = ForeignKeyField(Contactos)
    tipo = ForeignKeyField(TipoCorreo)
    correo = TextField()

    class Meta:
        order_by = ('contacto',)
        indexes = (
            (('contacto', 'tipo', 'correo'), True),
        )


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='')
    cuenta_socio = ForeignKeyField(SociosCuentasBanco, null=True)
    tipo_cadena_pago = TextField(default='')
    certificado_pago = TextField(default='')
    cadena_pago = TextField(default='')
    sello_pago = TextField(default='')

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

    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

    def _update_description(self, values):
        try:
            obj = MovimientosBanco.get(MovimientosBanco.id==values['id'])
        except MovimientosBanco.DoesNotExist:
            msg = 'No se encontró el movimiento'
            return {'ok': False, 'msg': msg}

        obj.descripcion = values['description']
        obj.save()
        msg = 'Descripción actualizada correctamente'
        return {'ok': True, 'msg': msg}

    @classmethod
    def post(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_{}'.format(opt))(cls, values)

    def _add(self, values):
        ids = values.pop('ids', '')

        cuenta_socio = values.get('cuenta_socio', None)
        if not ids:
            cuenta_socio = None

        if ids and cuenta_socio and \
            not SociosCuentasBanco.validate_partner_account(cuenta_socio, util.loads(ids)):
            msg = 'La cuenta seleccionada no corresponde al cliente'
            data = {'ok': False, 'msg': msg}
            return data

        if ids and not Facturas.validate_count_partners(util.loads(ids)):
            msg = 'Facturas relacionadas a diferentes clientes'
            data = {'ok': False, 'msg': msg}
            return data

        actualizar = False
        if 'saldo' in values:
            saldo = values['saldo']
        else:
            actualizar = True
            hora = values.pop('hora')
            account = CuentasBanco.get(CuentasBanco.id==int(values['cuenta']))
            values['fecha'] = '{}T{}'.format(values['fecha'][:10], hora)
            values['cuenta'] = account
            values['cuenta_socio'] = cuenta_socio
            values['moneda'] = account.moneda.key
            values['retiro'] = util.get_float(values['retiro'])
            values['deposito'] = util.get_float(values['deposito'])
            values['tipo_cambio'] = util.get_float(
                values.get('tipo_cambio', 1.00), True)
            values['forma_pago'] = int(values['forma_pago'])

            ultimo_saldo = self._ultimo_saldo(
                self, values['cuenta'], values['fecha'])

            values['saldo'] = \
                ultimo_saldo - values['retiro'] + values['deposito']

        with database_proxy.transaction():
            try:
                obj = MovimientosBanco.create(**values)
            except IntegrityError:
                msg = 'Error al agregar el movimiento'
                return {'ok': False, 'msg': msg}

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

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

    def _cancel(self, values):
        id = int(values['id'])
        try:
            obj = MovimientosBanco.get(MovimientosBanco.id==id)
        except MovimientosBanco.DoesNotExist:
            msg = 'No se encontró el movimiento'
            return {'ok': False, 'msg': msg}

        if obj.cancelado:
            msg = 'El movimiento ya esta cancelado'
            return {'ok': False, 'msg': msg}

        if obj.conciliado:
            msg = 'El movimiento esta conciliado, no se puede cancelar'
            return {'ok': False, 'msg': msg}

        filters = (
            (CfdiPagos.movimiento==obj) &
            (CfdiPagos.uuid.is_null(False)) &
            (CfdiPagos.cancelada==False)
        )
        cp = CfdiPagos.select().where(filters).count()
        if cp > 0:
            msg = 'El movimiento tiene Factura de Pago activas, no se puede cancelar'
            return {'ok': False, 'msg': msg}

        filters = (
            (CfdiPagos.movimiento==obj) &
            (CfdiPagos.uuid.is_null(True)) &
            (CfdiPagos.error!='')
        )
        cp = CfdiPagos.select().where(filters).count()
        if cp > 0:
            msg = 'El movimiento tiene Factura de Pago con error, no se puede cancelar'
            return {'ok': False, 'msg': msg}

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

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

        balance = CuentasBanco.get_saldo(obj.cuenta.id)

        msg = 'Movimiento cancelado correctamente'
        return {'ok': True, 'msg': msg, 'balance': balance}

    @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 = util.loads(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,
            SATFormaPago.name.alias('way_payment'),
            MovimientosBanco.descripcion,
            MovimientosBanco.retiro,
            MovimientosBanco.deposito,
            MovimientosBanco.saldo,
            fn.COUNT(CfdiPagos.id).alias('invoice')
        )
        .join(SATFormaPago).switch(MovimientosBanco)
        .join(CfdiPagos, JOIN.LEFT_OUTER).switch(MovimientosBanco)
        .where(filtros)
        .group_by(MovimientosBanco.id, SATFormaPago.name)
        .dicts()
        )

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


class Alumnos(BaseModel):
    rfc = TextField(null=True)
    curp = TextField(index=True, unique=True)
    foto = TextField(default='')
    nombre = TextField(index=True)
    paterno = TextField(index=True)
    materno = 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='')
    codigo_postal = TextField(default='')
    notas = TextField(default='')
    telefonos = TextField(default='')
    correos = TextField(default='')
    es_activo = BooleanField(default=True)
    fecha_alta = DateField(default=util.now)
    fecha_nacimiento = DateField(null=True)
    factura = ForeignKeyField(Socios, null=True)
    grupo = ForeignKeyField(Grupos, null=True)

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

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

    def _clean(self, values):
        fields = util.clean(util.loads(values))
        fields['rfc'] = fields['rfc'].upper().strip()
        fields['curp'] = fields['curp'].upper()
        fields['nombre'] = util.spaces(fields['nombre'])
        fields['paterno'] = util.spaces(fields['paterno'])
        fields['materno'] = util.spaces(fields['materno'])
        return fields

    def _get(self, where):
        rows = (Alumnos
            .select()
            .where(where)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_by_name(cls, values):
        rows = ()
        name = values.get('name', '')
        if name:
            rows = (Alumnos
                .select(
                    Alumnos.id,
                    Alumnos.nombre,
                    Alumnos.paterno,
                    Alumnos.materno,
                    Alumnos.rfc)
                .where((Alumnos.es_activo==True) &
                    (Alumnos.nombre.contains(name) |
                    Alumnos.paterno.contains(name) |
                    Alumnos.materno.contains(name) |
                    Alumnos.rfc.contains(name)))
                .dicts())
            rows = tuple(rows)

        return rows

    @classmethod
    def get_by(cls, values):
        if 'id' in values:
            id = int(values['id'])
            w = (Alumnos.id==id)
            rows = cls._get(cls, w)
            return rows[0]

        if not values:
            w = None

        return cls._get(cls, w)

    @classmethod
    def add(cls, values):
        fields = cls._clean(cls, values)
        try:
            obj = Alumnos.create(**fields)
        except IntegrityError as e:
            msg = 'Ya existe un alumno con este CURP'
            data = {'ok': False, 'msg': msg}
            return data

        data = {'ok': True}
        return data

    @classmethod
    def actualizar(cls, values):
        fields = cls._clean(cls, values)
        id = int(fields.pop('id'))
        try:
            q = Alumnos.update(**fields).where(Alumnos.id==id)
            q.execute()
        except IntegrityError:
            msg = 'Ya existe un Alumno con este CURP'
            data = {'ok': False, 'msg': msg}
            return data

        data = {'ok': True}
        return data

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


class AlumnosParientes(BaseModel):
    alumno = ForeignKeyField(Alumnos)
    tipo_pariente = ForeignKeyField(TipoPariente)
    foto = TextField(default='')
    nombre = TextField(index=True)
    paterno = TextField(index=True)
    materno = TextField(default='')
    fecha_nacimiento = DateField(null=True)
    puede_recoger = BooleanField(default=False)

    class Meta:
        order_by = ('alumno',)


class ParienteDirecciones(BaseModel):
    pariente = ForeignKeyField(AlumnosParientes)
    tipo = ForeignKeyField(TipoDireccion)
    direccion = TextField()

    class Meta:
        order_by = ('pariente',)


class ParienteTelefonos(BaseModel):
    pariente = ForeignKeyField(AlumnosParientes)
    tipo = ForeignKeyField(TipoTelefono)
    telefono = TextField()

    class Meta:
        order_by = ('pariente',)


class ParienteCorreos(BaseModel):
    pariente = ForeignKeyField(AlumnosParientes)
    tipo = ForeignKeyField(TipoCorreo)
    correo = TextField()

    class Meta:
        order_by = ('pariente',)


class Almacenes(BaseModel):
    name = TextField(unique=True)
    ubicacion = TextField(default='')

    class Meta:
        order_by = ('name',)

    @classmethod
    def get_for_sucursales(cls):
        rows = (Almacenes
            .select(
                Almacenes.id,
                Almacenes.name.alias('value'),
            )
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def _get_for_select(cls, values={}):
        rows = (Almacenes
            .select(
                Almacenes.id,
                Almacenes.name.alias('value'),
            )
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def _get_all(cls, values):
        rows = (Almacenes
            .select(
                Almacenes.id,
                Almacenes.name,
            )
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_data(cls, values):
        opt = values['opt']
        return getattr(cls, f'_get_{opt}')(values)

    @classmethod
    def _create(cls, args, user):
        try:
            obj, created = Almacenes.get_or_create(**args)
        except Exception as e:
            log.error(e)
            msg = 'Ocurrio un error al agregar el almacen'
            result = {'ok': False, 'msg': msg}
            return result

        if created:
            rows = Almacenes._get_all({})
            result = {'ok': True, 'rows': rows}
        else:
            msg = 'El almacen ya existe'
            result = {'ok': True, 'msg': msg}

        return result

    @classmethod
    def _delete(cls, args, user):
        id = args['id']
        try:
            q = Almacenes.delete().where(Almacenes.id==id)
            r = bool(q.execute())
            result = {'ok': r}
        except Exception as e:
            log.error(e)
            result = {'ok': False, 'msg': 'Ocurrio un error al intentar eliminar'}

        return result

    @classmethod
    def post(cls, values, user):
        opt = values['opt']
        args = utils.loads(values['values'])
        return getattr(cls, f'_{opt}')(args, user)


class Sucursales(BaseModel):
    warehouse = ForeignKeyField(Almacenes, null=True)
    nombre = TextField(default='')
    direccion = TextField(default='')
    serie_facturas = TextField(default='')
    serie_tickets = TextField(default='')

    class Meta:
        order_by = ('nombre',)

    @classmethod
    def _get_for_admin(cls, args):
        rows = (Sucursales
            .select(
                Sucursales.id,
                Sucursales.nombre.alias('name'),
                Sucursales.serie_facturas.alias('serie_invoice'),
                Sucursales.serie_tickets,
                Almacenes.name.alias('warehouse'),
            )
            .join(Almacenes, JOIN.LEFT_OUTER).switch(Sucursales)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def _get_for_select(cls, args):
        fields = (
            Sucursales.id,
            Sucursales.nombre.alias('value')
        )
        rows = Sucursales.select(*fields).dicts()
        return tuple(rows)

    @classmethod
    def _get_there_are_branchs(cls, args):
        result = bool(Sucursales.select().count())
        return result

    @classmethod
    def get_data(cls, values):
        opt = values['opt']
        return getattr(cls, f'_get_{opt}')(values)

    @classmethod
    def _create(cls, args):
        try:
            values = utils.loads(args)
            Sucursales.create(**values)
            result = {'ok': True}
        except Exception as e:
            log.error(e)
            msg = 'Ocurrio un error al agregar la sucursal'
            result = {'ok': False, 'msg': msg}

        return result

    @classmethod
    def _delete(cls, args):
        values = utils.loads(args)
        id = values['id']

        try:
            q = Sucursales.delete().where(Sucursales.id==id)
            r = bool(q.execute())
            result = {'ok': r}
        except Exception as e:
            log.error(e)
            result = {'ok': False, 'msg': 'Ocurrio un error al intentar eliminar'}

        return result

    @classmethod
    def post(cls, values):
        opt = values['opt']
        args = values['values']
        return getattr(cls, f'_{opt}')(args)


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)
    sucursal = ForeignKeyField(Sucursales, null=True)
    rol = ForeignKeyField(Roles, null=True)

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

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

    def _get_is_admin(self, filters, user):
        return {'is_admin': user.es_admin}

    def _get_is_superadmin(self, filters, user):
        return {'is_superadmin': user.es_superusuario}

    @classmethod
    def get_data(cls, filters, user):
        method = f"_get_{filters['opt']}"
        return getattr(cls, method)(cls, filters, user)

    @classmethod
    def _set_branch_null(cls, args, user):
        id = args['id']
        values = {'sucursal': None}
        where = (Usuarios.id == id)
        q = (Usuarios
            .update(**values)
            .where(where)
        )
        result = bool(q.execute())
        response = {'ok': result}
        return response

    @classmethod
    def _set_branch(cls, args, user):
        id = args['id_user']
        values = {'sucursal': args['id_branch']}
        where = (Usuarios.id == id)
        q = (Usuarios
            .update(**values)
            .where(where)
        )
        result = bool(q.execute())
        response = {'ok': result}
        return response

    @classmethod
    def post(cls, values, user):
        opt = values['opt']
        args = utils.loads(values['values'])
        return getattr(cls, f'_{opt}')(args, user)

    @classmethod
    def add(cls, values):
        values['contraseña'] = values.pop('contra')
        try:
            Usuarios.create(**values)
            return {'ok': True}
        except Exception as e:
            log.error(e)
            msg = 'Ocurrio un error, consulta a soporte técnico'
            return {'ok': False, 'msg': msg}

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

    @classmethod
    def edit(self, values):
        # ~ print (values)
        id = int(values.pop('id'))
        try:
            if 'contra' in values:
                values['contraseña'] = values.pop('contra')
            q = Usuarios.update(**values).where(Usuarios.id==id)
            result = {'ok': bool(q.execute())}
        except IntegrityError:
            msg = 'El usuario ya existe'
            result = {'ok': False, 'msg': msg}

        return result

    @classmethod
    def actualizar(self, values, user):
        id = int(values['id'])
        v = {'0': False, '1': True}

        if values['field'] == 'es_superusuario' and not user.es_superusuario:
            msg = 'Solo un super usuario puede hacer este cambio'
            return {'ok': False, 'msg': msg}

        if values['field'] == 'es_activo':
            q = (Usuarios
                .update(**{'es_activo': v[values['value']]})
                .where(Usuarios.id==id))
            result = bool(q.execute())
        elif values['field'] == 'es_admin':
            q = (Usuarios
                .update(**{'es_admin': v[values['value']]})
                .where(Usuarios.id==id))
            result = bool(q.execute())
        elif values['field'] == 'es_superusuario':
            q = (Usuarios
                .update(**{'es_superusuario': v[values['value']]})
                .where(Usuarios.id==id))
            result = bool(q.execute())

        return {'ok': result}

    @classmethod
    def get_(cls, user):
        if user.es_superusuario:
            rows = Usuarios.select().dicts()
        else:
            filters = (Usuarios.es_superusuario == False)
            rows = Usuarios.select().where(filters).dicts()
        for row in rows:
            del row['contraseña']
        return tuple(rows)


class Productos(BaseModel):
    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')
    cantidad_empaque = DecimalField(default=0.0, max_digits=14, decimal_places=4,
        auto_round=True)
    is_discontinued = BooleanField(default=False)
    objeto_impuesto = TextField(default='02')

    class Meta:
        order_by = ('descripcion',)

    @classmethod
    def _get_by_key(cls, args, user):
        try:
            warehouse = user.sucursal.warehouse
        except:
            warehouse = None

        key = args['key']

        select = (
            Productos.id.alias('id_product'),
            Productos.clave,
            Productos.clave_sat,
            Productos.descripcion,
            SATUnidades.id.alias('unidad'),
            Productos.valor_unitario,
            Productos.descuento,
            Productos.inventario,
            Productos.existencia,
            Productos.objeto_impuesto,
        )
        where = (
            (Productos.es_activo==True) &
            (
                (Productos.clave==key) |
                (Productos.codigo_barras==key)
            )
        )
        row = (Productos
            .select(*select)
            .join(SATUnidades).switch(Productos)
            .where(where)
            .limit(1)
            .dicts()
        )

        if not len(row):
            return {'ok': False}

        obj = row[0]

        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==obj['id_product']).dicts()
        )

        if obj['inventario'] and not warehouse is None:
            obj['existencia'] = WareHouseProduct.get_exists_by_warehouse(
                obj['id_product'], warehouse)

        result = {'ok': True, 'row': obj, 'taxes': taxes}

        return result

    @classmethod
    def get_data(cls, values, user):
        opt = values.pop('opt')
        return getattr(cls, f'_get_{opt}')(values, user)

    @classmethod
    def next_key(cls):
        try:
            with database_proxy.transaction():
                value = (Productos
                    .select(fn.Max(cast(Productos.clave, 'int')).alias('fm'))
                    .order_by(SQL('fm'))
                    .scalar())
        except Exception as e:
            values = (Productos
                .select(Productos.clave)
                .order_by(Productos.clave)
                .tuples()
            )
            values = [int(v[0]) for v in values if v[0].isdigit()]
            value = 0
            if values:
                value = max(values)

        value = value or 0
        value += 1
        return {'value': value}

    @classmethod
    def get_by_key(cls, values):
        clave = values.get('key', '')
        row = (Productos
            .select(
                Productos.id.alias('id_product'),
                Productos.clave,
                Productos.clave_sat,
                Productos.descripcion,
                SATUnidades.id.alias('unidad'),
                Productos.valor_unitario,
                Productos.descuento,
                Productos.inventario,
                Productos.existencia,
                Productos.objeto_impuesto)
            .join(SATUnidades).switch(Productos)
            .where((Productos.es_activo==True) &
                ((Productos.clave==clave) | (Productos.codigo_barras==clave)))
            .dicts()
        )
        if len(row):
            id = row[0]['id_product']
            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())
            product = row[0]
            return {'ok': True, 'row': product, 'taxes': taxes}

        return {'ok': False}

    def _validate_import(self, product):
        product['categoria'] = Categorias.get_by_id(int(product['categoria']))
        product['unidad'] = SATUnidades.get_by_name(product['unidad'])
        product['inventario'] = bool(product['inventario'])
        if not product['inventario']:
            product['existencia'] = 0.0

        impuestos = product['impuestos'].split('|')
        if not impuestos:
            taxes = [SATImpuestos.select().where(SATImpuestos.id==6)]
        else:
            taxes = []
            for i in range(0, len(impuestos), 3):
                w = (
                    (SATImpuestos.key == impuestos[i]) &
                    (SATImpuestos.name == impuestos[i+1]) &
                    (SATImpuestos.tasa == float(impuestos[i+2]))
                )
                try:
                    taxes.append(SATImpuestos.get(w))
                except:
                    pass

        product['impuestos'] = taxes
        w = (Productos.clave==product['clave'])
        return product, w

    def _import(self):
        emisor = Emisor.select()[0]
        rows, msg = util.import_products(emisor.rfc)
        if not rows:
            return {'ok': False, 'msg': msg}

        cs = 0
        np = 0
        ap = 0
        for p in rows:
            data, w = self._validate_import(self, p)
            if data['unidad'] is None:
                msg = 'Producto: {} - No se encontró la unidad'.format(
                    data['clave'])
                log.error(msg)
                continue

            result = util.get_sat_key('productos', data['clave_sat'])
            if not result['ok']:
                msg = 'Producto: {} - Clave SAT incorrecta: {}'.format(
                    data['clave'], data['clave_sat'])
                log.error(msg)
                cs += 1
                continue

            # ~ print (data)
            taxes = data.pop('impuestos')
            try:
                with database_proxy.transaction():
                    if Productos.select().where(w).exists():
                        q = Productos.update(**data).where(w)
                        q.execute()
                        obj = Productos.get(w)
                        obj.impuestos = taxes
                        log.info('\tProducto actualizado: {}'.format(data['clave']))
                        ap += 1
                    else:
                        obj = Productos.create(**data)
                        obj.impuestos = taxes
                        log.info('\tProducto agregado: {}'.format(data['clave']))
                        np += 1
            except Exception as e:
                msg = 'Error al importar producto: {}'.format(data['clave'])
                log.error(msg)
                log.error(e)

        msg = 'Productos encontrados: {}<BR>'.format(len(rows))
        msg += 'Productos agregados: {}<BR>'.format(np)
        msg += 'Productos actualizados: {}<BR>'.format(ap)
        msg += 'Productos con problemas: {}<BR>'.format(len(rows) - np - ap)
        msg += 'Productos con clave SAT erronea: {}'.format(cs)
        return {'ok': True, 'msg': msg}

    @classmethod
    def opt(cls, values):
        if values['opt'] == 'import':
            return cls._import(cls)

        return {'ok': False, 'msg': 'Sin opción'}

    @classmethod
    def get_by(cls, values):
        clave = values.get('id', '')
        if clave:
            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.es_activo==True) &
                    ((Productos.id==clave) | (Productos.clave==clave)))
                .dicts())
            if len(row):
                id = row[0]['id']
                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.es_activo==True) &
                    ((Productos.descripcion.contains(name)) |
                    (Productos.clave.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,
                    Productos.codigo_barras,
                    Productos.cuenta_predial,
                    Productos.inventario,
                    Productos.existencia,
                    Productos.minimo,
                    Productos.objeto_impuesto,
                    # ~ Productos.cantidad_empaque.alias('cant_by_packing'),
                )
                .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.es_activo,
                Productos.clave_sat,
                Productos.clave,
                Productos.descripcion,
                SATUnidades.name.alias('unidad'),
                Productos.valor_unitario,
                Productos.existencia)
            .join(SATUnidades)
            .dicts()
        )
        return {'ok': True, 'rows': tuple(rows)}

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

        fields['cantidad_empaque'] = fields.pop('cant_by_packing', 0.0)
        fields.pop('precio_con_impuestos', '')
        fields['es_activo'] = fields.pop('es_activo_producto')
        fields['descripcion'] = 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,
                'es_activo': obj.es_activo,
                'clave': obj.clave,
                'clave_sat': obj.clave_sat,
                '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):
        values['cuenta_predial'] = values.get('cuenta_predial', '')
        values['codigo_barras'] = values.get('codigo_barras', '')

        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
            except Exception as e:
                msg = str(e)
                if 'productos_clave' in msg:
                    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,
                'es_activo': obj.es_activo,
                'clave': obj.clave,
                'clave_sat': obj.clave_sat,
                'descripcion': obj.descripcion,
                'unidad': obj.unidad.name,
                'valor_unitario': obj.valor_unitario,
                'existencia': obj.existencia,
            }
        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

        count = (TicketsDetalle
            .select(fn.COUNT(TicketsDetalle.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 WareHouseProduct(BaseModel):
    warehouse = ForeignKeyField(Almacenes)
    product = ForeignKeyField(Productos)
    exists = DecimalField(default=0.0, max_digits=18, decimal_places=2,
        auto_round=True)

    class Meta:
        indexes = (
            (('warehouse', 'product'), True),
        )

    @classmethod
    def _get_by_product(cls, args, user):
        id = int(args['id'])
        select = (
            Almacenes.id,
            Almacenes.name.alias('warehouse'),
            WareHouseProduct.exists,
        )
        where = (WareHouseProduct.product==id)
        rows = (WareHouseProduct
            .select(*select)
            .join(Almacenes)
            .switch(WareHouseProduct)
            .where(where)
            .dicts()
        )
        return tuple(rows)

    @classmethod
    def get_exists_by_warehouse(cls, product, warehouse):
        where = (
            (WareHouseProduct.product==product) &
            (WareHouseProduct.warehouse==warehouse)
        )
        exists = (WareHouseProduct
            .select(WareHouseProduct.exists)
            .where(where)
            .limit(1)
            .scalar()
        ) or 0
        return exists

    @classmethod
    def _get_exists(cls, args, user):
        id_product = int(args['id'])
        warehouse = None
        try:
            warehouse = user.sucursal.warehouse
        except:
            obj = Productos.get(id==id_product)
            return obj.existencia

        where = (
            (WareHouseProduct.product==id) &
            (WareHouseProduct.warehouse==warehouse)
        )
        exists = (WareHouseProduct
            .select(WareHouseProduct.exists)
            .where(where)
            .limit(1)
            .scalar()
        )
        return exists

    @classmethod
    def get_data(cls, values, user):
        opt = values.pop('opt')
        return getattr(cls, f'_get_{opt}')(values, user)

    @classmethod
    def update_exists(cls, args):
        if args['storage'] is None:
            return

        values = dict(
            warehouse = args['storage'],
            product = args['product'],
        )
        obj, created = WareHouseProduct.get_or_create(**values)
        cant = args['cant']
        if created:
            cant = float(cant)
        obj.exists += cant
        obj.save()
        return

    @classmethod
    def _adjust_stock(cls, args, user):
        cant = Decimal(args['cant'])
        fields = dict(
            warehouse = args['storage'],
            product = args['id_product'],
        )
        obj = WareHouseProduct.get(**fields)
        obj.exists += cant
        obj.save()

        total = (WareHouseProduct
            .select(fn.Sum(WareHouseProduct.exists)).alias('total')
            .where(WareHouseProduct.product==fields['product'])
            .limit(1)
            .scalar()
        )
        query = (Productos
            .update(existencia=total)
            .where(Productos.id==fields['product'])
        )
        query.execute()

        msg = 'ajustar stock'
        _save_log(user.usuario, msg, 'WHP')

        result = {'ok': True, 'row': {'existencia': total}}

        return result

    @classmethod
    def post(cls, values, user):
        opt = values['opt']
        args = utils.loads(values['values'])
        return getattr(cls, f'_{opt}')(args, user)


class RangosPrecios(BaseModel):
    producto = ForeignKeyField(Productos)
    descripcion = TextField(default='')
    desde = DecimalField(default=0.0, max_digits=18, decimal_places=6,
        auto_round=True)
    hasta = 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 = IntegerField(default=0)

    class Meta:
        order_by = ('producto',)


class Facturas(BaseModel):
    cliente = ForeignKeyField(Socios)
    version = TextField(default=PRE_DEFAULT['CFDI']['VERSION'])
    serie = TextField(default='')
    folio = BigIntegerField(default=0)
    fecha = DateTimeField(default=utils.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='')
    divisas = 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='')
    exportacion = TextField(default='01')
    receptor_regimen = TextField(default='')
    periodicidad = TextField(default='')

    class Meta:
        order_by = ('fecha',)

    @classmethod
    def validate_count_partners(cls, ids):
        """Validate if invoices IDs are of unique client"""

        filters = (Facturas.id.in_(tuple(ids.keys())))
        partners = (Facturas.select(fn.COUNT(Socios.rfc))
            .where(filters)
            .join(Socios).switch(Facturas)
            .group_by(Socios.rfc)
            .order_by(Socios.rfc).tuples())
        if len(partners) > 1:
            return False
        return True

    @classmethod
    def cancel(cls, args):
        obj = Facturas.get(Facturas.id==args['id'])
        if obj.uuid is None:
            obj.estatus = 'Cancelada'
            obj.cancelada = True
            obj.fecha_cancelacion = util.now()
            obj.save()
            msg = 'Factura cancelada correctamente'
            return {'ok': True, 'msg': msg, 'row': {'estatus': obj.estatus}}

        return cls._cancel_xml_sign(obj, args)

    @classmethod
    def _cancel_xml_sign(cls, invoice, args):
        if not invoice.version in CANCEL_VERSION:
            msg = 'Solo es posible cancelar CFDI >= 3.3'
            return {'ok': False, 'msg': msg}

        pac = utils.get_pac_by_rfc(invoice.xml)
        auth = Configuracion.get_({'fields': 'auth_by_pac', 'pac': pac})

        certificado = Certificado.get(Certificado.es_fiel==False)
        result = utils.cancel_xml_sign(invoice, args, auth, certificado)

        if result['ok']:
            invoice.estatus = 'Cancelada'
            invoice.error = ''
            invoice.cancelada = True
            invoice.fecha_cancelacion = result['date']
            invoice.acuse = result['acuse'] or ''
            cls._actualizar_saldo_cliente(cls, invoice, True)
            cls._update_inventory(cls, invoice, True)
            cls._uncancel_tickets(cls, invoice)
        else:
            invoice.error = result['msg']
        invoice.save()

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


    def _cancel_xml(self, id):
        msg = 'Factura cancelada correctamente'
        auth = Emisor.get_auth()
        certificado = Certificado.select()[0]
        obj = Facturas.get(Facturas.id==id)

        if obj.version == '3.2':
            msg = 'No es posible cancelar CFDI 3.2'
            return {'ok': False, 'msg': msg}

        data, result = util.cancel_xml(auth, obj.uuid, certificado)

        if data['ok']:
            obj.estatus = 'Cancelada'
            obj.error = ''
            obj.cancelada = True
            obj.fecha_cancelacion = result['date']
            obj.acuse = result['acuse'] or ''
            self._actualizar_saldo_cliente(self, obj, True)
            self._update_inventory(self, obj, True)
            self._uncancel_tickets(self, obj)
        else:
            obj.error = data['msg']
        obj.save()
        return data

    @utils.run_in_thread
    def _uncancel_tickets(self, invoice):
        query = (Tickets
            .update(estatus='Generado', cancelado=False, factura=None)
            .where(Tickets.factura==invoice)
        )
        query.execute()
        return

    def _get_filters(self, values):
        if 'start' in values and 'end' in values:
            filters = Facturas.fecha.between(
                utils.parse_date(values['start']),
                utils.parse_date(values['end'], True)
            )
        else:
            if values['year'] == '-1':
                fy = (Facturas.fecha.year > 0)
            else:
                fy = (Facturas.fecha.year == int(values['year']))
            if values['month'] == '-1':
                fm = (Facturas.fecha.month > 0)
            else:
                fm = (Facturas.fecha.month == int(values['month']))
            if values['day'] == '-1':
                filters = (fy & fm)
            else:
                fd = (Facturas.fecha.day == int(values['day']))
                filters = (fy & fm & fd)

        if 'client' in values:
            filters &= (Socios.nombre==values['client'])

        return filters

    def _get_invoices(self, filters, user):
        filter_serie = False

        filter_by_user = Configuracion.get_bool('chk_config_user_show_doc')
        if not filter_by_user:
            if not user.sucursal is None and not user.sucursal.serie_facturas is None:
                filter_serie = (Facturas.serie==user.sucursal.serie_facturas)

        if filter_serie:
            filters &= filter_serie

        rows = tuple(Facturas.select(
                Facturas.id,
                Facturas.serie,
                Facturas.folio,
                Facturas.uuid,
                Facturas.fecha,
                Facturas.tipo_comprobante,
                Facturas.estatus,
                case(Facturas.pagada, (
                    (True, 'Si'),
                    (False, 'No'),
                )).alias('paid'),
                Facturas.metodo_pago,
                Facturas.total,
                Facturas.moneda.alias('currency'),
                Facturas.total_mn,
                Socios.nombre.alias('cliente'))
            .where(filters)
            .join(Socios)
            .switch(Facturas).dicts()
            )
        return {'ok': True, 'rows': rows}

    def _get_by_dates(self, filters, user):
        filters = self._get_filters(self, filters)
        return self._get_invoices(self, filters, user)

    def _get_by_notes(self, filters, user):
        notes = filters['notes']
        filters = self._get_filters(self, filters)
        filters &= (Facturas.notas.contains(notes))
        return self._get_invoices(self, filters, user)

    @classmethod
    def get_by(cls, filters, user):
        return getattr(cls, f"_get_by_{filters['by']}")(cls, filters, user)

    @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)
        cls._sync_xml(cls, obj)
        return obj.xml, name

    @classmethod
    def get_notes(cls, id):
        obj = Facturas.get(Facturas.id==int(id))
        return {'notes': obj.notas}

    @classmethod
    def save_notes(cls, values):
        obj = Facturas.get(Facturas.id==int(values['id']))
        obj.notas = values.get('notes', '')
        obj.save()
        return {'ok': True, 'msg': 'Notas guardadas correctamente'}

    def _get_not_in_xml(self, invoice, emisor):
        pdf_from = Configuracion.get_('make_pdf_from') or '1'

        values = {'el.version': VERSION_EMPRESA_LIBRE}

        values['notas'] = invoice.notas
        values['fechadof'] = str(emisor.fecha_dof)
        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',
        }
        if invoice.metodo_pago:
            values['metododepago'] = 'Método de Pago: ({}) {}'.format(
                invoice.metodo_pago, mp[invoice.metodo_pago])

        if invoice.forma_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)

        # ~ use_packing = Configuracion.get_bool('chk_use_packing')
        # ~ if use_packing:
            # ~ w = FacturasDetalle.factura == invoice
            # ~ q = (FacturasDetalle
                    # ~ .select(FacturasDetalle.empaques)
                    # ~ .where(w)
                    # ~ .order_by(FacturasDetalle.id.asc())
                    # ~ .tuples())
            # ~ values['pakings'] = [str(int(r[0])) for r in q]

        return values

    def _get_not_in_xml2(self, invoice, data):
        fields = (
            Socios.calle,
            Socios.no_exterior,
            Socios.no_interior,
            Socios.colonia,
            Socios.municipio,
            Socios.estado,
            Socios.pais,
            Socios.correo_facturas,
            Socios.telefonos,
        )
        where = (Socios.id==invoice.cliente.id)
        partner = Socios.select(*fields).where(where).dicts()[0]
        data['receptor'].update(partner)
        return

    @classmethod
    def get_pdf(cls, id, rfc, sync=True):
        try:
            emisor = Emisor.select()[0]
        except IndexError:
            return b'', 'sin_datos_de_emisor.pdf'

        obj = Facturas.get(Facturas.id==id)
        name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc)
        if obj.uuid is None:
            return b'', name

        pdf_from = Configuracion.get_('make_pdf_from') or '1'
        values = cls._get_not_in_xml(cls, obj, emisor)

        #Tmp to v2
        data = util.get_data_from_xml(obj, values, pdf_from)

        data.update(utils.CfdiToDict(obj.xml).values)
        cls._get_not_in_xml2(cls, obj, data)

        doc = util.to_pdf(data, emisor.rfc, pdf_from=pdf_from)

        if sync:
            target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
            cls._sync_pdf(cls, doc, name, target)

        return doc, name

    def _get_concepto(self, node, currency):
        d = util.get_dict(node.attrib)
        concepto = {
            'clave': f"{d['noidentificacion']}<BR>({d['claveprodserv']})",
            'descripcion': self._get_description(self, node, d),
            'unidad': f"{d['unidad']}<BR>({d['claveunidad']})",
            'cantidad': d['cantidad'],
            'valorunitario': util.format_currency(d['valorunitario'], currency),
            'importe': util.format_currency(d['importe'], currency),
        }
        return concepto

    def _get_description(self, node, data):
        return data['descripcion']

    def _get_tax(self, node, data, currency):
        type_tax = 'Traslado'
        if 'Retenciones' in node.tag:
            type_tax = 'Retención'

        for n in node:
            d = util.get_dict(n.attrib)
            name = VALUES_PDF['TAX'].get(d['impuesto'])
            tasa = ''
            if 'tasaocuota' in d:
                tasa = f"{float(d['tasaocuota']):.2f}"
            title = f"{type_tax} {name} {tasa}"
            importe = util.format_currency(d['importe'], currency)
            data['totales'].append((title, importe))
        return

    def _get_others_values(self, invoice, emisor):
        data = {
            'rfc': emisor.rfc.lower(),
            'version': invoice.version,
            'cancelada': VALUES_PDF['CANCEL'].get(invoice.cancelada),
            'cfdi_notas': invoice.notas,
        }

        xml = util.parse_xml2(invoice.xml)
        d = util.get_dict(xml.attrib)
        d.pop('Certificado', '')
        currency = d.get('Moneda', 'MXN')

        data.update({f'cfdi_{k.lower()}': v for k, v in d.items()})

        data['cfdi_tipodecomprobante'] = \
            VALUES_PDF['TYPE'][data['cfdi_tipodecomprobante']]

        if data.get('cfdi_formapago', ''):
            data['cfdi_formapago'] = str(
                SATFormaPago.get_by_key(data['cfdi_formapago']))
        if data.get('cfdi_metodopago', ''):
            data['cfdi_metodopago'] = 'Método de Pago: ({}) {}'.format(
                data['cfdi_metodopago'],
                VALUES_PDF['METHOD'][data['cfdi_metodopago']])

        data['cfdi_moneda'] = str(SATMonedas.get_by_key(currency))
        data['cfdi_tipocambio'] = util.format_currency(
            data['cfdi_tipocambio'], currency, 4)
        data['cfdi_totalenletras'] = util.to_letters(
            float(data['cfdi_total']), currency)

        data['totales'] = [(
            'Subtotal', util.format_currency(data['cfdi_subtotal'], currency))]

        for node in xml:
            if 'Emisor' in node.tag:
                d = {f'emisor_{k.lower()}': v for k, v in node.attrib.items()}
                data.update(d)
            elif 'Receptor' in node.tag:
                d = {f'receptor_{k.lower()}': v for k, v in node.attrib.items()}
                data.update(d)
            elif 'Conceptos' in node.tag:
                data['conceptos'] = []
                for subnode in node:
                    concepto = self._get_concepto(self, subnode, currency)
                    data['conceptos'].append(concepto)
            elif 'Impuestos' in node.tag:
                for subnode in node:
                    self._get_tax(self, subnode, data, currency)
            elif 'Complemento' in node.tag:
                for sn in node:
                    if 'TimbreFiscalDigital' in sn.tag:
                        d = {f'timbre_{k.lower()}': v for k, v in sn.attrib.items()}
                        data.update(d)

        data['emisor_logo'] = data['emisor_rfc'].lower()
        data['emisor_regimenfiscal'] = str(
            SATRegimenes.get_by_key(data['emisor_regimenfiscal']))
        data['receptor_usocfdi'] = str(
            SATUsoCfdi.get_by_key(data['receptor_usocfdi']))

        data['totales'].append((
            'Total', util.format_currency(data['cfdi_total'], currency)))

        data['timbre_cadenaoriginal'] = (
            f"||{data['timbre_version']}|"
            f"{data['timbre_uuid']}|"
            f"{data['timbre_fechatimbrado']}|"
            f"{data['timbre_sellocfd']}|"
            f"{data['timbre_nocertificadosat']}||"
        )

        qr_data = (
            f"https://verificacfdi.facturaelectronica.sat.gob.mx/"
            f"default.aspx?&id={data['timbre_uuid']}&re={data['emisor_rfc']}"
            f"&rr={data['receptor_rfc']}&tt={data['cfdi_total']}"
            f"&fe={data['cfdi_sello'][-8:]}"
        )
        cbb = utils.get_qr(qr_data, 'png', True)
        data['cbb'] = f'data:image/png;base64,{cbb}'

        return data

    @classmethod
    def get_html(cls, id):
        try:
            emisor = Emisor.select()[0]
        except IndexError:
            return '', 'sin_datos_de_emisor.html'

        obj = Facturas.get(Facturas.id==id)
        name = '{}{}_{}.html'.format(obj.serie, obj.folio, obj.cliente.rfc)
        if obj.uuid is None:
            return '', name

        data = cls._get_others_values(cls, obj, emisor)
        doc = util.to_html(data)
        # ~ util.html_to_pdf(doc)

        return doc, name

    @classmethod
    def get_ods(cls, id, rfc):
        try:
            emisor = Emisor.select()[0]
        except IndexError:
            return b'', 'sin_datos_de_emisor.pdf'

        obj = Facturas.get(Facturas.id==id)
        name = '{}{}_{}.ods'.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)
        data.update(utils.CfdiToDict(obj.xml).values)
        doc = util.to_pdf(data, emisor.rfc, True)
        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 _sync(self, id, rfc):
        return Facturas.sync(id, rfc)

    @util.run_in_thread
    def _sync_pdf(self, pdf, name_pdf, target):
        # ~ auth = Emisor.get_auth()
        files = (
            (pdf, name_pdf, target),
        )
        util.sync_cfdi(files)
        return

    @util.run_in_thread
    def _sync_xml(self, obj):
        emisor = Emisor.select()[0]
        name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
        target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
        files = (
            (obj.xml, name_xml, target),
        )
        util.sync_cfdi(files)
        return

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

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

        if invoice.cliente.rfc == RFCS['PUBLIC']:
            return

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

        if cancel:
            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'})
        contra = Configuracion.get_('correo_contra')
        in_zip = Configuracion.get_bool('chk_config_send_zip')

        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}

        if in_zip:
            files = (cls.get_zip(id, rfc),)
        else:
            files = (cls.get_pdf(id, rfc), cls.get_xml(id))

        fields = util.make_fields(obj.xml)
        starttls = bool(int(values.get('correo_starttls', '0')))
        server = {
            'servidor': values['correo_servidor'],
            'puerto': values['correo_puerto'],
            'ssl': bool(int(values['correo_ssl'])),
            'starttls': starttls,
            'usuario': values['correo_usuario'],
            'contra': utils.decrypt(contra, rfc),
        }
        options = {
            'para': obj.cliente.correo_facturas,
            'copia': values.get('correo_copia', ''),
            'confirmar': util.get_bool(values.get('correo_confirmacion', '0')),
            '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}

    @classmethod
    def sync(cls, id, rfc):
        obj = Facturas.get(Facturas.id==id)
        if obj.uuid is None:
            msg = 'La factura no esta timbrada'
            return

        # ~ emisor = Emisor.select()[0]
        pdf, name_pdf = cls.get_pdf(id, rfc, False)
        name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
        target = rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
        files = (
            (obj.xml, name_xml, target),
            (pdf, name_pdf, target),
        )
        util.sync_cfdi(files)
        return

    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.in_(('I', 'ingreso'))) &
            (Facturas.saldo>0)
        )
        if ids:
            filtros &= (Facturas.id.not_in(ids))

        currency = case(Facturas.moneda, (
            ('peso', 'MXN'),
        ), Facturas.moneda)

        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,
                currency.alias('currency'),
                Facturas.total_mn,
                Facturas.saldo,
            )
            .where(filtros)
            .join(Socios)
            .switch(Facturas)
            .dicts()
        )
        return {'ok': True, 'rows': rows}

    def _get_reinvoice(self, id):
        obj = Facturas.get(Facturas.id==id)

        receptor = {
            'id': obj.cliente.id,
            'nombre': obj.cliente.nombre,
            'rfc': obj.cliente.rfc,
            'codigo_postal': obj.cliente.codigo_postal,
            'notas': obj.notas,
            'regimenes': SociosRegimenes.get_by_key(obj.cliente.id)
        }
        invoice = {
            'tipo_comprobante': obj.tipo_comprobante,
            'serie': Folios.get_id(obj.serie),
            'uso_cfdi': obj.uso_cfdi,
            'metodo_pago': obj.metodo_pago,
            'forma_pago': obj.forma_pago,
            'condicion_pago': obj.condiciones_pago,
        }

        products = FacturasDetalle.reinvoice(id)

        data = {'receptor': receptor, 'invoice': invoice, 'products': products}
        return data

    def _get_opt(self, values):
        if values['opt'] == 'foliocustom':
            return self._get_folio_custom(self, values)

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

        if values['opt'] == 'reinvoice':
            return self._get_reinvoice(self, int(values['id']))

        if values['opt'] == 'detalle':
            return FacturasDetalle.get_detalle(int(values['id']))

        if values['opt'] == 'statussat':
            return self.get_status_sat(int(values['id']))

        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,
                case(Facturas.pagada, (
                    (True, 'Si'),
                    (False, 'No'),
                )).alias('paid'),
                Facturas.total,
                Facturas.moneda.alias('currency'),
                Facturas.total_mn,
                Socios.nombre.alias('cliente'))
            .where(filters)
            .join(Socios)
            .switch(Facturas).dicts()
            )

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

    @classmethod
    def remove(cls, id, user):
        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()
        q = FacturasComplementos.delete().where(FacturasComplementos.factura==obj)
        q.execute()
        Tickets.uncancel(obj)

        m = 'B {}'.format(obj.id)
        _save_log(user.usuario, m, 'F')

        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 or inicio_serie > inicio:
            inicio = inicio_serie
        else:
            inicio += 1

        return inicio

    def _get_folio_custom(self, values):
        result = {'ok': False, 'msg': 'Folio libre para facturar'}
        try:
            folio = int(values['folio'])
        except ValueError:
            result['ok'] = True
            result['msg'] = 'Valor de folio inválido'
            return result

        serie = values['serie']

        result['ok'] = (Facturas
            .select()
            .where((Facturas.serie==serie) & (Facturas.folio==folio))
            .exists())

        if result['ok']:
            result['msg'] = 'Folio previamente usado'

        return result

    def _calculate_totals(self, invoice, products, tipo_comprobante, user):
        tax_locales = Configuracion.get_bool('chk_config_tax_locales')
        tax_locales_truncate = Configuracion.get_bool('chk_config_tax_locales_truncate')
        tax_decimals = Configuracion.get_bool('chk_config_tax_decimals')
        # ~ use_packing = Configuracion.get_bool('chk_use_packing')

        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None
        total_retenciones = None
        locales_traslados = 0
        locales_retenciones = 0

        warehouse = None
        try:
            warehouse = user.sucursal.warehouse
        except:
            pass

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

            product['unidad'] = SATUnidades.get(SATUnidades.id==product['unidad']).key
            product['clave'] = p.clave
            product['clave_sat'] = p.clave_sat
            product['cuenta_predial'] = p.cuenta_predial

            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)

            # ~ if use_packing and p.cantidad_empaque:
                # ~ product['empaques'] = utils.round_up(
                    # ~ cantidad / float(p.cantidad_empaque))

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

            descuento_cfdi += product['descuento']
            subtotal += product['importe']

            if id_student:
                student = Alumnos.get(Alumnos.id==id_student)
                product['alumno'] = str(student)
                product['curp'] = student.curp
                product['nivel'] = student.grupo.nivel.nombre
                product['autorizacion'] = student.grupo.nivel.autorizacion.strip()

            product['warehouse'] = warehouse

            FacturasDetalle.create(**product)

            if tipo_comprobante == 'T':
                continue

            base = product['importe'] - product['descuento']
            for tax in p.impuestos:
                if tax_locales and tax.tipo == 'R' and tax.key == '000':
                    base = product['importe']

                if tax_decimals:
                    impuesto_producto = round(float(tax.tasa) * base, DECIMALES_TAX)
                else:
                    impuesto_producto = round(float(tax.tasa) * base, DECIMALES)

                if tax.key == '000' and tax_locales_truncate:
                    impuesto_producto = util.truncate(float(tax.tasa) * base)

                if tax.tipo == 'T' and tax.key != '000':
                    total_trasladados = (total_trasladados or 0) + impuesto_producto
                elif tax.tipo == 'R' and tax.key != '000':
                    total_retenciones = (total_retenciones or 0) + impuesto_producto
                elif tax.tipo == 'T' and tax.key == '000':
                    locales_traslados += impuesto_producto
                elif tax.tipo == 'R' and tax.key == '000':
                    locales_retenciones += impuesto_producto

                if tax.id in totals_tax:
                    totals_tax[tax.id].base += base
                    totals_tax[tax.id].suma_impuestos += impuesto_producto
                else:
                    tax.base = base
                    tax.suma_impuestos = impuesto_producto
                    totals_tax[tax.id] = tax

        if tipo_comprobante == 'T':
            subtotal = 0.0
            descuento = 0.0
        else:
            for tax in totals_tax.values():
                if tax.tipo == 'E':
                    continue

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

        if not total_trasladados is None:
            total_trasladados = round(total_trasladados, DECIMALES)
        if not total_retenciones is None:
            total_retenciones = round(total_retenciones, DECIMALES)

        total = subtotal - descuento_cfdi + \
            (total_trasladados or 0) - (total_retenciones or 0) \
            + locales_traslados - locales_retenciones
        total = round(total, DECIMALES)
        # ~ exchange_rate = float(invoice.tipo_cambio)
        total_mn = round(total * invoice.tipo_cambio, DECIMALES)
        # ~ total_mn = round(total * exchange_rate, DECIMALES)
        data = {
            'subtotal': subtotal,
            '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

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

        info = {}
        values = utils.loads(valores)
        info['IdCCP'] = util.get_idccp()
        info['TranspInternac'] = values['TranspInternac']
        autotransporte = {
            'PermSCT': values['autotransporte'].pop('PermSCT'),
            'NumPermisoSCT': values['autotransporte'].pop('NumPermisoSCT'),
        }
        identificacion = values['autotransporte']
        if 'PesoBrutoVehicular' in identificacion \
            and isinstance(identificacion['PesoBrutoVehicular'], float):
            identificacion['PesoBrutoVehicular'] = f"{identificacion['PesoBrutoVehicular']:.2f}"
        info['autotransporte'] = autotransporte
        info['autotransporte']['identificacion'] = identificacion
        info['autotransporte']['seguros'] = values['seguros']
        info['autotransporte']['remolques'] = values['remolques']

        total_distance = 0.00
        total_weight = 0.00
        mercancias = values['mercancias']
        for mercancia in mercancias:
            total_weight += float(mercancia['PesoEnKg'])
            if isinstance(mercancia['PesoEnKg'], (int, float)):
                mercancia['PesoEnKg'] = f"{mercancia['PesoEnKg']:.2f}"
            mercancia['Cantidad'] = f"{mercancia['Cantidad']:.2f}"

        info['mercancias'] = {}
        info['mercancias']['UnidadPeso'] = values['unidad_peso']
        info['mercancias']['PesoBrutoTotal'] = f"{total_weight:.2f}"
        info['mercancias']['NumTotalMercancias'] = f"{len(mercancias):,}"
        info['mercancias']['mercancias'] = mercancias

        ubicaciones = values['ubicaciones']
        for ubicacion in ubicaciones:
            if 'DistanciaRecorrida' in ubicacion:
                if ubicacion['TipoUbicacion'] == 'Origen':
                    del ubicacion['DistanciaRecorrida']
                elif ubicacion['DistanciaRecorrida']:
                    total_distance += float(ubicacion['DistanciaRecorrida'])
                    if isinstance(ubicacion['DistanciaRecorrida'], (int, float)):
                        ubicacion['DistanciaRecorrida'] = f"{ubicacion['DistanciaRecorrida']:.2f}"

            municipio = ubicacion.pop('Municipio')
            estado = ubicacion.pop('Estado')
            pais = ubicacion.pop('Pais')
            cp = ubicacion.pop('CodigoPostal')
            if municipio and estado and pais and cp:
                ubicacion['domicilio'] = {
                    'Municipio': municipio,
                    'Estado': estado,
                    'Pais': pais,
                    'CodigoPostal': cp,
                    }

        info['TotalDistRec'] = f"{total_distance:.2f}"
        info['ubicaciones'] = ubicaciones
        info['tiposfigura'] = values['tiposfigura']

        # ~ print(2, info)
        data = {
            'factura': invoice,
            'nombre': 'cartaporte',
            'valores': utils.dumps(info),
        }
        FacturasComplementos.create(**data)
        return

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

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

    def _get_serie(self, user, default_serie):
        if user.sucursal is None:
            return default_serie

        return user.sucursal.serie_facturas or default_serie

    @classmethod
    def add(cls, values, user):
        productos = util.loads(values.pop('productos'))
        relacionados = util.loads(values.pop('relacionados'))
        ine = values.pop('ine', {})
        cartaporte = values.pop('cartaporte', {})
        comercioe = values.pop('comercioe', {})
        tipo_comprobante = values['tipo_comprobante']
        folio_custom = values.pop('folio_custom', '')
        divisas = values.pop('divisas', '')
        if Configuracion.get_bool('chk_config_divisas'):
            divisas = divisas.lower()
            if divisas == 'ninguna':
                divisas = ''
            values['divisas'] = divisas

        leyendas_fiscales = utils.loads(values.pop('leyendas_fiscales', '[]'))

        values['fecha'] = utils.now()
        date_invoice = values.pop('date')
        if Configuracion.get_bool('chk_config_change_date_invoice'):
            values['fecha'] = utils.to_date(date_invoice)
            days = (utils.now() - values['fecha']).days
            if days > 2:
                msg = 'Fecha inválida'
                data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
                return data

        emisor = Emisor.select()[0]
        values['fecha'] = utils.adjust_time(values['fecha'], emisor.hours)

        values['serie'] = cls._get_serie(cls, user, values['serie'])
        if Configuracion.get_bool('chk_folio_custom') and folio_custom:
            fc = {'folio': folio_custom, 'serie': values['serie']}
            result = cls._get_folio_custom(cls, fc)
            if result['ok']:
                data = {'ok': False, 'row': {}, 'new': True, 'msg': result['msg']}
                return data
            values['folio'] = int(folio_custom)
        else:
            values['folio'] = cls._get_folio(cls, values['serie'])
        values['tipo_cambio'] = round(float(values['tipo_cambio']), 4)
        values['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
        values['anticipo'] = util.get_bool(values['anticipo'])
        values['donativo'] = util.get_bool(values['donativo'])

        if tipo_comprobante == 'T':
            values['metodo_pago'] = ''
            values['forma_pago'] = ''
            values['moneda'] = CARTA_PORTE['MONEDA']
            self_client = Socios.get(Socios.rfc==emisor.rfc)
            if values['cliente'] != self_client.id:
                values['cliente'] = self_client

        with database_proxy.atomic() as txn:
            # ~ print('VALUES\n\n', values)
            obj = Facturas.create(**values)
            totals = cls._calculate_totals(
                cls, obj, productos, tipo_comprobante, user)
            cls._guardar_relacionados(cls, obj, relacionados)
            cls._guardar_ine(cls, obj, ine)
            cls._save_cartaporte(cls, obj, cartaporte)
            cls._save_comercioe(cls, obj, comercioe)
            cls._save_leyendas_fiscales(cls, obj, leyendas_fiscales)
            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 = 'G {}'.format(obj.id)
        _save_log(user.usuario, msg, 'F')

        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,
            'paid': 'No',
            'total': obj.total,
            'currency': obj.moneda,
            'total_mn': obj.total_mn,
            'cliente': obj.cliente.nombre,
        }
        data = {'ok': True, 'row': row, 'new': True, 'error': False, 'msg': msg}
        return data

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

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

    def _make_xml(self, invoice):
        tax_decimals = Configuracion.get_bool('chk_config_tax_decimals')
        decimales_precios = Configuracion.get_bool('chk_config_decimales_precios')
        invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket')
        is_global = bool(invoice.periodicidad)
        base_iva_exento = 0.0

        data_global = {}
        if is_global:
            values = invoice.periodicidad.split('|')
            data_global['Periodicidad'] = values[0]
            data_global['Meses'] = values[1]
            data_global['Año'] = values[2]

        frm_vu = FORMAT
        if decimales_precios:
            frm_vu = FORMAT_PRECIO
        tmp = 0
        emisor = Emisor.select()[0]
        certificado = Certificado.get(Certificado.es_fiel==False)

        is_edu = False
        comprobante = {}
        relacionados = {}
        donativo = {}
        complementos = FacturasComplementos.get_by_invoice(invoice)
        comprobante['divisas'] = invoice.divisas
        if invoice.divisas:
            complementos['divisas'] = True

        if 'leyendas' in complementos:
            ids = complementos['leyendas']
            complementos['leyendas'] = SATLeyendasFiscales.get_by_id(ids)

        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['SubTotal'] = FORMAT.format(invoice.subtotal)
        comprobante['Moneda'] = invoice.moneda
        comprobante['TipoCambio'] = '1'
        if comprobante['Moneda'] != 'MXN':
            comprobante['TipoCambio'] = FORMAT_TAX.format(invoice.tipo_cambio)
        comprobante['Total'] = FORMAT.format(invoice.total)
        comprobante['TipoDeComprobante'] = invoice.tipo_comprobante
        if invoice.metodo_pago:
            comprobante['MetodoPago'] = invoice.metodo_pago
        comprobante['LugarExpedicion'] = invoice.lugar_expedicion
        if invoice.descuento:
            comprobante['Descuento'] = FORMAT.format(invoice.descuento)

        if invoice.tipo_comprobante == 'T':
            comprobante['SubTotal'] = '0'
            comprobante['Total'] = '0'
            del comprobante['FormaPago']
            del comprobante['TipoCambio']

        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,
            'DomicilioFiscalReceptor': invoice.cliente.codigo_postal,
            'RegimenFiscalReceptor': invoice.receptor_regimen
        }

        if invoice.cliente.tipo_persona == 4:
            if invoice.cliente.pais:
                receptor['ResidenciaFiscal'] = invoice.cliente.pais
            if invoice.cliente.id_fiscal:
                receptor['NumRegIdTrib'] = invoice.cliente.id_fiscal

        conceptos = []

        rows = FacturasDetalle.select().where(FacturasDetalle.factura==invoice)
        for row in rows:
            object_tax = '02'
            if not row.producto is None:
                object_tax = row.producto.objeto_impuesto

            if is_global:
                key_sat = row.clave_sat
                key = row.clave
            else:
                key_sat = row.producto.clave_sat
                key = row.producto.clave
            concepto = {
                'ClaveProdServ': key_sat,
                'NoIdentificacion': key,
                'Cantidad': FORMAT.format(row.cantidad),
                'ClaveUnidad': row.unidad,
                'Descripcion': row.descripcion,
                'ValorUnitario': frm_vu.format(row.valor_unitario),
                'Importe': FORMAT.format(row.importe),
                'ObjetoImp': object_tax,
            }
            if not is_global:
                concepto['Unidad'] = SATUnidades.get(SATUnidades.key==row.unidad).name[:20]

            if row.descuento:
                concepto['Descuento'] = FORMAT.format(row.descuento)

            if row.cuenta_predial:
                concepto['CuentaPredial'] = row.cuenta_predial

            if row.pedimento:
                concepto['Pedimento'] = row.pedimento

            if row.autorizacion:
                is_edu = True
                concepto['student'] = {
                    'version': '1.0',
                    'nombreAlumno': row.alumno,
                    'CURP': row.curp,
                    'nivelEducativo': row.nivel,
                    'autRVOE': row.autorizacion,
                }

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

            if invoice.tipo_comprobante != 'T':
                if is_global:
                    ticket = None
                    try:
                        where = (
                            (fn.Concat(Tickets.serie, Tickets.folio)==row.clave) &
                            (Tickets.estatus=='Facturado')
                        )
                        ticket = (Tickets
                            .get(where)
                        )
                        product_taxes = (TicketsImpuestos
                            .select()
                            .where(TicketsImpuestos.ticket==ticket)
                        )
                    except Tickets.DoesNotExist:
                        product_taxes = row.producto.impuestos
                else:
                    product_taxes = row.producto.impuestos

                if object_tax == '02':
                    for impuesto in product_taxes:
                        base = float(row.importe - row.descuento)
                        if is_global and not ticket is None:
                            base = float(impuesto.base)
                            impuesto = impuesto.impuesto

                        if impuesto.tipo == 'E':
                            tax = {
                                'Base': FORMAT.format(base),
                                'Impuesto': '002',
                                'TipoFactor': 'Exento',
                            }
                            traslados.append(tax)
                            base_iva_exento += base
                            continue

                        if impuesto.key == '000':
                            continue

                        tasa = float(impuesto.tasa)

                        if tax_decimals:
                            import_tax = round(tasa * base, DECIMALES_TAX)
                            tmp += import_tax
                            xml_importe = FORMAT_TAX.format(import_tax)
                        else:
                            import_tax = round(tasa * base, DECIMALES)
                            xml_importe = FORMAT.format(import_tax)

                        tipo_factor = 'Tasa'
                        if impuesto.factor != 'T':
                            tipo_factor = 'Cuota'

                        tasa_cuota = str(impuesto.tasa)
                        if emisor['RegimenFiscal'] == RESICO \
                            and len(emisor['Rfc']) == 13 \
                            and len(receptor['Rfc']) == 12 \
                            and tasa_cuota == '0.012500':
                            tasa_cuota = '0.0125'

                        tax = {
                            "Base": FORMAT.format(base),
                            "Impuesto": impuesto.key,
                            "TipoFactor": tipo_factor,
                            "TasaOCuota": tasa_cuota,
                            "Importe": xml_importe,
                        }
                        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_decimals:
                xml_importe = FORMAT_TAX.format(tax.importe)
                xml_tax_base = FORMAT_TAX.format(tax.base)
            else:
                xml_importe = FORMAT.format(tax.importe)
                xml_tax_base = FORMAT.format(tax.base)

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

        if base_iva_exento:
            traslado = {
                'Base': FORMAT.format(base_iva_exento),
                'Impuesto': '002',
                'TipoFactor': 'Exento',
            }
            traslados.append(traslado)

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

        if invoice.tipo_comprobante == 'T':
            impuestos = {}

        data = {
            'comprobante': comprobante,
            'relacionados': relacionados,
            'emisor': emisor,
            'receptor': receptor,
            'conceptos': conceptos,
            'impuestos': impuestos,
            'donativo': donativo,
            'edu': is_edu,
            'complementos': complementos,
            'global': data_global,
        }

        return utils.make_xml(data, certificado)

    @classmethod
    def get_status_sat(cls, id):
        obj = Facturas.get(Facturas.id == id)
        estatus_sat = utils.get_status_sat(obj.xml)
        if obj.estatus_sat != estatus_sat:
            obj.estatus_sat = estatus_sat
            obj.save()

        if obj.estatus_sat == 'Vigente' and obj.estatus == 'Cancelada':
            days = utils.get_days(obj.fecha_cancelacion)
            if days > 3:
                estatus_sat = 'uncancel'

        return estatus_sat

    @classmethod
    def get_verify_sat(cls, id):
        emisor = Emisor.select()[0].rfc
        obj = Facturas.get(Facturas.id == id)
        xml = util.parse_xml(obj.xml)
        sello = xml.attrib.get('sello', xml.attrib.get('Sello'))[-8:]
        data = {
            'url': 'https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?',
            'uuid': '&id={}'.format(obj.uuid),
            'emisor': '&re={}'.format(emisor),
            'receptor': '&rr={}'.format(obj.cliente.rfc),
            'total': '&tt={}'.format(str(obj.total)),
            'sello': '&fe={}'.format(sello),
        }
        result = {
            'url': '{url}{uuid}{emisor}{receptor}{total}{sello}'.format(**data)
        }
        return result

    @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, values, user):
        id = int(values['id'])
        update = util.loads(values.get('update', 'true'))

        rfc = Emisor.select()[0].rfc
        obj = Facturas.get(Facturas.id == id)
        xml = cls._make_xml(cls, obj)

        if obj.cliente.rfc == RFCS['PUBLIC']:
            update = False

        enviar_correo = util.get_bool(Configuracion.get_('correo_directo'))
        auth = Configuracion.get_({'fields': 'pac_auth'})

        anticipo = False
        msg = 'Factura timbrada correctamente'
        result = utils.xml_stamp(xml, auth)

        if result['ok']:
            cfdi_uuid = result['uuid']
            values = dict(
                xml = result['xml'],
                uuid = cfdi_uuid,
                fecha_timbrado = result['date'],
                estatus = 'Timbrada',
                error = '',
            )
            q = (Facturas
                .update(**values)
                .where(Facturas.id == id)
            )
            q.execute()

            row = {'uuid': cfdi_uuid, 'estatus': 'Timbrada'}
            if enviar_correo:
                cls._send(cls, id, rfc)
            if obj.tipo_comprobante == 'I' and obj.tipo_relacion == '07':
                anticipo = True
            cls._actualizar_saldo_cliente(cls, obj)
            if update:
                cls._update_inventory(cls, obj, user=user)
            cls._sync(cls, id, rfc)

            m = 'T {}'.format(obj.id)
            _save_log(user.usuario, m, 'F')
        else:
            msg = result['error']
            obj.estatus = 'Error'
            obj.xml = xml
            obj.error = msg
            obj.save()
            row = {'estatus': 'Error'}

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

        return result

    def _validate_import_32(self, data, sxml):
        currencies = {
            'Peso Mexicano': 'MXN',
        }
        regimenes = {
            'REGIMEN GENERAL DE LEY PERSONAS MORALES': '601',
        }
        # ~ print(data)
        invoice = data['invoice']
        receptor = data['receptor']
        name = receptor.get('nombre', '')
        tipo_persona = 1
        if receptor['rfc'] == 'XEXX010101000':
            tipo_persona = 4
        elif receptor['rfc'] == RFCS['PUBLIC']:
            tipo_persona = 3
        elif len(receptor['rfc']) == 12:
            tipo_persona = 2
        new = {
            'tipo_persona': tipo_persona,
            'rfc': receptor['rfc'],
            'slug': util.to_slug(name),
            'nombre': name,
            'es_cliente': True,
        }
        cliente, _ = Socios.get_or_create(**new)

        tipo_cambio = float(invoice.get('TipoCambio', '1.0'))
        total = float(invoice['Total'])
        invoice = {
            'cliente': cliente,
            'version': invoice['version'],
            'serie': invoice.get('serie', ''),
            'folio': int(invoice.get('folio', '0')),
            'fecha': invoice['fecha'],
            'fecha_timbrado': invoice['FechaTimbrado'],
            'forma_pago': invoice['formaDePago'],
            'condiciones_pago': invoice.get('CondicionesDePago', ''),
            'subtotal': float(invoice['SubTotal']),
            'descuento': float(invoice.get('Descuento', '0.0')),
            'moneda': currencies[invoice['Moneda']],
            'tipo_cambio': tipo_cambio,
            'total': total,
            'saldo': total,
            'total_mn': round(float(total * tipo_cambio), DECIMALES),
            'tipo_comprobante': invoice['TipoDeComprobante'],
            'metodo_pago': invoice['metodoDePago'],
            'lugar_expedicion': invoice['LugarExpedicion'],
            # ~ 'uso_cfdi': invoice['UsoCFDI'],
            'total_retenciones': float(invoice.get('TotalImpuestosRetenidos', '0.0')),
            'total_trasladados': float(invoice.get('TotalImpuestosTrasladados', '0.0')),
            'xml': sxml,
            'uuid': invoice['uuid'],
            'estatus': 'Importada',
            'regimen_fiscal': regimenes[invoice['regimen_fiscal']],
            'pagada': False,
            'tipo_relacion': invoice.get('TipoRelacion', '')
        }
        # ~ donativo = BooleanField(default=False)

        conceptos = []
        for concepto in data['conceptos']:
            valor_unitario = float(concepto['ValorUnitario'])
            descuento = float(concepto.get('Descuento', '0.0'))
            c = {
                'cantidad': float(concepto['Cantidad']),
                'valor_unitario': valor_unitario,
                'descuento': descuento,
                'precio_final': round(valor_unitario - descuento, DECIMALES),
                'importe': float(concepto['Importe']),
                'descripcion': concepto['Descripcion'],
                'unidad': concepto.get('Unidad', ''),
                'clave': concepto.get('NoIdentificacion', ''),
                'clave_sat': '01010101',
            }
            conceptos.append(c)

        new = {
            'invoice': invoice,
            'conceptos': conceptos,
        }
        return True, new

    def _validate_import(self, data, sxml):
        try:
            emisor = Emisor.select()[0]
        except IndexError:
            msg = 'Falta Emisor'
            log.error(msg)
            return False, {}

        if not DEBUG:
            if emisor.rfc != data['emisor']['rfc']:
                msg = 'El CFDI no es del Emisor'
                log.error(msg)
                return False, {}

        invoice = data['invoice']

        w = (Facturas.uuid==invoice['uuid'])
        if Facturas.select().where(w).exists():
            msg = 'Factura ya existe: {}'.format(invoice['uuid'])
            log.error(msg)
            return False, {}

        if invoice['version'] != '3.3':
            if invoice['version'] != '3.2':
                msg = 'CFDI no soportado'
                log.error(msg)
                return False, {}

            return self._validate_import_32(self, data, sxml)

        receptor = data['receptor']
        name = receptor.get('nombre', '')
        tipo_persona = 1
        if receptor['rfc'] == 'XEXX010101000':
            tipo_persona = 4
        elif receptor['rfc'] == RFCS['PUBLIC']:
            tipo_persona = 3
        elif len(receptor['rfc']) == 12:
            tipo_persona = 2
        new = {
            'tipo_persona': tipo_persona,
            'rfc': receptor['rfc'],
            'slug': util.to_slug(name),
            'nombre': name,
            'es_cliente': True,
        }
        cliente, _ = Socios.get_or_create(**new)

        tipo_cambio = float(invoice.get('TipoCambio', '1.0'))
        total = float(invoice['Total'])
        invoice = {
            'cliente': cliente,
            'version': invoice['version'],
            'serie': invoice.get('serie', ''),
            'folio': int(invoice.get('folio', '0')),
            'fecha': invoice['fecha'],
            'fecha_timbrado': invoice['FechaTimbrado'],
            'forma_pago': invoice['FormaPago'],
            'condiciones_pago': invoice.get('CondicionesDePago', ''),
            'subtotal': float(invoice['SubTotal']),
            'descuento': float(invoice.get('Descuento', '0.0')),
            'moneda': invoice['Moneda'],
            'tipo_cambio': tipo_cambio,
            'total': total,
            'saldo': total,
            'total_mn': round(float(total * tipo_cambio), DECIMALES),
            'tipo_comprobante': invoice['TipoDeComprobante'],
            'metodo_pago': invoice['MetodoPago'],
            'lugar_expedicion': invoice['LugarExpedicion'],
            'uso_cfdi': invoice['UsoCFDI'],
            'total_retenciones': float(invoice.get('TotalImpuestosRetenidos', '0.0')),
            'total_trasladados': float(invoice.get('TotalImpuestosTrasladados', '0.0')),
            'xml': sxml,
            'uuid': invoice['uuid'],
            'estatus': 'Importada',
            'regimen_fiscal': invoice['RegimenFiscal'],
            'pagada': False,
            'tipo_relacion': invoice.get('TipoRelacion', '')
        }
        # ~ donativo = BooleanField(default=False)

        conceptos = []
        for concepto in data['conceptos']:
            valor_unitario = float(concepto['ValorUnitario'])
            descuento = float(concepto.get('Descuento', '0.0'))
            clave = concepto.get('NoIdentificacion', '')
            producto = None
            if Productos.select().where(Productos.clave==clave).exists():
                producto = Productos.get(Productos.clave==clave)
            c = {
                'producto': producto,
                'cantidad': float(concepto['Cantidad']),
                'valor_unitario': valor_unitario,
                'descuento': descuento,
                'precio_final': round(valor_unitario - descuento, DECIMALES),
                'importe': float(concepto['Importe']),
                'descripcion': concepto['Descripcion'],
                'unidad': concepto.get('Unidad', ''),
                'clave': clave,
                'clave_sat': concepto['ClaveProdServ'],
            }
            conceptos.append(c)

        new = {
            'invoice': invoice,
            'conceptos': conceptos,
        }
        return True, new

    @classmethod
    def import_cfdi(cls, xml, sxml):
        result = {'status': 'error'}
        data = util.ImportCFDI(xml).get_data()
        ok, new = cls._validate_import(cls, data, sxml)
        if not ok:
            return result

        with database_proxy.atomic() as txn:
            obj = Facturas.create(**new['invoice'])
            for product in new['conceptos']:
                product['factura'] = obj
                FacturasDetalle.create(**product)

        return {'status': 'server'}

    def _set_invoices_payed(self, ids, user):
        ids = util.loads(ids)
        ok_ids = []
        for id in ids:
            with database_proxy.atomic() as txn:
                obj = Facturas.get(Facturas.id==id)
                obj.pagada = True
                saldo = obj.saldo
                obj.saldo = 0.00
                obj.save()
                q = (Socios
                    .update(saldo_cliente=Socios.saldo_cliente - saldo)
                    .where(Socios.id==obj.cliente.id)
                )
                q.execute()
                ok_ids.append(id)

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

        return {'ok': False}

    @classmethod
    def opt(cls, args, user):
        if args['opt'] == 'invoicepayed':
            return cls._set_invoices_payed(cls, args['ids'], user)

        if args['opt'] == 'timbrar':
            return cls.timbrar(args, user)

        if args['opt'] == 'cancel':
            admin_cancel = Configuracion.get_bool('chk_cancel_invoices_by_admin')
            if admin_cancel and not user.es_admin:
                msg = 'Solo un admin puede cancelar'
                result = {'ok': False, 'msg': msg, 'row': {}}
                return result

            values = utils.loads(args['values'])
            result = cls.cancel(values)
            if result['ok']:
                m = 'C {}'.format(values['id'])
                _save_log(user.usuario, m, 'F')
            return result

        return {'ok': False}

    # ~ v2
    @utils.run_in_thread
    def _update_inventory(self, invoice, cancel=False, user=None):
        if invoice.tipo_comprobante != 'I':
            return

        if invoice.cliente.rfc == RFCS['PUBLIC']:
            return

        warehouse = None
        try:
            warehouse = user.sucursal.warehouse
        except:
            pass

        products = FacturasDetalle.get_by_invoice(invoice.id)
        for p in products:
            if p.producto is None:
                continue
            if p.producto.inventario:
                cant = Decimal(p.cantidad)
                if cancel:
                    cant *= -1
                    warehouse = p.warehouse
                p.producto.existencia -= cant
                p.producto.save()

                if warehouse is None:
                    continue

                fields = (
                    WareHouseProduct.warehouse==warehouse,
                    WareHouseProduct.product==p.producto,
                )

                obj = WareHouseProduct.get(*fields)
                obj.exists -= cant
                obj.save()
        return

    @utils.run_in_thread
    def _update_client_balance(self, invoice, cancel=False):
        if invoice.tipo_comprobante == 'T':
            return

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

        if invoice.cliente.rfc == RFCS['PUBLIC']:
            return

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

        if cancel:
            importe *= -1

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

    def _put_uncancel(self, args, user):
        id = int(args['id'])
        obj = Facturas.get(Facturas.id==id)
        obj.estatus = 'Timbrada'
        obj.error = ''
        obj.cancelada = False
        obj.fecha_cancelacion = None
        obj.acuse = ''
        self._update_client_balance(self, obj)
        self._update_inventory(self, obj)
        obj.save()
        msg = 'Factura actualizada correctamente'
        result = {'result': True, 'msg': msg, 'values': {'estatus': 'Timbrada'}}
        return result

    @classmethod
    def put(cls, args, user):
        return getattr(cls, f"_put_{args['opt']}")(cls, args, user)


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

    @util.run_in_thread
    def _send_in_thread(self, id, obj, values):
        log.info('Generando PDF...')
        files = (self.get_pdf(id),)
        log.info('PDF Generado...')

        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'],
            'confirmar': util.get_bool(values.get('correo_confirmacion', '0')),
            'asunto': asunto,
            'mensaje': util.make_info_mail(values['correo_mensaje'], fields),
            'files': files,
        }
        data= {
            'server': server,
            'options': options,
        }
        log.info('Enviando prefactura...')
        result = util.send_mail(data)
        log.info('Prefactura enviada...')
        return

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

        rows = PreFacturasDetalle.count(id)
        if rows > 300:
            cls._send_in_thread(cls, id, obj, values)
            msg = 'Enviando correo...'
            return {'ok': True, '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)

        contra = Configuracion.get_('correo_contra')
        rfc = Emisor.select()[0].rfc

        asunto = 'Enviamos la prefactura: PRE-{}'.format(obj.folio)
        starttls = bool(int(values.get('correo_starttls', '0')))
        server = {
            'servidor': values['correo_servidor'],
            'puerto': values['correo_puerto'],
            'ssl': bool(int(values['correo_ssl'])),
            'starttls': starttls,
            'usuario': values['correo_usuario'],
            'contra': utils.decrypt(contra, rfc),
        }
        options = {
            'para': obj.cliente.correo_facturas,
            'copia': values['correo_copia'],
            'confirmar': util.get_bool(values.get('correo_confirmacion', '0')),
            '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
        data['donativo'] = obj['donativo']

        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'] = PRE_DEFAULT['CFDI']['VERSION']
        data['comprobante']['folio'] = str(data['comprobante']['folio'])
        data['comprobante']['seriefolio'] = '{}-{}'.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'] = {}

        data['donataria'] = {}
        data['ine'] = {}

        return data

    @util.run_in_thread
    def _get_pdf_in_thread(self, id):
        obj = PreFacturas.get(PreFacturas.id==id)
        name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc)
        data = self._get_info_to_pdf(self, id)
        doc = util.to_pdf(data, data['emisor']['rfc'])

        emisor = Emisor.select()[0]
        target = emisor.rfc + '/Prefacturas/'
        files = (
            (doc, name, target),
        )
        util.sync_cfdi(files)
        return

    @classmethod
    def get_pdf_in_thread(cls, id):
        return cls._get_pdf_in_thread(cls, id)

    @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)
        data['informacion_global'] = {}
        doc = util.to_pdf(data, data['emisor']['rfc'])
        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.alias('year'))
            .group_by(PreFacturas.fecha.year)
            .order_by(PreFacturas.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_(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):
        tax_locales = Configuracion.get_bool('chk_config_tax_locales')
        tax_decimals = Configuracion.get_bool('chk_config_tax_decimals')
        tax_locales_truncate = Configuracion.get_bool('chk_config_tax_locales_truncate')
        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None
        total_retenciones = None
        locales_traslados = 0
        locales_retenciones = 0

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

            product['unidad'] = SATUnidades.get(SATUnidades.id==product['unidad']).key
            product['clave'] = p.clave
            product['clave_sat'] = p.clave_sat
            product['cuenta_predial'] = p.cuenta_predial

            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'] = round(descuento * cantidad, DECIMALES)
            product['precio_final'] = precio_final
            product['importe'] = round(cantidad * valor_unitario, DECIMALES)

            descuento_cfdi += product['descuento']
            subtotal += product['importe']

            PreFacturasDetalle.create(**product)

            if invoice.tipo_comprobante == 'T':
                continue

            base = product['importe'] - product['descuento']
            for tax in p.impuestos:
                if tax_locales and tax.tipo == 'R' and tax.key == '000':
                    base = product['importe']

                if tax_decimals:
                    impuesto_producto = round(float(tax.tasa) * base, DECIMALES_TAX)
                else:
                    impuesto_producto = round(float(tax.tasa) * base, DECIMALES)

                if tax.key == '000' and tax_locales_truncate:
                    impuesto_producto = util.truncate(float(tax.tasa) * base)

                if tax.tipo == 'T' and tax.key != '000':
                    total_trasladados = (total_trasladados or 0) + impuesto_producto
                elif tax.tipo == 'R' and tax.key != '000':
                    total_retenciones = (total_retenciones or 0) + impuesto_producto
                elif tax.tipo == 'T' and tax.key == '000':
                    locales_traslados += impuesto_producto
                elif tax.tipo == 'R' and tax.key == '000':
                    locales_retenciones += impuesto_producto

                if tax.id in totals_tax:
                    totals_tax[tax.id].base += base
                    totals_tax[tax.id].suma_impuestos += impuesto_producto
                else:
                    tax.base = base
                    tax.suma_impuestos = impuesto_producto
                    totals_tax[tax.id] = tax

        if invoice.tipo_comprobante == 'T':
            subtotal = 0.0
            descuento = 0.0
        else:
            for tax in totals_tax.values():
                if tax.tipo == 'E':
                    continue

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

        total = subtotal - descuento_cfdi + \
            (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_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: utils.loads(r.valores) for r in query}

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


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='')
    empaques = DecimalField(default=0.0, max_digits=14, decimal_places=4,
        auto_round=True)
    warehouse = ForeignKeyField(Almacenes, null=True)

    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 get_by_invoice(cls, id):
        return FacturasDetalle.select().where(FacturasDetalle.factura==id)

    @classmethod
    def reinvoice(cls, id):
        data = []
        products = FacturasDetalle.select().where(FacturasDetalle.factura==id)
        for p in products:
            row = {'delete': '-', 'id_product': p.producto.id}
            row['clave'] = p.clave
            row['clave_sat'] = p.clave_sat
            row['descripcion'] = p.descripcion
            row['pedimento'] = p.pedimento
            row['unidad'] = p.producto.unidad.id
            row['cantidad'] = p.cantidad
            row['valor_unitario'] = p.valor_unitario
            descuento = p.valor_unitario - p.precio_final
            row['descuento'] = descuento
            pf = p.valor_unitario - descuento
            row['importe'] = round(pf * p.cantidad, DECIMALES)
            impuestos = cls._get_impuestos(cls, row['id_product'])
            data.append({'row': row, 'taxes': impuestos})
        return data

    @classmethod
    def get_detalle(cls, id):
        data = []
        products = FacturasDetalle.select().where(FacturasDetalle.factura==id)
        for p in products:
            row = {'id_product': p.producto.id}
            row['clave'] = p.clave
            row['clave_sat'] = p.clave_sat
            row['descripcion'] = p.descripcion
            row['unidad'] = p.producto.unidad.name
            row['cantidad'] = p.cantidad
            row['valor_unitario'] = p.valor_unitario
            descuento = p.valor_unitario - p.precio_final
            row['descuento'] = descuento
            pf = p.valor_unitario - descuento
            row['importe'] = round(pf * p.cantidad, DECIMALES)
            data.append(row)
        return data


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='')
    unidad = 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().where(PreFacturas.id==id)[0]
        receptor = {
            'id': q.cliente.id,
            'nombre': q.cliente.nombre,
            'rfc': q.cliente.rfc,
            'forma_pago': q.forma_pago,
            'uso_cfdi': q.uso_cfdi,
            'codigo_postal': q.cliente.codigo_postal,
            'regimenes': SociosRegimenes.get_by_key(q.cliente.id),
            'notas': q.notas,
        }

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

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

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

    @classmethod
    def count(cls, id):
        c = PreFacturasDetalle.select().where(
            PreFacturasDetalle.factura==id).count()
        return c

    @classmethod
    def can_open(cls, id):
        c = cls.count(id)
        PreFacturas.get_pdf_in_thread(id)
        return c < 300

    @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
            if p.cuenta_predial:
                info = '\nCuenta Predial Número: {}'.format(p.cuenta_predial)
                producto['descripcion'] += info
            if p.pedimento:
                info = '\nNúmero Pedimento: {}'.format(p.pedimento)
                producto['descripcion'] += info

            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)
    tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6,
        auto_round=True)

    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, values in ids.items():
            fac = Facturas.get(Facturas.id==int(i))
            this_pay = values['this_pay']
            importe = values['importe']

            type_change = 1.00
            if mov.moneda != fac.moneda:
                type_change = round(1 / values['type_change'], 6)
                validate = round(this_pay / type_change, 2)
                while validate > importe:
                    type_change += 0.000001
                    validate = round(this_pay / type_change, 2)

            mov_ant, numero = cls._movimiento_anterior(cls, mov, fac)
            nuevo = {
                'movimiento': mov,
                'factura': fac,
                'numero': numero,
                'importe': importe,
                'tipo_cambio': type_change,
            }
            if mov_ant is None:
                nuevo['saldo_anterior'] = float(fac.saldo)
            else:
                nuevo['saldo_anterior'] = float(mov_ant.saldo)

            if(fac.moneda in ('MXN', 'peso')):
                nuevo['saldo'] = nuevo['saldo_anterior'] - importe
            else:
                nuevo['importe'] = this_pay
                nuevo['saldo'] = nuevo['saldo_anterior'] - this_pay

            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

    def _get_related(self, values):
        id = int(values['id'])
        filters = (FacturasPagos.movimiento==id)

        rows = tuple(FacturasPagos
            .select(
                Facturas.id,
                Facturas.serie,
                Facturas.folio,
                Facturas.uuid,
                Facturas.fecha,
                Facturas.tipo_comprobante,
                Facturas.estatus,
                Socios.nombre.alias('cliente'),
                Facturas.total,
                FacturasPagos.saldo,
                FacturasPagos.importe,
            )
            .join(Facturas).switch(FacturasPagos)
            .join(Socios, on=(Facturas.cliente==Socios.id))
            .where(filters)
            .dicts()
        )

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

    @classmethod
    def get_values(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_get_{}'.format(opt))(cls, values)


class CfdiPagos(BaseModel):
    movimiento = ForeignKeyField(MovimientosBanco)
    socio = ForeignKeyField(Socios)
    serie = TextField(default='')
    folio = IntegerField(default=0)
    fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
    fecha_timbrado = DateTimeField(null=True)
    tipo_comprobante = TextField(default=DEFAULT_CFDIPAY['TYPE'])
    lugar_expedicion = TextField(default='')
    regimen_fiscal = TextField(default='')
    tipo_relacion = TextField(default='')
    uuid_relacionado = UUIDField(null=True)
    xml = TextField(default='')
    uuid = UUIDField(null=True)
    estatus = TextField(default='Guardada')
    estatus_sat = TextField(default='')
    notas = TextField(default='')
    error = TextField(default='')
    cancelada = BooleanField(default=False)
    fecha_cancelacion = DateTimeField(null=True)
    receptor_regimen = TextField(default='')

    class Meta:
        order_by = ('movimiento',)

    @classmethod
    def with_invoice(cls, id):
        if CfdiPagos.select().where(CfdiPagos.movimiento==id).count():
            return 'Si'
        return ''

    @classmethod
    def post(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_{}'.format(opt))(cls, values)

    def _delete(self, values):
        id_mov = int(values['id_mov'])

        filters = (
            (CfdiPagos.movimiento==id_mov)
        )
        last = CfdiPagos.select().where(filters)
        if not last:
            msg = 'El depósito no tiene facturas de pago'
            data = {'ok': False, 'msg': msg}
            return data

        if len(last) > 1:
            msg = 'Hay más de una factura activa para este depósito'
            data = {'ok': False, 'msg': msg}
            return data

        last = last[0]
        if last.uuid:
            msg = 'La factura esta timbrada, no se puede eliminar'
            data = {'ok': False, 'msg': msg}
            return data

        q = CfdiPagos.delete().where(CfdiPagos.id==last.id)
        q.execute()

        msg = 'Factura de pago eliminada correctamente'
        data = {'ok': True, 'msg': msg}
        return data

    def _cancel(self, values):
        id_mov = int(values['id_mov'])
        args = utils.loads(values['args'])

        filters = (
            (CfdiPagos.movimiento==id_mov) &
            (CfdiPagos.cancelada==False)
        )
        last = CfdiPagos.select().where(filters)
        if not last:
            msg = 'El depósito no tiene facturas de pago activas'
            data = {'ok': False, 'msg': msg}
            return data

        if len(last) > 1:
            msg = 'Hay más de una factura activa para este depósito'
            data = {'ok': False, 'msg': msg}
            return data

        last = last[0]
        if not last.uuid:
            msg = 'La factura no esta timbrada'
            data = {'ok': False, 'msg': msg}
            return data

        pac = utils.get_pac_by_rfc(last.xml)
        auth = Configuracion.get_({'fields': 'auth_by_pac', 'pac': pac})

        certificado = Certificado.get(Certificado.es_fiel==False)
        result = utils.cancel_xml_sign(last, args, auth, certificado)

        if result['ok']:
            last.estatus = 'Cancelada'
            last.error = ''
            last.cancelada = True
            last.fecha_cancelacion = result['date']
        else:
            last.error = result['msg']
        last.save()

        return {'ok': result['ok'], 'msg': result['msg'], 'id': last.id}

    def _cancel2(self, values):
        args = utils.loads(values['args'])
        id = args.pop('id')
        obj = CfdiPagos.get(CfdiPagos.id==id)

        if not obj.uuid:
            msg = 'La Factura de Pago no esta timbrada'
            data = {'ok': False, 'msg': msg}
            return data

        pac = utils.get_pac_by_rfc(obj.xml)
        auth = Configuracion.get_({'fields': 'auth_by_pac', 'pac': pac})
        certificado = Certificado.get(Certificado.es_fiel==False)

        result = utils.cancel_xml_sign(obj, args, auth, certificado)

        if result['ok']:
            obj.estatus = 'Cancelada'
            obj.error = ''
            obj.cancelada = True
            obj.fecha_cancelacion = result['date']
        else:
            obj.error = result['msg']
        obj.save()

        return {'ok': result['ok'], 'msg': result['msg']}

    def _get_folio(self, serie):
        folio = int(Configuracion.get_('txt_config_cfdipay_folio') or '0')
        start = (CfdiPagos
            .select(fn.Max(CfdiPagos.folio).alias('mf'))
            .where(CfdiPagos.serie==serie)
            .order_by(SQL('mf'))
            .scalar())

        if start is None:
            next_folio = 1
        else:
            next_folio = start + 1

        if folio > next_folio:
            next_folio = folio

        return next_folio

    def _new(self, values):
        id_mov = int(values['id_mov'])

        filters = (FacturasPagos.movimiento==id_mov)
        related = FacturasPagos.select().where(filters)
        if not related:
            msg = 'El pago no tiene facturas relacionadas'
            data = {'ok': False, 'msg': msg}
            return data

        partner = tuple(set([f.factura.cliente.rfc for f in related]))
        if len(partner) > 1:
            msg = 'Facturas relacionadas a diferentes clientes'
            data = {'ok': False, 'msg': msg}
            return data

        partner = related[0].factura.cliente
        partner_name = related[0].factura.cliente.nombre
        receptor_regimen = related[0].factura.receptor_regimen

        if not receptor_regimen:
            receptor_regimen = SociosRegimenes.get_by_socio_first(partner)

        if not receptor_regimen:
            msg = 'El receptor no tiene establecido su Regimen Fiscal'
            data = {'ok': False, 'msg': msg}
            return data

        emisor = Emisor.select()[0]
        regimen_fiscal = emisor.regimenes[0].key

        filters = (
            (CfdiPagos.movimiento==id_mov) &
            (CfdiPagos.cancelada==False)
        )
        previous = CfdiPagos.select().where(filters)
        if previous:
            previous = previous[0]
            if previous.uuid:
                msg = 'Hay una factura activa, es necesario cancelarla primero'
                data = {'ok': False, 'msg': msg}
                return data
            else:
                data = {'ok': True, 'new': False}
                return data

        fields = {}
        filters = (
            (CfdiPagos.movimiento==id_mov) &
            (CfdiPagos.cancelada==True)
        )
        previous = CfdiPagos.select().where(filters).order_by(CfdiPagos.id.desc())
        if previous:
            previous = previous[0]
            fields['tipo_relacion'] = DEFAULT_CFDIPAY['TYPE_RELATION']
            fields['uuid_relacionado'] = previous.uuid

        emisor = Emisor.select()[0]
        serie = Configuracion.get_('txt_config_cfdipay_serie') or DEFAULT_CFDIPAY['SERIE']
        fields['movimiento'] = id_mov
        fields['socio'] = partner
        fields['serie'] = serie
        fields['folio'] = self._get_folio(self, serie)
        fields['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
        fields['regimen_fiscal'] = regimen_fiscal
        fields['receptor_regimen'] = receptor_regimen

        with database_proxy.atomic() as txn:
            obj = CfdiPagos.create(**fields)

        row = {
            'id': obj.id,
            'serie': obj.serie,
            'folio': obj.folio,
            'uuid': obj.uuid,
            'fecha': obj.fecha,
            'tipo_comprobante': obj.tipo_comprobante,
            'estatus': obj.estatus,
            'cliente': partner_name,
        }
        data = {'ok': True, 'row': row, 'new': True}
        return data

    def _get_taxes_by_pay(self, pay, taxes_pay):
        invoice = Facturas.get(Facturas.uuid==pay['IdDocumento'])
        impuestos = {}
        traslados = []
        retenciones = []

        where = (FacturasImpuestos.factura==invoice)
        taxes = FacturasImpuestos.select().where(where)

        tax_proporcion = pay['ImpPagado'] / invoice.total
        # ~ print('Total', invoice.total)
        # ~ print('Pagado', pay['ImpPagado'])
        # ~ print('proporcion', tax_proporcion)

        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'

            import_dr = round(tax.importe * tax_proporcion, 2)
            base_dr =  round(tax.base * tax_proporcion, 2)

            new_import_tax = round(base_dr * tax.impuesto.tasa, 2)
            while new_import_tax > import_dr:
                base_dr -= Decimal(0.01)
                new_import_tax = round(base_dr * tax.impuesto.tasa, 2)
            import_dr = new_import_tax

            xml_tax_base = FORMAT.format(base_dr)
            xml_importe = FORMAT.format(import_dr)

            values = {
                "BaseDR": xml_tax_base,
                "ImpuestoDR": tax.impuesto.key,
                "TipoFactorDR": tipo_factor,
                "TasaOCuotaDR": str(tax.impuesto.tasa),
                "ImporteDR": xml_importe,
            }
            # ~ tax_key = tax.impuesto.key
            tax_key = f"{values['ImpuestoDR']}|{values['TasaOCuotaDR']}"
            if tax.impuesto.tipo == 'T':
                traslados.append(values)
                if tax_key in taxes_pay['traslados']:
                    taxes_pay['traslados'][tax_key]['BaseP'] += base_dr
                    taxes_pay['traslados'][tax_key]['ImporteP'] += import_dr
                else:
                    values = {
                        "BaseP": base_dr,
                        "ImpuestoP": tax.impuesto.key,
                        "TipoFactorP": tipo_factor,
                        "TasaOCuotaP": str(tax.impuesto.tasa),
                        "ImporteP": import_dr,
                    }
                    taxes_pay['traslados'][tax_key] = values
            elif tax.impuesto.tipo == 'R':
                tax_key = tax.impuesto.key
                retenciones.append(values)
                if tax_key in taxes_pay['retenciones']:
                    taxes_pay['retenciones'][tax_key] += import_dr
                else:
                    taxes_pay['retenciones'][tax_key] = import_dr

        impuestos['traslados'] = traslados
        impuestos['retenciones'] = retenciones

        return impuestos

    def _get_related_xml(self, id_mov, currency):
        #TAX ToDo
        TAX_IVA_16 = '002|0.160000'
        TAX_IVA_0 = '002|0.000000'

        filters = (FacturasPagos.movimiento==id_mov)
        related = tuple(FacturasPagos.select(
                Facturas.uuid.alias('IdDocumento'),
                Facturas.serie.alias('Serie'),
                Facturas.folio.alias('Folio'),
                Facturas.moneda.alias('MonedaDR'),
                FacturasPagos.tipo_cambio.alias('EquivalenciaDR'),
                FacturasPagos.numero.alias('NumParcialidad'),
                FacturasPagos.saldo_anterior.alias('ImpSaldoAnt'),
                FacturasPagos.importe.alias('ImpPagado'),
                FacturasPagos.saldo.alias('ImpSaldoInsoluto'),
            ).join(Facturas).switch(FacturasPagos)
            .where(filters)
            .dicts())

        taxes_pay = {'retenciones': {}, 'traslados': {}, 'totales': {}}

        for r in related:
            r['taxes'] = self._get_taxes_by_pay(self, r, taxes_pay)
            r['IdDocumento'] = str(r['IdDocumento'])
            r['Folio'] = str(r['Folio'])
            r['NumParcialidad'] = str(r['NumParcialidad'])

            equivalencia = r['EquivalenciaDR']
            if currency == r['MonedaDR']:
                r['EquivalenciaDR'] = '1'
            else:
                r['EquivalenciaDR'] = FORMAT6.format(equivalencia)

            r['ObjetoImpDR'] = '01'
            if r['taxes']['traslados'] or r['taxes']['retenciones']:
                r['ObjetoImpDR'] = '02'

            r['ImpSaldoAnt'] = FORMAT.format(r['ImpSaldoAnt'])
            r['ImpPagado'] = FORMAT.format(r['ImpPagado'])
            if round(r['ImpSaldoInsoluto'], 2) == 0.0:
                r['ImpSaldoInsoluto'] = '0.00'
            else:
                r['ImpSaldoInsoluto'] = FORMAT.format(r['ImpSaldoInsoluto'])

            if not r['Serie']:
                del r['Serie']

        total_tax_iva_16_base = 0
        total_tax_iva_0_base = 0
        total_tax_iva_16_importe = 0
        total_tax_retenciones_isr_importe = 0
        total_tax_retenciones_iva_importe = 0

        for key, importe in taxes_pay['retenciones'].items():
            taxes_pay['retenciones'][key] = FORMAT.format(importe)
            if key == '002':
                total_tax_retenciones_iva_importe += importe
            else:
                total_tax_retenciones_isr_importe += importe

        for k, tax in taxes_pay['traslados'].items():
            tax_type = taxes_pay['traslados'][k]['ImpuestoP']
            tax_tasa = taxes_pay['traslados'][k]['TasaOCuotaP']
            tax_base = taxes_pay['traslados'][k]['BaseP']
            importe = taxes_pay['traslados'][k]['ImporteP']

            tax_base /=  equivalencia
            importe /=  equivalencia

            current_tax = f'{tax_type}|{tax_tasa}'
            if current_tax == TAX_IVA_16:
                # ~ new_import_tax = round(Decimal(tax_tasa) * tax_base, 2)
                # ~ while new_import_tax > importe:
                    # ~ tax_base -= Decimal(0.01)
                    # ~ new_import_tax = round(Decimal(tax_tasa) * tax_base, 2)
                total_tax_iva_16_base += tax_base
                total_tax_iva_16_importe += importe
            elif current_tax == TAX_IVA_0:
                total_tax_iva_0_base += tax_base

            taxes_pay['traslados'][k]['BaseP'] = FORMAT.format(tax_base)
            taxes_pay['traslados'][k]['ImporteP'] = FORMAT.format(importe)

        taxes_pay['totales'] = {}

        if taxes_pay['traslados']:
            if total_tax_iva_16_base:
                taxes_pay['totales']['TotalTrasladosBaseIVA16'] = FORMAT.format(total_tax_iva_16_base)
                taxes_pay['totales']['TotalTrasladosImpuestoIVA16'] = FORMAT.format(total_tax_iva_16_importe)
            if total_tax_iva_0_base:
                taxes_pay['totales']['TotalTrasladosBaseIVA0'] = FORMAT.format(total_tax_iva_0_base)
                taxes_pay['totales']['TotalTrasladosImpuestoIVA0'] = FORMAT.format(0.0)

        if taxes_pay['retenciones']:
            if total_tax_retenciones_isr_importe:
                taxes_pay['totales']['TotalRetencionesISR'] = FORMAT.format(total_tax_retenciones_isr_importe)
            if total_tax_retenciones_iva_importe:
                taxes_pay['totales']['TotalRetencionesIVA'] = FORMAT.format(total_tax_retenciones_iva_importe)

        return related, taxes_pay

    def _generate_xml(self, invoice):
        emisor = Emisor.select()[0]
        certificado = Certificado.get(Certificado.es_fiel==False)
        used_data_bank = Configuracion.get_bool('chk_cfg_pays_data_bank')

        cfdi = {}
        related = {}
        cfdi['Serie'] = invoice.serie
        cfdi['Folio'] = str(invoice.folio)
        cfdi['Fecha'] = invoice.fecha.isoformat()[:19]
        cfdi['NoCertificado'] = certificado.serie
        cfdi['SubTotal'] = '0'
        cfdi['Moneda'] = DEFAULT_CFDIPAY['CURRENCY']
        # ~ cfdi['TipoCambio'] = DEFAULT_CFDIPAY['TC']
        cfdi['Total'] = '0'
        cfdi['TipoDeComprobante'] = invoice.tipo_comprobante
        cfdi['LugarExpedicion'] = invoice.lugar_expedicion

        if invoice.tipo_relacion:
            related = {
                'tipo': invoice.tipo_relacion,
                'cfdis': (str(invoice.uuid_relacionado),),
            }

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

        receptor = {
            'Rfc': invoice.socio.rfc,
            'Nombre': invoice.socio.nombre,
            'UsoCFDI': DEFAULT_CFDIPAY['USED'],
            'DomicilioFiscalReceptor': invoice.socio.codigo_postal,
            'RegimenFiscalReceptor': invoice.receptor_regimen
        }
        if invoice.socio.tipo_persona == 4:
            if invoice.socio.pais:
                receptor['ResidenciaFiscal'] = invoice.socio.pais
            if invoice.socio.id_fiscal:
                receptor['NumRegIdTrib'] = invoice.socio.id_fiscal

        conceptos = ({
            'ClaveProdServ': DEFAULT_CFDIPAY['KEYSAT'],
            'Cantidad': '1',
            'ClaveUnidad': DEFAULT_CFDIPAY['UNITKEY'],
            'Descripcion': DEFAULT_CFDIPAY['DESCRIPTION'],
            'ValorUnitario': '0',
            'Importe': '0',
            'ObjetoImp': '01',
        },)

        impuestos = {}

        mov = invoice.movimiento
        currency = mov.moneda
        related_docs, taxes_pay = self._get_related_xml(self, invoice.movimiento, currency)
        totales = taxes_pay.pop('totales')
        pagos = {
            'FechaPago': mov.fecha.isoformat()[:19],
            'FormaDePagoP': mov.forma_pago.key,
            'MonedaP': currency,
            'TipoCambioP': '1',
            'Monto': FORMAT.format(mov.deposito),
            'relacionados': related_docs,
            'taxes_pay': taxes_pay,
        }
        if mov.numero_operacion:
            pagos['NumOperacion'] = mov.numero_operacion

        if used_data_bank and mov.cuenta_socio:
            if mov.cuenta_socio.banco.rfc:
                pagos['RfcEmisorCtaOrd'] = mov.cuenta_socio.banco.rfc
                if mov.cuenta_socio.banco.rfc == RFC_EXTRANJERO:
                    pagos['NomBancoOrdExt'] = mov.cuenta_socio.banco.name
                pagos['CtaOrdenante'] = mov.cuenta_socio.cuenta
            if mov.cuenta.banco.rfc:
                pagos['RfcEmisorCtaBen'] = mov.cuenta.banco.rfc
                pagos['CtaBeneficiario'] = mov.cuenta.cuenta

        if currency != CURRENCY_MN:
            pagos['TipoCambioP'] = FORMAT_TAX.format(mov.tipo_cambio)

        totales['MontoTotalPagos'] = pagos['Monto']
        pagos['totales'] = totales

        complementos = {'pagos': pagos}
        data = {
            'comprobante': cfdi,
            'relacionados': related,
            'emisor': emisor,
            'receptor': receptor,
            'conceptos': conceptos,
            'impuestos': impuestos,
            'donativo': {},
            'edu': False,
            'complementos': complementos,
            'global': {},
        }
        return utils.make_xml(data, certificado)

    def _stamp(self, values):
        id_mov = int(values['id_mov'])
        send_email = Configuracion.get_bool('correo_directo')
        auth = Configuracion.get_({'fields': 'pac_auth'})

        filters = (
            (CfdiPagos.movimiento==id_mov) &
            (CfdiPagos.uuid.is_null(True))
        )
        obj = CfdiPagos.get(filters)
        obj.xml = self._generate_xml(self, obj)
        obj.estatus = 'Generada'
        obj.save()
        msg = 'Factura timbrada correctamente'
        result = utils.xml_stamp(obj.xml, auth)
        if result['ok']:
            obj.xml = result['xml']
            obj.uuid = result['uuid']
            obj.fecha_timbrado = result['date']
            obj.estatus = 'Timbrada'
            obj.error = ''
            row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
        else:
            msg = result['error']
            obj.estatus = 'Error'
            obj.error = msg
            row = {'estatus': 'Error'}

        obj.save()

        result = {
            'ok': result['ok'],
            'msg': msg,
            'id': obj.id,
            'row': row,
        }
        if result['ok']:
            self._sync(self, obj.id, send_email)

        return result

    def _get_related(self, values):
        id_mov = int(values['id_mov'])
        filters = (
            (CfdiPagos.movimiento==id_mov)
        )
        rows = tuple(CfdiPagos.select(
            CfdiPagos.id,
            CfdiPagos.serie,
            CfdiPagos.folio,
            CfdiPagos.uuid,
            CfdiPagos.fecha,
            CfdiPagos.tipo_comprobante,
            CfdiPagos.estatus,
            Socios.nombre.alias('cliente'),
            ).join(Socios).switch(CfdiPagos)
            .where(filters).dicts())
        return {'ok': True, 'rows': rows}

    @classmethod
    def get_file_xml(cls, id):
        obj = CfdiPagos.get(CfdiPagos.id==id)
        folio = str(obj.folio).zfill(6)
        name = '{}{}_{}.xml'.format(obj.serie, folio, obj.socio.rfc)

        emisor = Emisor.select()[0]
        target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
        files = (
            (obj.xml, name, target),
        )
        cls._sync_files(cls, files)
        return obj.xml, name

    def _get_not_in_xml(self, invoice, emisor):
        values = {'el.version': VERSION_EMPRESA_LIBRE}

        values['notas'] = invoice.notas
        values['fechadof'] = None

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

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

        values['moneda'] = 'XXX'

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

        receptor = Socios.select().where(Socios.id==invoice.socio.id).dicts()[0]
        values['receptor'] = {}
        for k, v in receptor.items():
            values['receptor'][k] = v

        values['pagos'] = True
        return values

    @classmethod
    def get_file_pdf(cls, id):
        try:
            emisor = Emisor.select()[0]
        except IndexError:
            return b'', 'sin_datos_de_emisor.pdf'

        obj = CfdiPagos.get(CfdiPagos.id==id)
        folio = str(obj.folio).zfill(6)
        name = '{}{}_{}.pdf'.format(obj.serie, folio, obj.socio.rfc)

        if obj.uuid is None:
            return b'', name

        target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
        values = cls._get_not_in_xml(cls, obj, emisor)
        data = util.get_data_from_xml(obj, values)
        data.update(utils.CfdiToDict(obj.xml).values)
        data['informacion_global'] = {}

        obj = SATFormaPago.get(SATFormaPago.key==data['pays']['FormaDePagoP'])
        data['pays']['formadepago'] = '{} ({})'.format(obj.name, obj.key)
        doc = util.to_pdf(data, emisor.rfc)

        files = (
            (doc, name, target),
        )
        cls._sync_files(cls, files)

        return doc, name

    @util.run_in_thread
    def _sync(self, id, send_email=False):
        files = (CfdiPagos.get_file_xml(id), CfdiPagos.get_file_pdf(id))
        if send_email:
            self._send(self, {'id': id}, files)
        return

    @util.run_in_thread
    def _sync_files(self, files):
        util.sync_files(files)
        return

    @classmethod
    def get_values(cls, values):
        opt = values.pop('opt')
        return getattr(cls, '_get_{}'.format(opt))(cls, values)

    def _get_status_sat(self, values):
        id = int(values['id'])
        obj = CfdiPagos.get(CfdiPagos.id == id)
        estatus_sat = utils.get_status_sat(obj.xml)
        if obj.estatus_sat != estatus_sat:
            obj.estatus_sat = estatus_sat
            obj.save()

        # ~ if obj.estatus_sat == 'Vigente' and obj.estatus == 'Cancelada':
            # ~ days = utils.get_days(obj.fecha_cancelacion)
            # ~ if days > 3:
                # ~ estatus_sat = 'uncancel'

        return estatus_sat

    def _get_table(self, values):
        # ~ print('\n\n', values, '\n')
        if 'start' in values:
            filters = CfdiPagos.fecha.between(
                util.get_date(values['start']),
                util.get_date(values['end'], True))
        else:
            if values['year'] == '-1':
                fy = (CfdiPagos.fecha.year > 0)
            else:
                fy = (CfdiPagos.fecha.year == int(values['year']))
            if values['month'] == '-1':
                fm = (CfdiPagos.fecha.month > 0)
            else:
                fm = (CfdiPagos.fecha.month == int(values['month']))
            filters = (fy & fm)

        rows = tuple(CfdiPagos.select(
            CfdiPagos.id,
            CfdiPagos.serie,
            CfdiPagos.folio,
            CfdiPagos.uuid,
            CfdiPagos.fecha,
            CfdiPagos.tipo_comprobante,
            CfdiPagos.estatus,
            case(MovimientosBanco.cancelado, (
                (True, 'Cancelado'),
                (False, 'Activo'),
            )).alias('movimiento'),
            MovimientosBanco.deposito.alias('total'),
            MovimientosBanco.moneda.alias('currency'),
            (MovimientosBanco.deposito * MovimientosBanco.tipo_cambio).alias('total_mn'),
            Socios.nombre.alias('cliente'))
            .join(MovimientosBanco).switch(CfdiPagos)
            .join(Socios).switch(CfdiPagos)
            .where(filters).dicts())
        return {'ok': True, 'rows': rows}

    def _send(self, values, files=()):
        id = int(values['id'])

        values = Configuracion.get_({'fields': 'correo'})
        contra = Configuracion.get_('correo_contra')
        rfc = Emisor.select()[0].rfc

        if not values:
            msg = 'No esta configurado el servidor de correo de salida'
            return {'ok': False, 'msg': msg}

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

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

        if not files:
            files = (self.get_file_pdf(id), self.get_file_xml(id))

        fields = util.make_fields(obj.xml)
        starttls = bool(int(values.get('correo_starttls', '0')))
        server = {
            'servidor': values['correo_servidor'],
            'puerto': values['correo_puerto'],
            'ssl': bool(int(values['correo_ssl'])),
            'starttls': starttls,
            'usuario': values['correo_usuario'],
            'contra': utils.decrypt(contra, rfc),
        }
        options = {
            'para': to,
            'copia': values.get('correo_copia', ''),
            'confirmar': util.get_bool(values.get('correo_confirmacion', '0')),
            '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}


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


class CamposPersonalizados(BaseModel):
    nombre = TextField()
    slug = TextField(unique=True)

    class Meta:
        order_by = ('nombre',)


class FacturasPersonalizados(BaseModel):
    factura = ForeignKeyField(Facturas)
    campo = TextField()
    valor = TextField()

    class Meta:
        order_by = ('factura',)


class Tickets(BaseModel):
    cliente = ForeignKeyField(Socios, null=True)
    serie = TextField(default='')
    folio = IntegerField(default=0)
    fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
    forma_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)
    total = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_trasladados = DecimalField(
        max_digits=20, decimal_places=6, auto_round=True, null=True)
    estatus = TextField(default='Generado')
    notas = TextField(default='')
    factura = ForeignKeyField(Facturas, null=True)
    cancelado = BooleanField(default=False)
    vendedor = TextField(default='')
    comision = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    cambio = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)

    class Meta:
        order_by = ('fecha',)
        indexes = (
            (('serie', 'folio'), True),
        )

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

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

        return inicio

    def _without_tax(self, importe, obj):
        # ~ todo
        # ~ Por ahora se asume que solo tiene IVA trasladado 0.16

        for tax in obj.impuestos:
            tasa = 1.0 + float(tax.tasa)
            base = round(importe / tasa, DECIMALES)
        return base

    def _calcular_totales(self, ticket, productos, user=None):
        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None

        warehouse = None
        try:
            warehouse = user.sucursal.warehouse
        except:
            pass

        for producto in productos:
            id_producto = producto.pop('id_product')
            p = Productos.get(Productos.id==id_producto)
            producto['descripcion'] = p.descripcion
            producto['ticket'] = ticket.id
            producto['producto'] = id_producto

            cantidad = float(producto['cantidad'])
            valor_unitario = self._without_tax(self, producto['valor_unitario'], p)
            descuento = float(producto['descuento'] or 0)
            precio_final = valor_unitario - descuento
            importe = round(cantidad * precio_final, DECIMALES)

            producto['cantidad'] = cantidad
            producto['valor_unitario'] = valor_unitario
            producto['descuento'] = descuento
            producto['precio_final'] = precio_final
            producto['importe'] = importe

            producto['warehouse'] = warehouse

            descuento_cfdi += descuento
            subtotal += importe

            TicketsDetalle.create(**producto)

            # ~ if p.inventario:
                # ~ p.existencia -= Decimal(cantidad)
                # ~ p.save()

            base = producto['importe']
            for tax in p.impuestos:
                impuesto_producto = round(float(tax.tasa) * base, DECIMALES)
                if tax.tipo == 'T' and tax.key != '000':
                    total_trasladados = (total_trasladados or 0) + impuesto_producto

                if tax.id in totals_tax:
                    totals_tax[tax.id].base += base
                    totals_tax[tax.id].importe += impuesto_producto
                else:
                    tax.base = base
                    tax.importe = impuesto_producto
                    totals_tax[tax.id] = tax


        for tax in totals_tax.values():
            ticket_tax = {
                'ticket': ticket.id,
                'impuesto': tax.id,
                'base': tax.base,
                'importe': tax.importe,
            }
            TicketsImpuestos.create(**ticket_tax)

        total = subtotal + (total_trasladados or 0)
        data = {
            'subtotal': subtotal + descuento,
            'descuento': descuento_cfdi,
            'total': total,
            'total_trasladados': total_trasladados,
        }
        return data

    def _get_serie(self, user, invoice=False):
        default_serie = DEFAULT_SERIE_TICKET
        if invoice:
            default_serie = Folios.get_default()

        if user.sucursal is None:
            return default_serie

        if invoice:
            return user.sucursal.serie_facturas or default_serie
        else:
            return user.sucursal.serie_tickets or default_serie

    @classmethod
    def get_notes(cls, tickets):
        rows = Tickets.select(Tickets.notas).where(Tickets.id.in_(tickets))
        return '\n'.join([r.notas for r in rows])

    @utils.run_in_thread
    def _update_inventory(cls, id_ticket, cancel=False, user=None):
        warehouse = None
        try:
            warehouse = user.sucursal.warehouse
        except:
            pass

        products = (TicketsDetalle
            .select()
            .where(TicketsDetalle.ticket==id_ticket)
        )
        for p in products:
            if p.producto.inventario:
                cant = Decimal(p.cantidad)
                if cancel:
                    cant *= -1
                    warehouse = p.warehouse
                p.producto.existencia -= cant
                p.producto.save()

                if warehouse is None:
                    continue

                where = (
                    WareHouseProduct.warehouse==warehouse,
                    WareHouseProduct.product==p.producto,
                )

                obj = WareHouseProduct.get(*where)
                obj.exists -= cant
                obj.save()
        return

    @classmethod
    def add(cls, values, user):
        productos = util.loads(values.pop('productos'))
        values['serie'] = cls._get_serie(cls, user)
        values['folio'] = cls._get_folio(cls, values['serie'])

        with database_proxy.atomic() as txn:
            obj = Tickets.create(**values)
            totals = cls._calcular_totales(cls, obj, productos, user)
            obj.subtotal = totals['subtotal']
            obj.descuento = totals['descuento']
            obj.total_trasladados = totals['total_trasladados']
            obj.total = totals['total']
            obj.save()

        cls._update_inventory(cls, obj.id, user=user)

        row = {
            'id': obj.id,
            'serie': obj.serie,
            'folio': obj.folio,
            'fecha': obj.fecha,
            'estatus': obj.estatus,
            'total': obj.total,
        }
        data = {'ok': True, 'row': row}
        return data

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

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

        return inicio

    @classmethod
    def uncancel(cls, invoice):
        query = (Tickets
            .update(estatus='Generado', cancelado=False, factura=None)
            .where(Tickets.factura==invoice)
        )
        result = query.execute()
        return result

    def _cancel_tickets(self, invoice, tickets):
        query = (Tickets
            .update(estatus='Facturado', cancelado=True, factura=invoice)
            .where(Tickets.id.in_(tickets))
        )
        result = query.execute()
        return result

    def _calculate_totals_invoice(self, invoice, tickets):
        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None
        total_retenciones = None
        notes = Tickets.get_notes(tickets)

        details = TicketsDetalle.select().where(TicketsDetalle.ticket.in_(tickets))

        for detail in details:
            product = {}
            p = detail.producto
            product['unidad'] = p.unidad.key
            product['clave'] = p.clave
            product['clave_sat'] = p.clave_sat

            product['factura'] = invoice.id
            product['producto'] = p.id
            product['descripcion'] = detail.descripcion

            cantidad = float(detail.cantidad)
            valor_unitario = float(detail.valor_unitario)
            descuento = float(detail.descuento)
            precio_final = valor_unitario - descuento
            importe = round(cantidad * precio_final, DECIMALES)

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

            descuento_cfdi += product['descuento']
            subtotal += product['importe']

            FacturasDetalle.create(**product)

            base = product['importe'] - product['descuento']
            for tax in p.impuestos:
                impuesto_producto = round(float(tax.tasa) * base, DECIMALES)
                if tax.tipo == 'T' and tax.key != '000':
                    total_trasladados = (total_trasladados or 0) + impuesto_producto
                elif tax.tipo == 'R' and tax.key != '000':
                    total_retenciones = (total_retenciones or 0) + impuesto_producto
                elif tax.tipo == 'T' and tax.key == '000':
                    locales_traslados += impuesto_producto
                elif tax.tipo == 'R' and tax.key == '000':
                    locales_retenciones += impuesto_producto

                if tax.id in totals_tax:
                    totals_tax[tax.id].base += base
                    totals_tax[tax.id].suma_impuestos += impuesto_producto
                else:
                    tax.base = base
                    tax.suma_impuestos = impuesto_producto
                    totals_tax[tax.id] = tax

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

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

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

    def _get_totals_invoice_by_ticket(self, invoice, ids):
        subtotal = 0
        descuento_cfdi = 0
        totals_tax = {}
        total_trasladados = None
        total_retenciones = None
        notes = Tickets.get_notes(ids)

        rows = Tickets.select().where(Tickets.id.in_(ids))

        tax_sum = {}

        for row in rows:
            details = DEFAULT_GLOBAL.copy()
            details['clave'] = row.serie + str(row.folio)
            details['factura'] = invoice.id

            unit_value = row.subtotal
            discount = row.descuento
            final_price = unit_value - discount
            importe = final_price

            details['valor_unitario'] = unit_value
            details['descuento'] = discount
            details['precio_final'] = final_price
            details['importe'] = importe

            descuento_cfdi += details['descuento']
            subtotal += details['importe']
            FacturasDetalle.create(**details)

            taxes = (TicketsImpuestos
                .select(
                    TicketsImpuestos.impuesto,
                    TicketsImpuestos.base,
                    TicketsImpuestos.importe)
                .where(TicketsImpuestos.ticket == row.id)
            )

            for r in taxes:
                tax_id = r.impuesto.id
                tasa = r.impuesto.tasa
                tax_importe = round(tasa * r.base, DECIMALES)
                if tax_id in tax_sum:
                    tax_sum[tax_id]['base'] += r.base
                    tax_sum[tax_id]['importe'] += tax_importe
                else:
                    values = {
                        'tipo': r.impuesto.tipo,
                        'key': r.impuesto.key,
                        'base': r.base,
                        'importe': tax_importe}
                    tax_sum[tax_id] = values

        for i, tax in tax_sum.items():
            tax_importe = round(tax['importe'], DECIMALES)
            invoice_tax = {
                'factura': invoice.id,
                'impuesto': i,
                'base': tax['base'],
                'importe': tax_importe
            }
            FacturasImpuestos.create(**invoice_tax)

            if tax['tipo'] == 'T' and tax['key'] != '000':
                total_trasladados = (total_trasladados or 0) + tax_importe
            elif tax['tipo'] == 'R' and tax['key'] != '000':
                total_retenciones = (total_retenciones or 0) + tax_importe

        total = subtotal - descuento_cfdi + \
            (total_trasladados or 0) - (total_retenciones or 0)

        type_change = Decimal(invoice.tipo_cambio)
        total_mn = round(total * type_change, DECIMALES)
        data = {
            'subtotal': subtotal,
            'descuento': descuento_cfdi,
            'total': total,
            'total_mn': total_mn,
            'total_trasladados': total_trasladados,
            'total_retenciones': total_retenciones,
            'notas': notes,
        }

        return data

    @classmethod
    def invoice(cls, values, user):
        is_invoice_day = util.get_bool(values['is_invoice_day'])
        periodicidad = values.get('periodicidad', '')
        id_client = int(values['client'])
        tickets = util.loads(values['tickets'])
        # ~ invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket')
        invoice_by_ticket = True

        if is_invoice_day:
            filters = (
                Socios.rfc == RFCS['PUBLIC'] and
                Socios.slug == 'publico_en_general'
            )
            try:
                client = Socios.get(filters)
            except Socios.DoesNotExist:
                msg = 'No existe el cliente PUBLICO EN GENERAL. Agregalo primero.'
                data = {'ok': False, 'msg': msg}
                return data
        else:
            client = Socios.get(Socios.id==id_client)
            periodicidad = ''

        if client.forma_pago is None:
            msg = 'La Forma de Pago del cliente, no esta asignada'
            data = {'ok': False, 'msg': msg}
            return data

        try:
            receptor_regimen = SociosRegimenes.get_by_key(client)[0]['id']
        except Exception as e:
            log.error(e)
            msg = 'Error al obtener el Regimen Fiscal del receptor'
            data = {'ok': False, 'msg': msg}
            return data

        payment_type = cls._get_payment_type(cls, tickets)

        emisor = Emisor.select()[0]
        data = {}
        data['cliente'] = client
        data['serie'] = cls._get_serie(cls, user, True)
        data['folio'] = cls._get_folio_invoice(cls, data['serie'])
        data['forma_pago'] = payment_type
        data['tipo_cambio'] = 1.00
        data['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
        data['uso_cfdi'] = client.uso_cfdi.key
        data['regimen_fiscal'] = emisor.regimenes[0].key
        data['receptor_regimen'] = receptor_regimen
        data['periodicidad'] = periodicidad

        with database_proxy.atomic() as txn:
            obj = Facturas.create(**data)
            if is_invoice_day and invoice_by_ticket:
                totals = cls._get_totals_invoice_by_ticket(cls, obj, tickets)
            else:
                totals = cls._calculate_totals_invoice(cls, obj, tickets)
            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.notas = totals['notas']
            obj.save()
            cls._cancel_tickets(cls, obj, tickets)

        msg = 'Factura generada correctamente.<BR><BR>Enviando a timbrar'
        data = {'ok': True, 'msg': msg, 'id': obj.id}
        return data

    def _get_payment_type(self, ids):
        """Get max of payment type"""
        query = (Tickets
            .select(
                Tickets.forma_pago,
                fn.Sum(Tickets.subtotal).alias('total'))
            .where(Tickets.id.in_(ids))
            .group_by(Tickets.forma_pago)
            .order_by(SQL('total').desc())
            .limit(1)
            .scalar()
        )
        return query

    # ~ def _update_inventory_if_cancel(self, id):
        # ~ products = TicketsDetalle.select().where(TicketsDetalle.ticket==id)
        # ~ for p in products:
            # ~ if p.producto.inventario:
                # ~ p.producto.existencia += p.cantidad
                # ~ p.producto.save()
        # ~ return

    @classmethod
    def cancel(cls, values, user):
        admin_cancel = Configuracion.get_bool('chk_cancel_tickets_by_admin')
        if admin_cancel and not user.es_admin:
            msg = 'Solo un admin puede cancelar tickets'
            result = {'ok': False, 'msg': msg, 'row': {}}
            return result

        id = int(values['id'])
        msg = 'Ticket cancelado correctamente'
        fields = {'cancelado': True, 'estatus': 'Cancelado'}
        with database_proxy.atomic() as txn:
            obj = Tickets.update(**fields).where(Tickets.id==id)
            result = bool(obj.execute())
            if result:
                cls._update_inventory(cls, id, True, user)
        row = {'estatus': 'Cancelado'}
        return {'ok': result, 'row': row, 'msg': msg}

    def _get_filters(self, values):
        opt = values.get('opt', '')
        if not opt:
            return

        if opt == 'active':
            filters = (Tickets.cancelado==False)
            return filters

        if opt == 'today':
            t = util.today()
            filters = (
                (Tickets.fecha.day == t.day) &
                (Tickets.fecha.month == t.month) &
                (Tickets.fecha.year == t.year)
            )
            return filters

        if opt == 'yearmonth':
            if values['year'] == '-1':
                fy = (Tickets.fecha.year > 0)
            else:
                fy = (Tickets.fecha.year == int(values['year']))
            if values['month'] == '-1':
                fm = (Tickets.fecha.month > 0)
            else:
                fm = (Tickets.fecha.month == int(values['month']))
            filters = (fy & fm)
            return filters

        if opt == 'dates':
            dates = util.loads(values['range'])
            filters = Tickets.fecha.between(
                util.get_date(dates['start']),
                util.get_date(dates['end'], True)
            )
            return filters

        return

    @classmethod
    def get_by(cls, values, user):
        filter_serie = False

        filter_by_user = Configuracion.get_bool('chk_ticket_user_show_doc')
        if not filter_by_user:
            if not user.sucursal is None and not user.sucursal.serie_tickets is None:
                filter_serie = (Tickets.serie==user.sucursal.serie_tickets)

        filters = cls._get_filters(cls, values)

        if filter_serie:
            filters = filters & filter_serie

        rows = tuple(Tickets
            .select(
                Tickets.id,
                Tickets.serie,
                Tickets.folio,
                Tickets.fecha,
                Tickets.estatus,
                Tickets.total)
            .where(filters)
            .dicts()
        )
        return {'ok': True, 'rows': rows}

    @classmethod
    def filter_years(cls):
        data = [{'id': -1, 'value': 'Todos'}]
        rows = (Tickets
            .select(Tickets.fecha.year.alias('year'))
            .group_by(Tickets.fecha.year)
            .order_by(Tickets.fecha.year)
        )
        if not rows is None:
            data += [{'id': int(r.year), 'value': int(r.year)} for r in rows]
        return tuple(data)

    def _get_info_to_pdf(self, id):
        data = {}
        obj = Tickets.select().where(Tickets.id==id).dicts()[0]

        formapago = SATFormaPago.get(SATFormaPago.key==obj['forma_pago'])

        emisor  = util.get_dict(Emisor.select().dicts()[0])

        emisor['nointerior'] = emisor['no_interior']
        emisor['noexterior'] = emisor['no_exterior']
        emisor['codigopostal'] = emisor['codigo_postal']

        data['is_ticket'] = True
        data['cancelada'] = obj['cancelado']
        data['donativo'] = ''
        if obj['estatus'] == 'Facturado':
            data['cancelada'] = False

        data['comprobante'] = obj
        data['comprobante']['version'] = 'ticket'
        data['comprobante']['folio'] = str(data['comprobante']['folio'])
        data['comprobante']['seriefolio'] = '{}-{}'.format(
            data['comprobante']['serie'], data['comprobante']['folio'])
        data['comprobante']['fecha'] = str(data['comprobante']['fecha'])
        data['comprobante']['formadepago'] = str(formapago)
        data['comprobante']['totalenletras'] = util.to_letters(
            data['comprobante']['total'], 'peso')

        data['emisor'] = emisor
        data['receptor'] = {'nombre': 'Público en general'}

        data['conceptos'] = TicketsDetalle.get_by_ticket(id)
        data['totales'] = {}
        data['totales']['total'] = str(data['comprobante']['total'])
        data['totales']['subtotal'] = str(data['comprobante']['subtotal'])
        data['totales']['moneda'] = 'peso'
        data['totales']['traslados'] = ()
        data['totales']['retenciones'] = ()
        data['totales']['taxlocales'] = ()
        data['timbre'] = {}
        data['donataria'] = {}
        data['ine'] = {}
        data['leyendas'] = ()

        return data

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

    def _format_ticket(self, id):
        emisor  = util.get_dict(Emisor.select().dicts()[0])
        regimen = Emisor.select()[0].regimenes[0].name
        ticket = Tickets.select().where(Tickets.id==id).dicts()[0]
        products = TicketsDetalle.get_by_print(id)
        emisor['name'] = '{}\n'.format(emisor['nombre'])
        emisor['rfc'] = 'RFC: {}\n'.format(emisor['rfc'])
        emisor['regimen'] = 'Regimen: {}\n'.format(regimen)
        interior = ''
        if emisor['no_interior']:
            interior = ', {}'.format(emisor['no_interior'])
        colonia = ''
        if emisor['colonia']:
            colonia = ', Col. {}'.format(emisor['colonia'])
        municipio = ''
        if emisor['municipio']:
            municipio = ', {}'.format(emisor['municipio'])
        estado = ''
        if emisor['estado']:
            estado = ', {}'.format(emisor['estado'])
        cp = ''
        if emisor['codigo_postal']:
            cp = ', C.P. {}'.format(emisor['codigo_postal'])
        pais = ''
        if emisor['pais']:
            pais = ', {}'.format(emisor['pais'])
        direccion = '{} {}{}{}{}{}{}{}\n\n'.format(emisor['calle'],
            emisor['no_exterior'], interior, colonia, municipio, estado, cp,
            pais)
        emisor['address'] = direccion

        ticket['title'] = 'Ticket: {}{}\n\n'.format(ticket['serie'],
            ticket['folio'])
        ticket['date'] = 'Fecha y hora: {}\n'.format(ticket['fecha'])
        ticket['letters'] = '{}\n\n'.format(
            util.to_letters(ticket['total'], 'peso'))
        ticket['total'] = 'TOTAL: $ {:>12,.2f}\n\n'.format(ticket['total'])

        data = {
            'emisor': emisor,
            'receptor': {'name': '{}\n\n'.format(PUBLIC)},
            'ticket': ticket,
            'products': products,
        }
        return data

    def _get_info_printer(self):
        info = {}
        value = Configuracion.get_('txt_ticket_printer')
        if not value:
            return info

        values = value.split(':')
        if len(values) == 1:
            info = {'ip': values[0], 'usb': ()}
        elif len(values) == 2:
            info = {'ip': '', 'usb': (int(values[0], 16), int(values[1], 16))}
        elif len(values) == 5:
            info = {'ip': '', 'usb': (
                int(values[0], 16),
                int(values[1], 16),
                int(values[2]),
                int(values[3], 16),
                int(values[4], 16),
                )
            }

        return info

    @classmethod
    def printer(cls, values):
        id = int(values['id'])

        info_printer = cls._get_info_printer(cls)
        if not info_printer:
            msg = 'Es necesario configurar una impresora.'
            result = {'ok': False, 'msg': msg}
            return result

        data = cls._format_ticket(cls, id)
        result = util.print_ticket(data, info_printer)
        msg = 'Ticket impreso correctamente'
        if not result:
            msg = 'Asegurate de que la impresora este conectada y funcionando.'
        result = {'ok': result, 'msg': msg}
        return result


class TicketsDetalle(BaseModel):
    ticket = ForeignKeyField(Tickets)
    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)
    warehouse = ForeignKeyField(Almacenes, null=True)

    class Meta:
        order_by = ('ticket',)

    def _with_tax(self, product):
        precio_final = float(product.precio_final)
        base = float(product.precio_final)
        for tax in product.producto.impuestos:
            impuesto_producto = round(float(tax.tasa) * base, DECIMALES)
            precio_final += impuesto_producto

        return precio_final

    @classmethod
    def get_by_print(cls, id):
        products = TicketsDetalle.select().where(TicketsDetalle.ticket==id)
        lines = []
        for p in products:
            price_with_tax = cls._with_tax(cls, p)
            importe = round(price_with_tax * float(p.cantidad), DECIMALES)
            l = '{:>6,.2f} {:<4} {:<14} {:>9,.2f} {:>10,.2f}\n'.format(
                p.cantidad, p.producto.unidad.name[:5], p.descripcion[:14],
                price_with_tax, importe
            )
            lines.append(l)

        return lines

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

        productos = TicketsDetalle.select().where(TicketsDetalle.ticket==id)

        for p in productos:
            producto = {}
            producto['noidentificacion'] = p.producto.clave
            producto['descripcion'] = p.descripcion
            producto['unidad'] = p.producto.unidad.name
            producto['cantidad'] = str(p.cantidad)
            price_with_tax = cls._with_tax(cls, p)
            producto['valorunitario'] = str(price_with_tax)
            importe = round(price_with_tax * float(p.cantidad), DECIMALES)
            producto['importe'] = str(importe)
            data.append(producto)

        return data

    def _get_by_ticket_id(cls, args, user):
        products = []
        id = int(args['id'])
        rows = TicketsDetalle.select().where(TicketsDetalle.ticket==id)

        for row in rows:
            # ~ descuento
            # ~ precio_final
            # ~ inventario
            # ~ existencia
            product = dict(
                id_product = row.producto.id,
                clave = row.producto.clave,
                clave_sat = row.producto.clave_sat,
                descripcion = row.descripcion,
                unidad = row.producto.unidad.id,
                cantidad = float(row.cantidad),
                valor_unitario = cls._with_tax(cls, row),
            )
            product['importe'] = round(
                product['valor_unitario'] * product['cantidad'], DECIMALES)
            products.append(product)

        return tuple(products)

    @classmethod
    def get_data(cls, values, user):
        opt = values.pop('opt')
        method = f'_get_{opt}'
        return getattr(cls, method)(cls, values, user)


class TicketsImpuestos(BaseModel):
    ticket = ForeignKeyField(Tickets)
    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 = ('ticket',)
        indexes = (
            (('ticket', 'impuesto'), True),
        )


class SeriesProductos(BaseModel):
    factura = ForeignKeyField(FacturasDetalle, null=True)
    ticket = ForeignKeyField(TicketsDetalle, null=True)
    serie = TextField(default='')

    class Meta:
        order_by = ('serie',)


class Departamentos(BaseModel):
    nombre = TextField(default='', unique=True)
    descripcion = TextField(default='')

    class Meta:
        order_by = ('nombre',)


class Puestos(BaseModel):
    departamento = ForeignKeyField(Departamentos, null=True)
    nombre = TextField(default='')
    descripcion = TextField(default='')

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

    @classmethod
    def get_by_depto(cls, puesto, depto):
        departamento = None
        if depto:
            with database_proxy.transaction():
                departamento, _ = Departamentos.get_or_create(nombre=depto)
        data = {'departamento': departamento, 'nombre': puesto}
        obj, _ = Puestos.get_or_create(**data)
        return obj


class Empleados(BaseModel):
    num_empleado = TextField(default='')
    rfc = TextField(default='', unique=True)
    curp = TextField(default='', unique=True)
    nombre = TextField(default='')
    paterno = TextField(default='')
    materno = TextField(default='')
    nombre_completo = TextField(default='')
    es_activo = BooleanField(default=True)
    es_extranjero = BooleanField(default=False)
    fecha_alta = DateField(default=util.now)
    fecha_ingreso = DateField(null=True)
    imss = TextField(default='')
    tipo_contrato = ForeignKeyField(SATTipoContrato)
    es_sindicalizado = BooleanField(default=False)
    tipo_jornada = ForeignKeyField(SATTipoJornada, null=True)
    tipo_regimen = ForeignKeyField(SATTipoRegimen)
    puesto = ForeignKeyField(Puestos, null=True)
    riesgo_puesto = ForeignKeyField(SATRiesgoPuesto, null=True)
    periodicidad_pago = ForeignKeyField(SATPeriodicidadPago)
    banco = ForeignKeyField(SATBancos, null=True)
    cuenta_bancaria = TextField(default='')
    clabe = TextField(default='')
    salario_base = DecimalField(default=0.0, max_digits=18, decimal_places=6,
        auto_round=True)
    salario_diario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
        auto_round=True)
    estado = ForeignKeyField(SATEstados)
    codigo_postal = TextField(default='')
    notas = TextField(default='')
    correo = TextField(default='')
    regimen_fiscal = TextField(default='')

    class Meta:
        order_by = ('nombre_completo',)
        indexes = (
            (('num_empleado', 'rfc'), True),
        )

    def _validate_import(self, row):
        sn = {'si': True, 'no': False}
        data = row.copy()
        data['nombre_completo'] = '{} {} {}'.format(
            row['nombre'], row['paterno'], row['materno']).strip()

        data['fecha_ingreso'] = None
        if row['fecha_ingreso']:
            data['fecha_ingreso'] = util.calc_to_date(row['fecha_ingreso'])

        data['tipo_contrato'] = SATTipoContrato.get_by_key(row['tipo_contrato'])
        data['es_sindicalizado'] = sn.get(row['es_sindicalizado'].lower(), False)
        data['tipo_jornada'] = SATTipoJornada.get_by_key(row['tipo_jornada'])
        data['tipo_regimen'] = SATTipoRegimen.get_by_key(row['tipo_regimen'])
        data['puesto'] = Puestos.get_by_depto(row['puesto'], row['departamento'])
        data['riesgo_puesto'] = SATRiesgoPuesto.get_by_key(row['riesgo_puesto'])
        data['periodicidad_pago'] = SATPeriodicidadPago.get_by_key(row['periodicidad_pago'])
        data['banco'] = SATBancos.get_by_key(row['banco'])
        data['estado'] = SATEstados.get_by_key(row['estado'])
        del data['departamento']
        return data

    def _import(self):
        emisor = Emisor.select()[0]
        rows, msg = util.import_employees(emisor.rfc)
        if not rows:
            return {'ok': False, 'msg': msg}

        en = 0
        ea = 0
        for row in rows:
            if not DEBUG and row['rfc'] == 'BASM740115RW0':
                continue
            data = self._validate_import(self, row)
            w = (Empleados.rfc==row['rfc'])
            with database_proxy.transaction():
                if Empleados.select().where(w).exists():
                    q = Empleados.update(**data).where(w)
                    q.execute()
                    ea += 1
                else:
                    obj = Empleados.create(**data)
                    en += 1

        msg = 'Empleados:<BR><BR>Encontrados: {}<BR>'.format(len(rows))
        msg += 'Nuevos: {}<BR>'.format(en)
        msg += 'Actualizados: {}<BR>'.format(ea)
        msg += 'No importados: {}'.format(len(rows) - en - ea)
        return {'ok': True, 'msg': msg}

    def _get(self):
        rows = (Empleados
            .select(
                Empleados.id,
                Empleados.num_empleado,
                Empleados.rfc,
                Empleados.curp,
                Empleados.nombre_completo,
                Empleados.imss,
                Empleados.salario_base,
                Empleados.salario_diario,
                Empleados.fecha_ingreso)
            .dicts()
        )
        return {'ok': True, 'rows': tuple(rows)}

    @classmethod
    def get_by(cls, values):
        if not 'opt' in values:
            return cls._get(cls)

        if values['opt'] == 'import':
            return cls._import(cls)

    @classmethod
    def remove(cls, id):
        try:
            q = Empleados.delete().where(Empleados.id==id)
            return bool(q.execute())
        except Exception as e:
            return False


class CfdiNomina(BaseModel):
    empleado = ForeignKeyField(Empleados)
    version = TextField(default=PRE_DEFAULT['CFDI']['VERSION'])
    serie = TextField(default='N')
    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='N')
    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='Guardado')
    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='')
    tipo_relacion = TextField(default='')
    error = TextField(default='')
    version_nomina = TextField(default=PRE_DEFAULT['NOMINA']['VERSION'])
    registro_patronal = TextField(default='')
    rfc_patron_origen = TextField(default='')
    tipo_nomina = ForeignKeyField(SATTipoNomina)
    fecha_pago = DateField()
    fecha_inicial_pago = DateField()
    fecha_final_pago = DateField()
    dias_pagados = DecimalField(default=0.0, max_digits=12, decimal_places=2,
        auto_round=True)
    origen_recurso = ForeignKeyField(SATOrigenRecurso, null=True)
    monto_recurso_propio = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)

    class Meta:
        order_by = ('fecha',)

    @classmethod
    def _get_status_sat(cls, filters, user):
        id = filters['id']

        obj = CfdiNomina.get(CfdiNomina.id == id)
        estatus_sat = utils.get_status_sat(obj.xml)
        if obj.estatus_sat != estatus_sat:
            obj.estatus_sat = estatus_sat
            obj.save()

        if obj.estatus_sat == 'Vigente' and obj.estatus == 'Cancelada':
            days = utils.get_days(obj.fecha_cancelacion)
            if days > 3:
                estatus_sat = 'uncancel'

        return estatus_sat

    @classmethod
    def _get_yearmonth(cls, filters, user):
        if filters['year'] == '-1':
            fy = (CfdiNomina.fecha.year > 0)
        else:
            fy = (CfdiNomina.fecha.year == int(filters['year']))
        if filters['month'] == '-1':
            fm = (CfdiNomina.fecha.month > 0)
        else:
            fm = (CfdiNomina.fecha.month == int(filters['month']))
        filters = (fy & fm)
        return cls._get(cls, filters)

    @classmethod
    def _get_dates(cls, filters, user):
        dates = util.loads(filters['range'])
        filters = CfdiNomina.fecha.between(
            util.get_date(dates['start']),
            util.get_date(dates['end'], True)
        )
        return cls._get(cls, filters)

    @classmethod
    def _get_import(cls, filters, user):
        return cls._import(cls)

    @classmethod
    def _get_stamp(cls, filters, user):
        return cls._stamp(cls)

    @classmethod
    def get_data(cls, values, user):
        opt = values.pop('opt')
        return getattr(cls, f'_get_{opt}')(values, user)

    def _cancel(self, values, user):
        id = int(values['id'])
        args = utils.loads(values['args'])

        obj = CfdiNomina.get(CfdiNomina.id==id)
        if obj.uuid is None:
            msg = 'Solo se pueden cancelar recibos timbrados'
            return {'ok': False, 'msg': msg}

        auth = Configuracion.get_({'fields': 'pac_auth'})
        certificado = Certificado.get(Certificado.es_fiel==False)
        result = utils.cancel_xml_sign(obj, args, auth, certificado)

        if result['ok']:
            obj.estatus = 'Cancelado'
            obj.error = ''
            obj.cancelada = True
            obj.fecha_cancelacion = result['date']
            obj.acuse = result['acuse'] or ''
        else:
            obj.error = result['msg']
        obj.save()

        return result

    def _send_mail(self, values, user):
        id = int(values['id'])
        msg = 'Recibo de Nómina enviado correctamente'
        result = {'ok': True, 'msg': msg}

        config = Configuracion.get_({'fields': 'correo'})
        contra = Configuracion.get_('correo_contra')
        if not config:
            msg = 'Envío de correo no configurado.'
            result['ok'] = False
            result['msg'] = msg
            return result

        emisor = Emisor.select()[0]
        obj = CfdiNomina.get(CfdiNomina.id==id)
        to = obj.empleado.correo
        if not to:
            msg = 'El empleado no tiene correo.'
            result['ok'] = False
            result['msg'] = msg
            return result

        name = '{}{}_{}'.format(obj.serie, obj.folio, obj.empleado.rfc)
        values = self._get_not_in_xml(self, obj, emisor)
        data = util.get_data_from_xml(obj, values)
        doc = util.to_pdf(data, emisor.rfc)
        files = (
            (obj.xml, f'{name}.xml'),
            (doc, f'{name}.pdf'),
        )

        message = subject = f"Enviamos tu recibo de nómina"
        starttls = bool(int(values.get('correo_starttls', '0')))
        server = {
            'server': config['correo_servidor'],
            'port': config['correo_puerto'],
            'ssl': utils.to_bool(config['correo_ssl']),
            'starttls': starttls,
            'user': config['correo_usuario'],
            'pass': utils.decrypt(contra, emisor.rfc),
        }
        mail = {
            'to': to,
            'copy': config.get('correo_copia', ''),
            'subject': subject,
            'message': message,
            'files': files,
        }
        data= {
            'server': server,
            'mail': mail,
        }
        r = utils.send_mail(data)
        if not r['ok']:
            result['ok'] = False
            result['msg'] = r['msg']

        return result

    @classmethod
    def post(cls, values, user):
        opt = values.pop('opt')
        return getattr(cls, '_{}'.format(opt))(cls, values, user)

    def _get_serie(self):
        serie = Configuracion.get_('txt_config_nomina_serie')
        if not serie:
            serie = DEFAULT_SAT_NOMINA['SERIE']
        return serie

    def _get_folio(self, serie):
        folio = int(Configuracion.get_('txt_config_nomina_folio') or '0')
        inicio = (CfdiNomina
            .select(fn.Max(CfdiNomina.folio).alias('mf'))
            .where(CfdiNomina.serie==serie)
            .order_by(SQL('mf'))
            .scalar())

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

        if folio > new:
            new = folio

        return new

    def _get_relacionados(values):
        rows = values.split('|')
        msg = ''
        uuids = []
        for row in rows:
            sf = row.split('-')
            if len(sf) == 1:
                msg = 'Formato incorrecto en CFDI relacionado: SERIE-FOLIO'
                return [], msg
            try:
                obj = CfdiNomina.get(
                    CfdiNomina.serie==sf[0], CfdiNomina.folio==int(sf[1]))
            except CfdiNomina.DoesNotExist:
                msg = 'No existe el CFDI relacionado: {}'.format(row)
                return [], msg

            uuids.append({'cfdi_origen': obj.id})

        return uuids, msg

    def _validate_nomina(self, row):
        sn = {'si': True, 'no': False}
        data = row.copy()
        rfc = data.pop('rfc')
        days_pay = data.pop('dias_pagados')
        try:
            data['empleado'] = Empleados.get(Empleados.rfc==rfc)
        except Empleados.DoesNotExist:
            msg = 'No existe el Empleado con RFC: {}'.format(rfc)
            return {}, msg

        tipo_nomina = SATTipoNomina.get_by_key(row['tipo_nomina'])
        if tipo_nomina is None:
            msg = 'RFC: {}, Tipo de Nómina no existe: {}'.format(row['tipo_nomina'])
            return {}, msg

        if data['relacionados']:
            data['relacionados'], msg = self._get_relacionados(data['relacionados'])
            if msg:
                return {}, msg

        if isinstance(row['fecha_pago'], str):
            msg = f"La Fecha de Pago debe ser una fecha: {row['fecha_pago']}"
            return {}, msg

        if isinstance(row['fecha_inicial_pago'], str):
            msg = f"La Fecha Inicial de Pago debe ser una fecha: {row['fecha_inicial_pago']}"
            return {}, msg

        if isinstance(row['fecha_final_pago'], str):
            msg = f"La Fecha Final de Pago debe ser una fecha: {row['fecha_final_pago']}"
            return {}, msg

        data['serie'] = self._get_serie(self)
        data['folio'] = self._get_folio(self, data['serie'])
        data['forma_pago'] = DEFAULT_SAT_NOMINA['FORMA_PAGO']
        data['uso_cfdi'] = DEFAULT_SAT_NOMINA['USO_CFDI']
        data['tipo_nomina'] = tipo_nomina
        data['fecha_pago'] = util.calc_to_date(row['fecha_pago'])
        data['fecha_inicial_pago'] = util.calc_to_date(row['fecha_inicial_pago'])
        data['fecha_final_pago'] = util.calc_to_date(row['fecha_final_pago'])
        data['dias_pagados'] = days_pay

        return data, ''

    def _validate_percepciones(self, headers, row):
        total_gravado = 0.0
        total_exento = 0.0
        total_jubilacion = 0.0
        total_separacion = 0.0

        data = []
        for i, key in enumerate(headers[::2]):
            gravado = 0.0
            exento = 0.0
            if isinstance(row[i * 2], float):
                gravado = round(row[i * 2], DECIMALES)
            if isinstance(row[i * 2 + 1], float):
                exento = round(row[i * 2 + 1], DECIMALES)

            if not gravado and not exento:
                continue
            tp = SATTipoPercepcion.get_by_key(key)
            if tp is None:
                continue

            total_gravado += gravado
            total_exento += exento
            if key in ('039', '044'):
                total_jubilacion += gravado + exento
            elif key in ('022', '023', '025'):
                total_separacion += gravado + exento

            new = {
                'tipo_percepcion': tp,
                'importe_gravado': gravado,
                'importe_exento': exento,
            }
            data.append(new)

        total_percepciones = round(total_gravado + total_exento, DECIMALES)
        total_sueldos = round(total_percepciones - total_jubilacion - total_separacion, DECIMALES)
        totals = {
            'total_gravado': total_gravado,
            'total_exento': total_exento,
            'total_jubilacion': total_jubilacion,
            'total_separacion': total_separacion,
            'total_sueldos': total_sueldos,
            'total_percepciones': total_percepciones
        }

        return data, totals, ''

    def _validate_deducciones(self, headers, row, new_titles):
        total_retenciones = 0.0
        total_otras_deducciones = 0.0

        data = []
        for i, value in enumerate(row):
            key = headers[0][i]
            importe = 0.0
            if isinstance(value, float):
                importe = round(value, DECIMALES)

            if not importe:
                continue

            td = SATTipoDeduccion.get_by_key(key)
            if td is None:
                continue

            if key == '002':
                total_retenciones += importe
            else:
                total_otras_deducciones += importe

            new = {
                'tipo_deduccion': td,
                'importe': importe,
                'concepto': new_titles.get(i, ''),
            }
            data.append(new)

        totals = {
            'total_retenciones': total_retenciones,
            'total_otras_deducciones': total_otras_deducciones,
            'total_deducciones': round(
                total_retenciones + total_otras_deducciones, DECIMALES)
        }

        return data, totals, ''

    def _validate_otros_pagos(self, headers, row):
        total_otros_pagos = 0.0

        data = []
        subsidio_causado = round(row[0], DECIMALES)
        for i, value in enumerate(row):
            if not i:
                continue

            key = headers[0][i]
            importe = 0.0
            if isinstance(value, float):
                importe = round(value, DECIMALES)

            # ~ if not importe:
                # ~ continue

            td = SATTipoOtroPago.get_by_key(key)
            if td is None:
                continue

            total_otros_pagos += importe

            new = {
                'tipo_otro_pago': td,
                'importe': importe,
            }
            if key == '002':
                new['subsidio_causado'] = subsidio_causado
            data.append(new)

        totals = {'total_otros_pagos': total_otros_pagos}

        return data, totals, ''

    def _validate_separacion(self, row):
        if not row[0]:
            return {}, ''

        if not row[2] or isinstance(row[2], str):
            msg = 'El campo Ultimo Sueldo debe ser un importe'
            return {}, msg

        if isinstance(row[3], str):
            msg = 'El campo Ingreso Acumulable debe ser un importe'
            return {}, msg

        if isinstance(row[4], str):
            msg = 'El campo Ingreso No Acumulable debe ser un importe'
            return {}, msg

        new = {
            'total_pagado': round(row[0], DECIMALES),
            'years_servicio': int(row[1]),
            'ultimo_sueldo': round(row[2], DECIMALES),
            'ingreso_acumulable': round(row[3], DECIMALES),
            'ingreso_no_acumulable': round(row[4], DECIMALES),
        }
        return new, ''

    def _validate_horas_extras(self, row):
        data = []
        for i, key in enumerate(row[::4]):
            days = 0
            if isinstance(row[i * 4], float):
                days = int(row[i * 4])
            key = row[i * 4 + 1]
            the = SATTipoHoras.get_by_key(key)
            if the is None:
                continue

            hours = 0
            if isinstance(row[i * 4 + 2], float):
                hours = int(row[i * 4 + 2])
            importe = 0.0
            if isinstance(row[i * 4 + 3], float):
                importe = round(row[i * 4 + 3], DECIMALES)
            if not hours or not importe:
                continue

            new = {
                'dias': days,
                'tipos_horas': the,
                'horas_extra': hours,
                'importe_pagado': importe,
            }
            data.append(new)

        return data, ''

    def _validate_incapacidades(self, row):
        data = []
        for i, key in enumerate(row[::3]):
            msg = ''
            key = row[i * 3]
            ti = SATTipoIncapacidad.get_by_key(key)
            if ti is None:
                msg = 'No se encontrón el tipo de incapacidad'
                break

            if isinstance(row[i * 3 + 1], str):
                msg = 'Los dias de incapacidad debe ser un número'
                break
            if isinstance(row[i * 3 + 2], str):
                msg = 'El importe de la incapacidad debe ser un número'
                break

            days = int(row[i * 3 + 1])
            importe = round(row[i * 3 + 2], DECIMALES)
            if not days or not importe:
                break

            new = {
                'dias': days,
                'tipo': ti,
                'importe': importe,
            }
            data.append(new)

        return data, msg

    def _validate_exists(self, values):
        result = (CfdiNomina
            .select()
            .where(
                (CfdiNomina.empleado==values['empleado']) &
                (CfdiNomina.fecha_pago==values['fecha_pago']) &
                (CfdiNomina.fecha_inicial_pago==values['fecha_inicial_pago']) &
                (CfdiNomina.fecha_final_pago==values['fecha_final_pago']) &
                (CfdiNomina.total==values['total']) &
                (CfdiNomina.cancelada==False)
            )
            .exists())
        return result

    def _import(self):
        util.log_file('nomina', kill=True)
        emisor = Emisor.select()[0]
        data, msg = util.import_nomina(emisor.rfc)
        if not data:
            return {'ok': False, 'msg': msg}

        hp = data['percepciones'][0]
        percepciones = data['percepciones'][2:]
        hd = data['deducciones'][:1]
        deducciones = data['deducciones'][2:]
        ho = data['otros_pagos'][:1]
        otros_pagos = data['otros_pagos'][2:]
        separacion = data['separacion'][2:]
        horas_extras = data['horas_extras'][2:]
        incapacidades = data['incapacidades'][2:]
        new_titles = data['new_titles']

        for i, row in enumerate(data['nomina']):
            row['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
            row['regimen_fiscal'] = emisor.regimenes[0].key
            row['registro_patronal'] = emisor.registro_patronal
            new_nomina, msg = self._validate_nomina(self, row)
            if msg:
                util.log_file('nomina', msg)
                continue

            relacionados = new_nomina.pop('relacionados', '')
            if relacionados:
                new_nomina['tipo_relacion'] = '04'

            new_percepciones, total_percepciones, msg = \
                self._validate_percepciones(self, hp, percepciones[i])
            if msg:
                util.log_file('nomina', msg)
                continue

            new_deducciones, total_deducciones, msg = \
                self._validate_deducciones(self, hd, deducciones[i], new_titles)
            if msg:
                util.log_file('nomina', msg)
                continue

            new_otros_pagos, total_otros_pagos, msg = \
                self._validate_otros_pagos(self, ho, otros_pagos[i])
            if msg:
                util.log_file('nomina', msg)
                continue

            new_separacion, msg = self._validate_separacion(self, separacion[i])
            if msg:
                util.log_file('nomina', msg)
                continue

            new_horas_extras, msg = self._validate_horas_extras(self, horas_extras[i])
            if msg:
                util.log_file('nomina', msg)
                continue

            new_incapacidades, msg = self._validate_incapacidades(self, incapacidades[i])
            if msg:
                util.log_file('nomina', msg)
                continue

            totals = total_percepciones.copy()
            totals.update(total_deducciones)
            totals.update(total_otros_pagos)
            totals['subtotal'] = round(totals['total_percepciones'] +
                totals['total_otros_pagos'], DECIMALES)
            totals['descuento'] = totals['total_deducciones']
            totals['total'] = round(totals['subtotal'] -
                totals['descuento'], DECIMALES)

            new_nomina['subtotal'] = totals['subtotal']
            new_nomina['descuento'] = totals['descuento']
            new_nomina['total'] = totals['total']
            new_nomina['total_mn'] = totals['total']

            if self._validate_exists(self, new_nomina):
                info = '{}'.format(new_nomina['empleado'].nombre_completo)
                msg = 'Nomina existente: {}'.format(info)
                util.log_file('nomina', msg)
                continue

            try:
                with database_proxy.transaction():
                    obj = CfdiNomina.create(**new_nomina)
                    for row in new_percepciones:
                        row['cfdi'] = obj
                        CfdiNominaPercepciones.create(**row)
                    for row in new_deducciones:
                        row['cfdi'] = obj
                        CfdiNominaDeducciones.create(**row)
                    for row in new_otros_pagos:
                        row['cfdi'] = obj
                        CfdiNominaOtroPago.create(**row)
                    for row in new_horas_extras:
                        row['cfdi'] = obj
                        CfdiNominaHorasExtra.create(**row)
                    for row in new_incapacidades:
                        row['cfdi'] = obj
                        CfdiNominaIncapacidad.create(**row)
                    for row in relacionados:
                        row['cfdi'] = obj
                        CfdiNominaRelacionados.create(**row)

                    if new_separacion:
                        new_separacion['cfdi'] = obj
                        CfdiNominaSeparacion.create(**new_separacion)

                    concepto = {
                        'cfdi': obj,
                        'valor_unitario': totals['subtotal'],
                        'importe': totals['subtotal'],
                        'descuento': totals['total_deducciones'],
                    }
                    CfdiNominaDetalle.create(**concepto)

                    totals['cfdi'] = obj
                    CfdiNominaTotales.create(**totals)
            except Exception as e:
                msg = 'ERROR: {}-{}'.format(new_nomina['serie'], new_nomina['folio'])
                util.log_file('nomina', msg)
                util.log_file('nomina', str(e))
                continue

        msg = 'Nómina importada correctamente'
        return {'ok': True, 'msg': msg}

    def _get(self, where=''):
        if not where:
            where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False))
        rows = (CfdiNomina
            .select(
                CfdiNomina.id,
                CfdiNomina.serie,
                CfdiNomina.folio,
                CfdiNomina.uuid,
                CfdiNomina.fecha,
                CfdiNomina.estatus,
                CfdiNomina.fecha_pago,
                CfdiNomina.total,
                Empleados.nombre_completo.alias('empleado')
            )
            .where(where)
            .join(Empleados)
            .switch(CfdiNomina)
            .order_by(CfdiNomina.id)
            .dicts()
        )
        return {'ok': True, 'rows': tuple(rows)}

    def _validate_rules_nomina(self, data):
        receptor = data['receptor']
        if receptor['TipoContrato'] in ('09', '10', '99'):
            data['emisor'].pop('RegistroPatronal', None)

        # ~ msg = 'Recibo: {}-{}'.format(row.serie, row.folio)
        # ~ util.log_file('nomina', msg)

        # ~ NOM 154
        if data['nomina']['TipoNomina'] == 'E':
            data['receptor']['PeriodicidadPago'] = '99'

        # ~ NOM 162
            # ~ Si el atributo TipoContrato está entre  01 al 08, el atributo
            # ~ Nomina.Emisor.RegistroPatronal debe existir.

        return

    def _make_xml(self, cfdi):
        emisor = Emisor.select()[0]
        empleado = cfdi.empleado
        certificado = Certificado.get(Certificado.es_fiel==False)
        totals = CfdiNominaTotales.select().where(CfdiNominaTotales.cfdi==cfdi)[0]

        comprobante = {}
        relacionados = {}
        complementos = None

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

        if cfdi.tipo_relacion:
            relacionados = {
                'tipo': cfdi.tipo_relacion,
                'cfdis': CfdiNominaRelacionados.get_(cfdi),
            }

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

        receptor = {
            'Rfc': cfdi.empleado.rfc,
            'Nombre': cfdi.empleado.nombre_completo,
            'DomicilioFiscalReceptor': cfdi.empleado.codigo_postal,
            'RegimenFiscalReceptor': cfdi.empleado.regimen_fiscal,
            'UsoCFDI': cfdi.uso_cfdi,
        }

        conceptos = []
        rows = CfdiNominaDetalle.select().where(CfdiNominaDetalle.cfdi==cfdi)
        for row in rows:
            concepto = {
                'ClaveProdServ': row.clave_sat,
                'Cantidad': '1',
                'ClaveUnidad': row.clave_unidad,
                'Descripcion': row.descripcion,
                'ValorUnitario': FORMAT.format(row.valor_unitario),
                'Importe': FORMAT.format(row.importe),
                'ObjetoImp': '01',
            }
            if row.descuento:
                concepto['Descuento'] = FORMAT.format(row.descuento)

            conceptos.append(concepto)

        nomina = {
            'Version': cfdi.version_nomina,
            'TipoNomina': cfdi.tipo_nomina.key,
            'FechaPago': str(cfdi.fecha_pago),
            'FechaInicialPago': str(cfdi.fecha_inicial_pago),
            'FechaFinalPago': str(cfdi.fecha_final_pago),
            'NumDiasPagados': FORMAT3.format(cfdi.dias_pagados),
        }
        if totals.total_percepciones:
            nomina['TotalPercepciones'] = FORMAT.format(totals.total_percepciones)
        if totals.total_deducciones:
            nomina['TotalDeducciones'] = FORMAT.format(totals.total_deducciones)
        # ~ if totals.total_otros_pagos:
        nomina['TotalOtrosPagos'] = FORMAT.format(totals.total_otros_pagos)

        nomina_emisor = {}
        if emisor.curp:
            nomina_emisor['Curp'] = emisor.curp
        if emisor.registro_patronal:
            nomina_emisor['RegistroPatronal'] = emisor.registro_patronal

        nomina_receptor = {
            'Curp': empleado.curp,
            'TipoContrato': empleado.tipo_contrato.key,
            'Sindicalizado': {True: 'Si', False: 'No'}.get(empleado.es_sindicalizado),
            'TipoJornada': empleado.tipo_jornada.key,
            'TipoRegimen': empleado.tipo_regimen.key,
            'NumEmpleado': str(empleado.num_empleado),
            'PeriodicidadPago': empleado.periodicidad_pago.key,
            'ClaveEntFed': empleado.estado.key,
        }
        if empleado.riesgo_puesto.key != '99':
            nomina_receptor['RiesgoPuesto'] = empleado.riesgo_puesto.key

        if empleado.imss:
            nomina_receptor['NumSeguridadSocial'] = empleado.imss.replace('-', '')

        if empleado.fecha_ingreso:
            nomina_receptor['FechaInicioRelLaboral'] = str(empleado.fecha_ingreso)
            days = util.get_days(empleado.fecha_ingreso, cfdi.fecha_final_pago)
            weeks = days // 7
            if weeks:
                ant = 'P{}W'.format(weeks)
            else:
                ant = 'P{}D'.format(days)
            nomina_receptor['Antigüedad'] = ant

        if empleado.puesto.nombre:
            if empleado.puesto.departamento.nombre:
                nomina_receptor['Departamento']  = empleado.puesto.departamento.nombre
            nomina_receptor['Puesto']  = empleado.puesto.nombre

        if empleado.clabe:
            nomina_receptor['CuentaBancaria']  = empleado.clabe.replace('-', '')
        elif empleado.cuenta_bancaria:
            nomina_receptor['CuentaBancaria']  = empleado.cuenta_bancaria.replace('-', '')
            nomina_receptor['Banco']  = empleado.banco.key

        if empleado.salario_base:
            nomina_receptor['SalarioBaseCotApor'] = FORMAT.format(empleado.salario_base)
        if empleado.salario_diario:
            nomina_receptor['SalarioDiarioIntegrado'] = FORMAT.format(empleado.salario_diario)

        percepciones = {
            'TotalSueldos': FORMAT.format(totals.total_sueldos),
            'TotalGravado': FORMAT.format(totals.total_gravado),
            'TotalExento': FORMAT.format(totals.total_exento),
        }
        if totals.total_separacion:
            percepciones['TotalSeparacionIndemnizacion'] = FORMAT.format(totals.total_separacion)
        if totals.total_jubilacion:
            percepciones['TotalJubilacionPensionRetiro'] = FORMAT.format(totals.total_jubilacion)

        rows = CfdiNominaPercepciones.select().where(
            CfdiNominaPercepciones.cfdi==cfdi)
        details = []
        for row in rows:
            concepto = row.concepto or row.tipo_percepcion.nombre or row.tipo_percepcion.name
            p = {
                'TipoPercepcion': row.tipo_percepcion.key,
                'Clave': row.tipo_percepcion.clave or row.tipo_percepcion.key,
                'Concepto': concepto[:100],
                'ImporteGravado': FORMAT.format(row.importe_gravado),
                'ImporteExento': FORMAT.format(row.importe_exento),
            }
            details.append(p)
        percepciones['details'] = details

        rows = CfdiNominaSeparacion.select().where(CfdiNominaSeparacion.cfdi==cfdi)
        for row in rows:
            separacion = {
                'TotalPagado': FORMAT.format(row.total_pagado),
                'NumAñosServicio': str(row.years_servicio),
                'UltimoSueldoMensOrd': FORMAT.format(row.ultimo_sueldo),
                'IngresoAcumulable': FORMAT.format(row.ingreso_acumulable),
                'IngresoNoAcumulable': FORMAT.format(row.ingreso_no_acumulable),
            }
            percepciones['separacion'] = separacion
            break

        rows = CfdiNominaHorasExtra.select().where(CfdiNominaHorasExtra.cfdi==cfdi)
        details = []
        for row in rows:
            n = {
                'Dias': str(row.dias),
                'TipoHoras': row.tipos_horas.key,
                'HorasExtra': str(row.horas_extra),
                'ImportePagado': FORMAT.format(row.importe_pagado),
            }
            details.append(n)
        percepciones['hours_extra'] = details

        deducciones = {
            'TotalOtrasDeducciones': FORMAT.format(totals.total_otras_deducciones),
        }
        if totals.total_retenciones:
            deducciones['TotalImpuestosRetenidos'] = \
              FORMAT.format(totals.total_retenciones)

        rows = CfdiNominaDeducciones.select().where(CfdiNominaDeducciones.cfdi==cfdi)
        details = []
        for row in rows:
            concepto = row.concepto or row.tipo_deduccion.nombre or row.tipo_deduccion.name
            p = {
                'TipoDeduccion': row.tipo_deduccion.key,
                'Clave': row.tipo_deduccion.clave or row.tipo_deduccion.key,
                'Concepto': concepto[:100],
                'Importe': FORMAT.format(row.importe),
            }
            details.append(p)
        deducciones['details'] = details

        rows = CfdiNominaOtroPago.select().where(CfdiNominaOtroPago.cfdi==cfdi)
        otros_pagos = []
        for row in rows:
            concepto = row.concepto or row.tipo_otro_pago.nombre or row.tipo_otro_pago.name

            if nomina_receptor['TipoRegimen'] != '02' and \
                row.tipo_otro_pago.key in ('002', '007', '008'):
                continue

            p = {
                'TipoOtroPago': row.tipo_otro_pago.key,
                'Clave': row.tipo_otro_pago.clave or row.tipo_otro_pago.key,
                'Concepto': concepto[:100],
                'Importe': FORMAT.format(row.importe),
            }
            if row.tipo_otro_pago.key == '002':
                p['subsidio'] = {
                    'SubsidioCausado': FORMAT.format(row.subsidio_causado)
                }
            otros_pagos.append(p)

        if not otros_pagos:
            del nomina['TotalOtrosPagos']

        rows = CfdiNominaIncapacidad.select().where(CfdiNominaIncapacidad.cfdi==cfdi)
        incapacidades = []
        for row in rows:
            n = {
                'DiasIncapacidad': str(row.dias),
                'TipoIncapacidad': row.tipo.key,
                'ImporteMonetario': FORMAT.format(row.importe),
            }
            incapacidades.append(n)

        nomina = {
            'nomina': nomina,
            'emisor': nomina_emisor,
            'receptor': nomina_receptor,
            'percepciones': percepciones,
            'deducciones': deducciones,
            'otros_pagos': otros_pagos,
            'incapacidades': incapacidades,
        }
        self._validate_rules_nomina(self, nomina)

        data = {
            'comprobante': comprobante,
            'relacionados': relacionados,
            'emisor': cfdi_emisor,
            'receptor': receptor,
            'conceptos': conceptos,
            'complementos': complementos,
            'nomina': nomina,
            'impuestos': {},
            'donativo': {},
        }
        # ~ return util.make_xml(data, certificado, auth)
        return utils.make_xml(data, certificado)

    def _stamp_id(self, id):
        # ~ auth = Emisor.get_auth()
        auth = Configuracion.get_({'fields': 'pac_auth'})
        obj = CfdiNomina.get(CfdiNomina.id==id)
        obj.xml = self._make_xml(self, obj)
        obj.estatus = 'Generado'
        obj.save()

        # ~ result = util.timbra_xml(obj.xml, auth)
        result = utils.xml_stamp(obj.xml, auth)
        # ~ print (result)
        if result['ok']:
            obj.xml = result['xml']
            obj.uuid = result['uuid']
            obj.fecha_timbrado = result['date']
            obj.estatus = 'Timbrado'
            obj.error = ''
            obj.save()
            # ~ cls._sync(cls, id, auth)
        else:
            msg = result['error']
            obj.estatus = 'Error'
            obj.error = msg
            obj.save()

        return result['ok'], obj.error

    def _stamp(self):
        msg = ''
        where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False))
        rows = CfdiNomina.select().where(where).order_by(CfdiNomina.id)
        util.log_file('nomina', kill=True)

        if not len(rows):
            msg = 'Sin recibos por timbrar'
            return {'ok': True, 'msg_ok': msg}

        msg_error = ''
        ok_stamp = 0
        for row in rows:
            msg = 'Timbrando el recibo: {}-{}'.format(row.serie, row.folio)
            util.log_file('nomina', msg)
            result, msg = self._stamp_id(self, row.id)
            if result:
                msg = 'Recibo: {}-{}, timbrado correctamente'.format(row.serie, row.folio)
                ok_stamp += 1
                util.log_file('nomina', msg)
            else:
                msg = 'Error la timbrar: {}-{}, {}'.format(row.serie, row.folio, msg)
                util.log_file('nomina', msg)
                msg_error = msg
                break

        ok = False
        if ok_stamp:
            msg = 'Se timbraron {} recibos'.format(ok_stamp)
            ok = True

        error = False
        if msg_error:
            error = True

        return {'ok': ok, 'msg_ok': msg, 'error': error, 'msg_error': msg_error}

    def _get_by_download(self, filters):
        emisor = Emisor.select()[0]
        ids = util.loads(filters['ids'])
        w = CfdiNomina.id.in_(ids)
        rows = CfdiNomina.select().where(w)

        files = {}
        for row in rows:
            name = '{}{}_{}'.format(row.serie, row.folio, row.empleado.rfc)
            files[f'{name}.xml'] = row.xml

            values = self._get_not_in_xml(self, row, emisor)
            data = util.get_data_from_xml(row, values)
            doc = util.to_pdf(data, emisor.rfc)
            files[f'{name}.pdf'] = doc

        fz = utils.to_zip(files)

        return {'data': fz, 'name': name + 'zip'}

    @classmethod
    def get_by(cls, values):
        if not values:
            return cls._get(cls)

        if values.get('by', ''):
            return getattr(cls, f"_get_by_{values['by']}")(cls, values)

    @classmethod
    def remove(cls, ids):
        ids = util.loads(ids)
        for id in ids:
            obj = CfdiNomina.get(CfdiNomina.id==id)
            if obj.uuid:
                continue
            with database_proxy.transaction():
                q = CfdiNominaDetalle.delete().where(CfdiNominaDetalle.cfdi==obj)
                q.execute()
                q = CfdiNominaTotales.delete().where(CfdiNominaTotales.cfdi==obj)
                q.execute()
                q = CfdiNominaPercepciones.delete().where(CfdiNominaPercepciones.cfdi==obj)
                q.execute()
                q = CfdiNominaDeducciones.delete().where(CfdiNominaDeducciones.cfdi==obj)
                q.execute()
                q = CfdiNominaOtroPago.delete().where(CfdiNominaOtroPago.cfdi==obj)
                q.execute()
                q = CfdiNominaSeparacion.delete().where(CfdiNominaSeparacion.cfdi==obj)
                q.execute()
                q = CfdiNominaHorasExtra.delete().where(CfdiNominaHorasExtra.cfdi==obj)
                q.execute()
                q = CfdiNominaIncapacidad.delete().where(CfdiNominaIncapacidad.cfdi==obj)
                q.execute()
                obj.delete_instance()

        return True

    @classmethod
    def get_xml(cls, id):
        obj = CfdiNomina.get(CfdiNomina.id==id)
        name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.empleado.rfc)
        # ~ cls._sync_xml(cls, obj)
        return obj.xml, name

    def _get_not_in_xml(self, invoice, emisor):
        values = {'is_nomina': True, 'el.version': VERSION_EMPRESA_LIBRE}

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

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

        obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago)
        values['formadepago'] = '{} ({})'.format(obj.name, obj.key)

        obj = SATPeriodicidadPago.get(SATPeriodicidadPago.id==invoice.empleado.periodicidad_pago)
        values['periodicidaddepago'] = '{} ({})'.format(obj.name, obj.key)

        values['tiporelacion'] = invoice.tipo_relacion
        values['usocfdi'] = invoice.uso_cfdi
        values['receptor'] = {}
        values['fechadof'] = None
        values['version'] = invoice.version_nomina

        return values

    @classmethod
    def get_pdf(cls, id, rfc, sync=True):
        try:
            emisor = Emisor.select()[0]
        except IndexError:
            return b'', 'sin_datos_de_emisor.pdf'

        obj = CfdiNomina.get(CfdiNomina.id==id)
        name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.empleado.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)

        data.update(utils.CfdiToDict(obj.xml).values)

        doc = util.to_pdf(data, emisor.rfc)

        # ~ if sync:
            # ~ target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
            # ~ cls._sync_pdf(cls, doc, name, target)
        return doc, name

    @classmethod
    def filter_years(cls):
        data = [{'id': -1, 'value': 'Todos'}]
        rows = (CfdiNomina
            .select(CfdiNomina.fecha.year.alias('year'))
            .group_by(CfdiNomina.fecha.year)
            .order_by(CfdiNomina.fecha.year)
        )
        if not rows is None:
            data += [{'id': int(r.year), 'value': int(r.year)} for r in rows]
        return tuple(data)


class CfdiNominaDetalle(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    clave_sat = TextField(default=DEFAULT_SAT_NOMINA['CLAVE'])
    cantidad = DecimalField(default=1.0, max_digits=18, decimal_places=6,
        auto_round=True)
    clave_unidad = TextField(default=DEFAULT_SAT_NOMINA['UNIDAD'])
    descripcion = TextField(default=DEFAULT_SAT_NOMINA['DESCRIPCION'])
    valor_unitario = 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)
    descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
        auto_round=True)

    class Meta:
        order_by = ('cfdi',)


class CfdiNominaTotales(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    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)
    total_percepciones = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    total_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_deducciones = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_otros_pagos = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_sueldos = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_separacion = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_jubilacion = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_retenciones = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    total_otras_deducciones = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    total = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)


class CfdiNominaJubilacion(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    total_una_exhibicion = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    total_parcialidad = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    monto_diario = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    ingreso_acumulable = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)


class CfdiNominaSeparacion(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    total_pagado = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    years_servicio = IntegerField(default=0)
    ultimo_sueldo = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    ingreso_acumulable = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)
    ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)


class CfdiNominaPercepciones(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    tipo_percepcion = ForeignKeyField(SATTipoPercepcion)
    concepto = TextField(default='')
    importe_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    importe_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    valor_mercado = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    precio_al_ortorgarse = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)


class CfdiNominaDeducciones(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    tipo_deduccion = ForeignKeyField(SATTipoDeduccion)
    concepto = TextField(default='')
    importe = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)


class CfdiNominaOtroPago(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    tipo_otro_pago = ForeignKeyField(SATTipoOtroPago)
    concepto = TextField(default='')
    importe = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    subsidio_causado = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    saldo_a_favor = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    year = IntegerField(default=0)
    remanente_saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)


class CfdiNominaIncapacidad(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    dias = IntegerField(default=0)
    tipo = ForeignKeyField(SATTipoIncapacidad)
    importe = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)


class CfdiNominaHorasExtra(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    dias = IntegerField(default=0)
    tipos_horas = ForeignKeyField(SATTipoHoras)
    horas_extra = IntegerField(default=0)
    importe_pagado = DecimalField(default=0.0, max_digits=20,
        decimal_places=6, auto_round=True)


class CfdiNominaSubcontratos(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    rfc = TextField(default='')
    porcentaje = DecimalField(default=0.0, max_digits=12, decimal_places=2,
        auto_round=True)

    class Meta:
        order_by = ('cfdi',)


class CfdiNominaOtros(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina)
    node = TextField(default='')
    key = TextField(default='')
    value = TextField(default='')


class CfdiNominaRelacionados(BaseModel):
    cfdi = ForeignKeyField(CfdiNomina, related_name='original')
    cfdi_origen = ForeignKeyField(CfdiNomina, related_name='relacion')

    class Meta:
        order_by = ('cfdi',)

    @classmethod
    def get_(cls, cfdi):
        query = (CfdiNominaRelacionados
            .select()
            .where(CfdiNominaRelacionados.cfdi==cfdi)
        )
        return [str(r.cfdi_origen.uuid) for r in query]


class PartnerProducts(BaseModel):
    partner = ForeignKeyField(Socios)
    key = TextField(default='')
    product = ForeignKeyField(Productos)

    class Meta:
        order_by = ('partner', 'key')
        indexes = (
            (('partner', 'key', 'product'), True),
        )

    def _get_product(self, filters):
        partner = utils.loads(filters['partner'])

        id = int(partner['id'])
        rfc = partner['rfc']
        key = filters['product_key']

        where = (Socios.rfc == rfc)
        if id:
            where = (Socios.id == id)
        where = (where & (PartnerProducts.key == key))
        select = (
            Productos.id.alias('id_product'),
            Productos.clave_sat.alias('key_sat1'),
            Productos.descripcion.alias('description1'),
            Productos.valor_unitario.alias('unit_value1'),
        )
        try:
            obj = (PartnerProducts
                .select(*select)
                .join(Socios).switch(PartnerProducts)
                .join(Productos).switch(PartnerProducts)
                .where(where)
                .dicts()[0]
            )
        except IndexError:
            msg = 'No se encontró un producto existente'
            data = {'ok': False, 'msg': msg}
            return data

        data = {'ok': True, 'row': obj}

        return data

    @classmethod
    def get_data(cls, filters):
        method = f"_get_{filters['opt']}"
        return getattr(cls, method)(cls, filters)


class PartnerInvoices(BaseModel):
    partner = ForeignKeyField(Socios)
    serie = TextField(default='')
    folio = TextField(default='')
    cfdi_date = DateTimeField()
    cfdi_stamp = DateTimeField()
    pay_way = TextField(default='')
    pay_conditions = TextField(default='')
    pay_method = TextField(default='')
    subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    discount = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    currency = TextField(default='MXN')
    type_change = 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)
    type_cfdi = TextField(default='')
    cfdi_use = 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()
    status = TextField(default='Guardada')
    status_sat = TextField(default='Vigente')
    notes = TextField(default='')
    balance = DecimalField(default=0.0, max_digits=20, decimal_places=6,
        auto_round=True)
    pagada = BooleanField(default=False)
    cancelada = BooleanField(default=False)
    date_cancel = DateTimeField(null=True)
    error = TextField(default='')

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


class InventoryEntries(BaseModel):
    storage = ForeignKeyField(Almacenes, null=True)
    invoice = ForeignKeyField(PartnerInvoices, null=True)
    product = ForeignKeyField(Productos)
    date_add = DateTimeField(default=utils.now)
    cant = DecimalField(default=0.0, max_digits=18, decimal_places=2,
        auto_round=True)

    class Meta:
        order_by = ('-date_add',)
        indexes = (
            (('storage', 'product', 'date_add', 'cant'), True),
        )

    @classmethod
    def get_data(cls, filters):
        method = f"_get_{filters['opt']}"
        return getattr(cls, method)(cls, filters)

    def _get_or_create_partner(self, args):
        obj = None
        msg = ''
        id = int(args['id'])

        type_person = 1
        if len(args['rfc']) == 12:
            type_person = 2
        if args['rfc'] == RFCS['FOREIGN']:
            type_person = 4

        fields = dict(
            tipo_persona = type_person,
            rfc = args['rfc'],
            nombre = args['nombre'],
            slug = utils.to_slug(args['nombre']),
            es_proveedor = True,
        )
        try:
            if id:
                obj = Socios.get(Socios.id==id)
            else:
                obj = Socios.create(**fields)
        except IntegrityError as e:
            msg = 'Ocurrio un error, al dar de alta el emisor'

        return obj, msg

    def _get_or_create_product(self, args, cant):
        # ~ almacen = ForeignKeyField(Almacenes, null=True)
        id = int(args.get('id_product', '0'))
        if id:
            obj = Productos.get(Productos.id==id)
        else:
            category = Categorias._get_by_name('Productos')
            next_key = Productos.next_key()['value']
            unit = SATUnidades.get_by_name(args['unit'])
            fields = dict(
                categoria = category,
                clave = next_key,
                clave_sat = args['key_sat'],
                descripcion = args['description'],
                unidad = unit,
                valor_unitario = Decimal(args['unit_value']),
                inventario = True,
            )
            obj = Productos.create(**fields)
            where = (SATImpuestos.activo==True & SATImpuestos.default==True)
            taxes = SATImpuestos.select().where(where)
            obj.impuestos = taxes
            obj.save()

        res = (Productos
            .update(existencia=Productos.existencia + cant)
            .where(Productos.id==obj.id).execute()
            )

        return obj

    def _add_entries(self, products, partner):
        msg = ''

        for p in products:
            cant = float(p['cant'])
            product = self._get_or_create_product(self, p, cant)
            values = dict(
                product = product,
                cant = cant,
            )
            InventoryEntries.create(**values)
            values = dict(
                partner = partner,
                key = p['key'],
                product = product,
            )
            PartnerProducts.get_or_create(**values)

        return msg

    def _create(self, args, user):
        # ~ print('ARGS', args)
        if not user.es_admin:
            msg = 'Solo un administrador puede usar esta función.'
            result = {'ok': False, 'msg': msg}
            return result

        partner, error = self._get_or_create_partner(self, args['partner'])
        if error:
            result = {'ok': False, 'msg': error}
            return result

        # ~ ToDo - Save invoice

        error = self._add_entries(self, args['products'], partner)
        if error:
            result = {'ok': False, 'msg': error}
            return result

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

    def _create_manual(self, args, user):
        if not user.es_admin:
            msg = 'Solo un administrador puede usar esta función.'
            result = {'ok': False, 'msg': msg}
            return result

        product = Productos.get(Productos.id==args['product_id'])
        if not product.inventario:
            msg = 'Este producto no lleva inventario.'
            result = {'ok': False, 'msg': msg}
            return result

        storage = None
        if args['warehouse']:
            storage = args['warehouse']
        cant = Decimal(args['cant'])
        values = dict(
            storage = storage,
            product = product,
            cant = cant,
        )
        obj = InventoryEntries.create(**values)
        msg = 'a {}'.format(obj.id)
        _save_log(user.usuario, msg, 'IE')

        WareHouseProduct.update_exists(values)

        new_cant = product.existencia + cant
        q = (Productos
            .update(existencia=Productos.existencia + cant)
            .where(Productos.id==product.id)
        )
        q.execute()

        result = {'ok': True, 'row': {'existencia': new_cant}}
        return result

    def _warehouse_movement(self, args, user):
        if not user.es_admin:
            msg = 'Solo un administrador puede usar esta función.'
            result = {'ok': False, 'msg': msg}
            return result

        product = Productos.get(Productos.id==args['id_product'])
        if not product.inventario:
            msg = 'Este producto no lleva inventario.'
            result = {'ok': False, 'msg': msg}
            return result

        storage = args['target']
        cant = Decimal(args['cant'])
        values = dict(
            storage = storage,
            product = product,
            cant = cant,
        )
        obj = InventoryEntries.create(**values)
        msg = 'a {}'.format(obj.id)
        _save_log(user.usuario, msg, 'IE')

        WareHouseProduct.update_exists(values)

        values['storage'] = args['source']
        values['cant'] *= -1
        obj = InventoryEntries.create(**values)
        msg = 'a {}'.format(obj.id)
        _save_log(user.usuario, msg, 'IE')

        WareHouseProduct.update_exists(values)

        result = {'ok': True}
        return result


    @classmethod
    def post(cls, values, user=None):
        opt = values['opt']
        args = utils.loads(values['values'])
        return getattr(cls, f'_{opt}')(cls, args, user)


@util.run_in_thread
def _save_log(user, action, table):
    data = {'usuario': user, 'accion': action, 'tabla': table}
    Registro.add(data)
    return


@util.run_in_thread
def _send_notify_access(args):
    admins = (Usuarios
        .select(Usuarios.correo)
        .where(Usuarios.es_admin==True)
        .scalar(as_tuple=True))

    if not admins:
        return

    config = Configuracion.get_({'fields': 'correo'})
    contra = Configuracion.get_('correo_contra')
    if not config:
        return

    user = args['usuario']
    rfc = args['rfc']
    ip = args['ip']

    url = f"http://ip-api.com/line/{ip}?fields=city"
    city = utils.get_url(url)
    message = f"Desde la IP: {ip} en: {city}"

    server = {
        'server': config['correo_servidor'],
        'port': config['correo_puerto'],
        'ssl': utils.to_bool(config['correo_ssl']),
        'starttls': utils.to_bool(config['correo_starttls']),
        'user': config['correo_usuario'],
        'pass': utils.decrypt(contra, rfc),
    }
    mail = {
        'to': ','.join(admins),
        'subject': f"Usuario {user} identificado",
        'message': message,
    }
    data= {
        'server': server,
        'mail': mail,
    }
    result = utils.send_mail(data)

    return


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

    conectar(values)
    try:
        obj = Usuarios.get(usuario=args['usuario'], es_activo=True)
    except Usuarios.DoesNotExist:
        return respuesta, None
    except ProgrammingError as e:
        log.error(e)
        return respuesta, None

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

    obj.ultimo_ingreso = util.now()
    obj.save()
    respuesta['msg'] = ''
    respuesta['login'] = True
    respuesta['user'] = str(obj)
    respuesta['super'] = obj.es_superusuario

    notify_access = Configuracion.get_bool('chk_users_notify_access')
    if notify_access:
       _send_notify_access(args)

    return respuesta, obj


def get_cp(cp):
    con = sqlite3.connect(PATH_CP)
    cursor = con.cursor()
    sql = """SELECT estado, municipio, clave, colonia, key_estado
        FROM codigos, colonias, municipios, estados
        WHERE codigos.id_colonia=colonias.id
            AND codigos.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][0],
            'municipio': rows[0][1],
            'key_municipio': rows[0][2],
            'key_estado': rows[0][4],
        }
        if len(rows) == 1:
            data['colonia'] = rows[0][3]
        else:
            data['colonia'] = [r[3] for r in rows]

    return data


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


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


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


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


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


def get_title_app(by=1):
    html = {
        1: '<font color="#610B0B">{}</font>',
        2: '<font color="#610B0B">Bienvenido a {}</font>',
        3: '<font color="#000000">{}</font>',
    }
    # ~ return html[by].format(TITLE_APP)
    return html[by].format(f'Empresa Libre v{VERSION_EMPRESA_LIBRE}')


def test_correo(values):
    server = {
        'servidor': values['correo_servidor'],
        'puerto': values['correo_puerto'],
        'ssl': util.get_bool(values['correo_ssl']),
        'starttls': util.get_bool(values['correo_starttls']),
        'usuario': values['correo_usuario'],
        'contra': values['correo_contra'],
    }
    ccp = values.get('correo_copia', '')
    options = {
        'para': values['correo_usuario'],
        'copia': ccp,
        'confirmar': util.get_bool(values['correo_confirmacion']),
        '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': RFCS['PUBLIC']},
        {'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.upper())

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


def _crear_tablas(rfc):
    tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion,
        Folios, Registro, CamposPersonalizados, Permisos,
        Emisor, Facturas, FacturasDetalle, FacturasImpuestos, FacturasPagos,
        FacturasRelacionadas, FacturasComplementos, FacturasPersonalizados,
        SeriesProductos, Almacenes, Productos, RangosPrecios, Sucursales,
        PreFacturas, PreFacturasDetalle, PreFacturasImpuestos,
        PreFacturasRelacionadas, Tickets, TicketsDetalle, TicketsImpuestos,
        SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes,
        SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos,
        SATNivelesEducativos, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago,
        SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras,
        SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago,
        SATTipoPercepcion, SATTipoRegimen, SATLeyendasFiscales,
        Socios, SociosCuentasBanco, Contactos, ContactoCorreos,
        ContactoDirecciones, Empleados, ContactoTelefonos, Departamentos,
        Puestos,
        Tags, Roles, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco,
        TipoCorreo, TipoDireccion, TipoPariente, TipoResponsable, TipoTelefono,
        TipoTitulo, TipoMovimientoAlumno, TipoMovimientoAlmacen,
        CfdiPagos, NivelesEducativos, Alumnos, AlumnosParientes, Grupos,
        ParienteDirecciones, ParienteTelefonos, ParienteCorreos,
        CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle,
        CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion,
        CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones,
        CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos,
        CfdiNominaTotales,
        Emisor.regimenes.get_through_model(),
        Socios.tags.get_through_model(),
        Productos.impuestos.get_through_model(),
        Productos.tags.get_through_model(),
        PartnerProducts,
        InventoryEntries,
        PartnerInvoices,
        WareHouseProduct,
        SATUnidadesPeso,
        SociosRegimenes,
    ]
    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(rfc=''):
    from playhouse.migrate import PostgresqlMigrator, migrate

    if rfc:
        rfc = rfc.strip().upper()
    else:
        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)

    tablas = [Sucursales, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago,
        SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras,
        SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago,
        SATTipoPercepcion, SATTipoRegimen, Departamentos, Puestos, Empleados,
        CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle,
        CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion,
        CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones,
        CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos,
        CfdiNominaTotales, SATNivelesEducativos, Roles, Permisos,
        SociosCuentasBanco, SATLeyendasFiscales,
        PartnerProducts,
        InventoryEntries,
        PartnerInvoices,
        WareHouseProduct,
        SATUnidadesPeso,
        SociosRegimenes,
    ]
    log.info('Creando tablas nuevas...')
    database_proxy.create_tables(tablas, True)
    log.info('Tablas creadas correctamente...')

    log.info('Iniciando migración de tablas...')
    migrations = []
    migrator = PostgresqlMigrator(database_proxy)

    columns = [c.name for c in database_proxy.get_columns('usuarios')]
    if not 'sucursal_id' in columns:
        sucursal = ForeignKeyField(Sucursales, null=True, to_field=Sucursales.id)
        migrations.append(
            migrator.add_column('usuarios', 'sucursal_id', sucursal))
    if not 'rol_id' in columns:
        rol = ForeignKeyField(Roles, null=True, to_field=Roles.id)
        migrations.append(
            migrator.add_column('usuarios', 'rol_id', rol))

    columns = [c.name for c in database_proxy.get_columns('emisor')]
    if not 'registro_patronal' in columns:
        registro_patronal = TextField(default='')
        migrations.append(
            migrator.add_column(
                'emisor', 'registro_patronal', registro_patronal))
    if not 'curp' in columns:
        curp = TextField(default='')
        migrations.append(migrator.add_column('emisor', 'curp', curp))
    if not 'hours' in columns:
        hours = IntegerField(default=0)
        migrations.append(migrator.add_column('emisor', 'hours', hours))

    columns = [c.name for c in database_proxy.get_columns('socios')]
    if not 'id_fiscal' in columns:
        id_fiscal = TextField(default='')
        migrations.append(
            migrator.add_column('socios', 'id_fiscal', id_fiscal))
    if not 'plantilla' in columns:
        plantilla = TextField(default='')
        migrations.append(
            migrator.add_column('socios', 'plantilla', plantilla))
    if not 'correo_facturasp' in columns:
        correo_facturasp = TextField(default='')
        migrations.append(
            migrator.add_column('socios', 'correo_facturasp', correo_facturasp))
    if not 'regimen_fiscal' in columns:
        regimen_fiscal = TextField(default='')
        migrations.append(
            migrator.add_column('socios', 'regimen_fiscal', regimen_fiscal))

    columns = [c.name for c in database_proxy.get_columns('folios')]
    if not 'plantilla' in columns:
        plantilla = TextField(default='')
        migrations.append(
            migrator.add_column('folios', 'plantilla', plantilla))

    columns = [c.name for c in database_proxy.get_columns('prefacturasdetalle')]
    if not 'unidad' in columns:
        unidad = TextField(default='')
        migrations.append(
            migrator.add_column('prefacturasdetalle', 'unidad', unidad))

    columns = [c.name for c in database_proxy.get_columns('facturaspagos')]
    if 'cancelado' in columns:
        migrations.append(migrator.drop_column('facturaspagos', 'cancelado'))
    if not 'tipo_cambio' in columns:
        tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6,
            auto_round=True)
        migrations.append(migrator.add_column('facturaspagos', 'tipo_cambio', tipo_cambio))

    columns = [c.name for c in database_proxy.get_columns('certificado')]
    if not 'es_fiel' in columns:
        es_fiel = BooleanField(default=False)
        migrations.append(migrator.add_column('certificado', 'es_fiel', es_fiel))

    columns = [c.name for c in database_proxy.get_columns('cfdipagos')]
    if not 'serie' in columns:
        socio = ForeignKeyField(Socios, null=True, to_field=Socios.id)
        serie = TextField(default='')
        folio = IntegerField(default=0)
        lugar_expedicion = TextField(default='')
        regimen_fiscal = TextField(default='')
        tipo_comprobante = TextField(default=DEFAULT_CFDIPAY['TYPE'])
        error = TextField(default='')
        tipo_relacion = TextField(default='')
        uuid_relacionado = UUIDField(null=True)
        cancelada = BooleanField(default=False)
        migrations.append(migrator.add_column('cfdipagos', 'serie', serie))
        migrations.append(migrator.add_column('cfdipagos', 'folio', folio))
        migrations.append(migrator.add_column('cfdipagos', 'lugar_expedicion', lugar_expedicion))
        migrations.append(migrator.add_column('cfdipagos', 'regimen_fiscal', regimen_fiscal))
        migrations.append(migrator.add_column('cfdipagos', 'tipo_comprobante', tipo_comprobante))
        migrations.append(migrator.add_column('cfdipagos', 'error', error))
        migrations.append(migrator.add_column('cfdipagos', 'tipo_relacion', tipo_relacion))
        migrations.append(migrator.add_column('cfdipagos', 'uuid_relacionado', uuid_relacionado))
        migrations.append(migrator.add_column('cfdipagos', 'socio_id', socio))
        migrations.append(migrator.drop_column('cfdipagos', 'cancelado'))
        migrations.append(migrator.add_column('cfdipagos', 'cancelada', cancelada))
    if not 'receptor_regimen' in columns:
        receptor_regimen = TextField(default='')
        migrations.append(migrator.add_column('cfdipagos', 'receptor_regimen', receptor_regimen))

    if not 'fecha_cancelacion' in columns:
        fecha_cancelacion = DateTimeField(null=True)
        migrations.append(migrator.add_column('cfdipagos', 'fecha_cancelacion', fecha_cancelacion))

    table = 'movimientosbanco'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'cuenta_socio_id' in columns:
        cuenta_socio = ForeignKeyField(SociosCuentasBanco, null=True, to_field=SociosCuentasBanco.id)
        migrations.append(migrator.add_column(table, 'cuenta_socio_id', cuenta_socio))
        migrations.append(migrator.drop_column(table, 'origen_rfc'))
        migrations.append(migrator.drop_column(table, 'origen_nombre'))
        migrations.append(migrator.drop_column(table, 'origen_cuenta'))
        migrations.append(migrator.drop_column(table, 'destino_rfc'))
        migrations.append(migrator.drop_column(table, 'destino_cuenta'))

    table = 'socioscuentasbanco'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'activa' in columns:
        activa = BooleanField(default=True)
        migrations.append(migrator.add_column(table, 'activa', activa))

    table = 'productos'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'cantidad_empaque' in columns:
        cantidad_empaque = DecimalField(default=0.0, max_digits=14,
            decimal_places=4, auto_round=True)
        migrations.append(migrator.add_column(
            table, 'cantidad_empaque', cantidad_empaque))
    if not 'is_discontinued' in columns:
        is_discontinued = BooleanField(default=False)
        migrations.append(migrator.add_column(
            table, 'is_discontinued', is_discontinued))
    if not 'objeto_impuesto' in columns:
        objeto_impuesto = TextField(default='02')
        migrations.append(migrator.add_column(table, 'objeto_impuesto', objeto_impuesto))

    if 'almacen_id' in columns:
        migrations.append(migrator.drop_column(table, 'almacen_id'))

    table = 'facturasdetalle'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'empaques' in columns:
        empaques = DecimalField(default=0.0, max_digits=14,
            decimal_places=4, auto_round=True)
        migrations.append(migrator.add_column(
            table, 'empaques', empaques))

    table = 'facturas'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'divisas' in columns:
        divisas = TextField(default='')
        migrations.append(migrator.add_column(table, 'divisas', divisas))
    if not 'exportacion' in columns:
        new_field = TextField(default='01')
        migrations.append(migrator.add_column(table, 'exportacion', new_field))
    if not 'receptor_regimen' in columns:
        receptor_regimen = TextField(default='')
        migrations.append(migrator.add_column(table, 'receptor_regimen', receptor_regimen))
    if not 'periodicidad' in columns:
        periodicidad = TextField(default='')
        migrations.append(migrator.add_column(table, 'periodicidad', periodicidad))

    table = 'almacenes'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if 'nombre' in columns:
        name = TextField(unique=True, default='')
        migrations.append(migrator.drop_column(table, 'nombre'))
        migrations.append(migrator.add_column(table, 'name', name))

    table = 'sucursales'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'warehouse_id' in columns:
        warehouse = ForeignKeyField(Almacenes, null=True, to_field=Almacenes.id)
        migrations.append(migrator.add_column(table, 'warehouse_id', warehouse))

    table = 'facturasdetalle'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not 'warehouse_id' in columns:
        warehouse = ForeignKeyField(Almacenes, null=True, to_field=Almacenes.id)
        migrations.append(migrator.add_column(table, 'warehouse_id', warehouse))

    table = 'ticketsdetalle'
    field = 'warehouse_id'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not field in columns:
        warehouse = ForeignKeyField(Almacenes, null=True, to_field=Almacenes.id)
        migrations.append(migrator.add_column(table, field, warehouse))

    table = 'empleados'
    field = 'regimen_fiscal'
    columns = [c.name for c in database_proxy.get_columns(table)]
    if not field in columns:
        regimen_fiscal = TextField(default='')
        migrations.append(migrator.add_column(table, field, regimen_fiscal))

    # migrations.append(migrator.add_index('tickets', ('serie', 'folio'), True))

    if migrations:
        with database_proxy.atomic() as txn:
            migrate(*migrations)

    Configuracion.add({'version': VERSION})

    log.info('Tablas migradas correctamente...')
    # ~ _importar_valores('', rfc)

    log.info('Actualizando valores...')
    try:
        q = SATRegimenes.update(**{'activo': True}).where(SATRegimenes.key=='616')
        q.execute()
    except Exception as e:
        log.error(e)
    else:
        log.info('Valores actualizados...')

    try:
        q = SATEstados.update(**{'key': 'CMX'}).where(SATEstados.key=='DIF')
        q.execute()
    except Exception as e:
        log.error(e)
    else:
        log.info('Valores actualizados...')

    q = Configuracion.delete().where(Configuracion.clave=='chk_config_tax_decimals')
    q.execute()
    log.info('Config Tax Update...')

    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 _borrar_rfc():
    rfc = input('Introduce el RFC a borrar: ').strip().lower()
    if not rfc:
        msg = 'El RFC es requerido'
        log.error(msg)
        return

    confirm = input('¿Estás seguro de borrar el RFC? [si]')
    if confirm != 'si':
        log.info('Proceso cancelado...')
        return

    if _delete_emisor(rfc.upper()):
        utils.db_delete(rfc, PATHS['BK'])
        log.info('RFC borrado correctamente...')
        return

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


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


def get_empresas():
    data = util.get_rfcs()
    rows = []
    for row in data:
        rows.append({'delete': '-', 'rfc': row[0]})
    return tuple(rows)


def empresa_agregar(rfc, no_bd):
    rfc = rfc.upper()
    if util.get_con(rfc):
        msg = 'El RFC ya esta dado de alta'
        return {'ok': False, 'msg': msg}

    user = rfc.replace('&', '').lower()
    if not no_bd:
        if not util.crear_rol(user):
            msg = 'No se pudo crear el usuario, es probable que ya exista'
            return {'ok': False, 'msg': msg}

        if not util.crear_db(user):
            msg = 'No se pudo crear la base de datos'
            return {'ok': False, 'msg': msg}

    args = {
        "type": "postgres",
        "name": user,
        "user": user,
        "password": user,
    }
    if not no_bd:
        if not conectar(args.copy()):
            msg = 'No se pudo conectar a la base de datos'
            return {'ok': False, 'msg': msg}

    if not _add_emisor(rfc, util.dumps(args)):
        msg = 'No se pudo guardar el nuevo emisor'
        return {'ok': False, 'msg': msg}

    if not no_bd:
        if not _crear_tablas(rfc):
            msg = 'No se pudo crear las tablas'
            return {'ok': False, 'msg': msg}

    msg = 'Emisor dado de alta correctamente'
    row = {'delete': '-', 'rfc': rfc}
    return {'ok': True, 'msg': msg, 'row': row}


def empresa_borrar(rfc):
    utils.db_delete(rfc.lower(), PATHS['BK'])
    return True


def respaldar_dbs():
    if not MV:
        msg = 'Solo MV'
        return {'ok': False, 'msg': msg}

    result = utils.db_backup_local()
    return result


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:
                pass

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


def _import_from_folder(path):
    files = util.get_files(path, 'json')
    if not files:
        msg = 'No se encontraron archivos para importar'
        log.error(msg)
        return

    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 valores...')
    for p in files:
        msg = '\tImportando tabla: {}'
        data = util.import_json(p)
        log.info(msg.format(data['tabla']))
        table = globals()[data['tabla']]
        for r in data['datos']:
            try:
                with database_proxy.atomic() as txn:
                    table.create(**r)
            except IntegrityError:
                pass

    log.info('Valores importados...')
    return


def _export_documents():
    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('Exportar documentos...')

    n = util.now()
    year = input('Introduce el año [{}]: '.format(n.year)).strip()
    if not year:
        year = str(n.year)
    month = input('Introduce el mes [{}]: '.format(n.month)).strip()
    if not month:
        month = str(n.month)

    without_stamp = []
    with_error = []

    filters = {
        'year': year,
        'month': month,
    }
    result = Facturas.get_(filters)
    if result['ok']:
        t = len(result['rows'])
        for i, row in enumerate(result['rows']):
            msg = 'Extrayendo factura {} de {}: {}-{}'.format(
                i+1, t, row['serie'], row['folio'])
            log.info(msg)
            _, name = Facturas.get_xml(row['id'])
            if not row['uuid']:
                without_stamp.append(f"{row['serie']}-{row['folio']}")
            msg = '\tXML extraido...'
            log.info(msg)
            name = name[:-3] + 'pdf'
            path = '/home/mau/facturas/{}/{}/{}/{}'.format(
                rfc, year, month.zfill(2), name)
            if util.exists(path):
                continue
            try:
                Facturas.get_pdf(row['id'], rfc, True)
            except Exception as e:
                with_error.append(f"{row['serie']}-{row['folio']}")
                print(e)
            msg = '\tPDF generado...'
            log.info(msg)

    log.info('Documentos exportados...')
    log.info('Sin sellar...')
    for i in without_stamp:
        log.info(i)
    log.info('Con error...')
    for i in with_error:
        log.info(i)
    return


# ~ v2

def companies_get():
    data = utils.rfc_get()
    rows = []
    for row in data:
        rows.append({'delete': '-', 'rfc': row[0].upper()})
    return tuple(rows)


def _list_clients():
    rows = utils.rfc_get()
    for row in rows:
        msg = f'RFC: {row[0].upper()}'
        print(msg)
    return


def _new_client(rfc, no_database):
    rfc = rfc.lower()
    if not rfc:
        log.error('Falta el RFC')
        return

    if utils.rfc_exists(rfc):
        msg = 'El RFC ya esta dado de alta'
        log.error(msg)
        return {'ok': False, 'msg': msg}

    user = rfc.replace('&', '').lower()
    if not no_database:
        if not utils.db_create(user):
            msg = 'No se pudo crear la base de datos'
            log.error(msg)
            return {'ok': False, 'msg': msg}

    args = {
        'type': 'postgres',
        'name': user,
        'user': user,
        'password': user,
    }
    if not conectar(args.copy()):
        msg = 'No se pudo conectar a la base de datos'
        log.error(msg)
        return {'ok': False, 'msg': msg}

    if not utils.rfc_add(rfc, args):
        msg = 'No se pudo guardar el nuevo emisor'
        log.error(msg)
        return {'ok': False, 'msg': msg}

    if not no_database:
        if not _crear_tablas(rfc):
            msg = 'No se pudo crear las tablas'
            log.error(msg)
            return {'ok': False, 'msg': msg}

    desconectar()
    msg = 'Emisor dado de alta correctamente'
    row = {'delete': '-', 'rfc': rfc.upper()}
    result = {'ok': True, 'msg': msg, 'row': row}
    return result


def _delete_client(rfc, no_database, ask=True):
    rfc = rfc.lower()
    if not rfc:
        log.error('Falta el RFC')
        return

    if not utils.rfc_exists(rfc):
        msg = 'El RFC no esta dado de alta'
        log.error(msg)
        return {'ok': False, 'msg': msg}

    if ask:
        confirm = input('¿Estás seguro de borrar el RFC? [si]')
        if confirm != 'si':
            log.info('Proceso cancelado...')
            return False

    if utils.db_delete(rfc, PATHS['BK'], no_database):
        log.info('RFC borrado correctamente...')
        result = True
    else:
        log.error('No se pudo borrar el RFC')
        result = False

    return result


def _update_sat():
    clients = utils.rfc_get()
    tables = utils.json_loads(PATHS['SAT'])
    for rfc, con in clients:
        log.info(f'Importando datos en: {rfc.upper()}')
        conectar(utils.loads(con))
        for table in tables:
            t = globals()[table['tabla']]
            with database_proxy.atomic():
                for r in table['datos']:
                    try:
                        t.get_or_create(**r)
                    except:
                        pass
            log.info(f"\tTabla importada: {table['tabla']}")
        desconectar()
    log.info('Importación terminada...')
    return


def _new_superuser(rfc):
    rfc = rfc.lower()
    if not rfc:
        log.error('Falta el RFC')
        return

    if not utils.rfc_exists(rfc):
        msg = 'El RFC no esta dado de alta'
        log.error(msg)
        return

    args = utils.get_data_con(rfc)

    user = input('Introduce el nuevo nombre para el superusuario: ').strip()
    if not user:
        msg = 'El nombre de usuario es requerido'
        log.error(msg)
        return

    ok, password = utils.get_pass()
    if not ok:
        log.error(password)
        return

    conectar(args)
    try:
        obj = Usuarios.create(
            usuario=user, contraseña=password, es_superusuario=True)
        msg = 'SuperUsuario creado correctamente...'
        log.info(msg)
    except IntegrityError:
        msg = 'El usuario ya existe'
        log.error(msg)

    desconectar()
    return


def _change_pass(rfc):
    rfc = rfc.lower()
    if not rfc:
        log.error('Falta el RFC')
        return

    if not utils.rfc_exists(rfc):
        msg = 'El RFC no esta dado de alta'
        log.error(msg)
        return

    args = utils.get_data_con(rfc)
    conectar(args)

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

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

    ok, password = utils.get_pass()
    if not ok:
        log.error(password)
        desconectar()
        return

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

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


def _migrate_cert(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)
    log.info('Exportando certificados...')

    auth = Emisor.get_auth()
    data = {
        'lst_pac': 'finkok',
        'user_timbrado_finkok': auth['USER'],
        'token_timbrado_finkok': auth['PASS'],
    }
    Configuracion.add(data)

    obj = Certificado.get(Certificado.es_fiel==False)
    cert = utils.SATCertificate
    cert.save_cert(obj)

    desconectar()
    log.info('Proceso terminado correctamente...')
    return


def _import_clients(rfc, path):
    if not rfc:
        msg = 'El RFC es requerido'
        log.error(msg)
        return

    if not path:
        msg = 'El archivo CSV es necesario'
        log.error(msg)
        return

    args = util.get_con(rfc)
    if not args:
        return

    conectar(args)
    log.info('Importando clientes...')

    data = utils.read_csv(path)
    t = len(data)
    ok = 0
    for i, row in enumerate(data):
        msg = f'\tImportando cliente {i+1} de {t}'
        log.info(msg)
        row['rfc'] = row['rfc'].upper()
        row['nombre'] = utils.spaces(row['nombre'])
        row['slug'] = utils.to_slug(row['nombre'])

        w = ((Socios.rfc==row['rfc']) & (Socios.slug==row['slug']))
        if Socios.select().where(w).exists():
            msg = '\tYa existe el RFC y Razón Social'
            log.info(msg)
            continue

        row['es_cliente'] = True
        row['forma_pago'] = SATFormaPago.get(SATFormaPago.id==row['forma_pago'])

        try:
            obj = Socios.create(**row)
            ok += 1
        except Exception as e:
            log.error(e)
            break

    desconectar()
    msg = f'Total de clientes: {t}'
    log.info(msg)
    msg = f'Clientes nuevos: {ok}'
    log.info(msg)
    log.info('Proceso terminado correctamente...')
    return


def _import_stock(rfc, path):
    if not rfc:
        msg = 'El RFC es requerido'
        log.error(msg)
        return

    if not path:
        msg = 'El archivo CSV es necesario'
        log.error(msg)
        return

    args = util.get_con(rfc)
    if not args:
        return

    conectar(args)
    log.info('Importando existencias...')

    data = utils.read_csv(path)
    t = len(data)
    ok = 0
    for i, row in enumerate(data):
        msg = f'\tImportando existencia {i+1} de {t}'
        log.info(msg)

        cant = Decimal(row['cantidad'])
        if cant <= 0:
            continue

        storage = int(row['almacen'])

        product = Productos.get(Productos.clave==row['clave'])
        if not product.inventario:
            msg = 'Este producto no lleva inventario.'
            log.debug(msg)
            continue

        values = dict(
            storage = storage,
            product = product,
            cant = cant,
        )
        obj = InventoryEntries.create(**values)
        WareHouseProduct.update_exists(values)

        new_cant = product.existencia + cant
        q = (Productos
            .update(existencia=Productos.existencia + cant)
            .where(Productos.id==product.id)
        )
        q.execute()

    desconectar()
    msg = f'Total de productos: {t}'
    log.info(msg)
    log.info('Proceso terminado correctamente...')
    return


def _process_command_line_arguments():
    parser = argparse.ArgumentParser(
        description='Empresa Libre')
    parser.add_argument('-lc', '--list-clients', dest='list_clients',
        action='store_true', default=False, required=False)
    parser.add_argument('-nc', '--new-client', dest='new_client',
        action='store_true', default=False, required=False)
    parser.add_argument('-dc', '--delete-client', dest='delete_client',
        action='store_true', default=False, required=False)
    parser.add_argument('-ndb', '--no-database', dest='no_database',
        action='store_true', default=False, required=False)
    parser.add_argument('-m', '--migrate', dest='migrate',
        action='store_true', default=False, required=False)
    parser.add_argument('-us', '--update-sat', dest='update_sat',
        action='store_true', default=False, required=False)
    parser.add_argument('-ns', '--new-superuser', dest='new_superuser',
        action='store_true', default=False, required=False)
    parser.add_argument('-cp', '--change-pass', dest='change_pass',
        action='store_true', default=False, required=False)
    parser.add_argument('-bk', '--backup', dest='backup',
        action='store_true', default=False, required=False)
    parser.add_argument('-ed', '--export-documents', dest='export_documents',
        action='store_true', default=False, required=False)
    parser.add_argument('-ic', '--import-clients', dest='import_clients',
        action='store_true', default=False, required=False)
    parser.add_argument('-is', '--import-stock', dest='import_stock',
        action='store_true', default=False, required=False)

    parser.add_argument('-mc'   , '--migrate-cert', dest='migrate_cert',
        action='store_true', default=False, required=False)

    parser.add_argument('-r', '--rfc', dest='rfc', default='')
    parser.add_argument('-f', '--file', dest='file', default='')
    return parser.parse_args()


def main(args):
    if args.list_clients:
        _list_clients()
        return

    if args.new_client:
        _new_client(args.rfc, args.no_database)
        return

    if args.delete_client:
        _delete_client(args.rfc, args.no_database)
        return

    if args.migrate:
        _migrate_tables(args.rfc)
        return

    if args.update_sat:
        _update_sat()
        return

    if args.new_superuser:
        _new_superuser(args.rfc)
        return

    if args.change_pass:
        _change_pass(args.rfc)
        return

    if args.backup:
        utils.db_backup(IS_MV, URL['SEAFILE'])
        return

    if args.export_documents:
        _export_documents()
        return

    if args.migrate_cert:
        _migrate_cert(args.rfc)
        return

    if args.import_clients:
        _import_clients(args.rfc, args.file)
        return

    if args.import_stock:
        _import_stock(args.rfc, args.file)
        return

    return


if __name__ == '__main__':
    args = _process_command_line_arguments()
    main(args)

