#!/usr/bin/env python3

# ~ Empresa Libre
# ~ Copyright (C) 2016-2018  Mauricio Baeza Servin (web@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 base64
import datetime
import getpass
import hashlib
import io
import json
import locale
import mimetypes
import os
import re
import requests
import sqlite3
import socket
import subprocess
import tempfile
import textwrap
import threading
import time
import unicodedata
import uuid
import zipfile

from io import BytesIO
from math import trunc
from pathlib import Path
from xml.etree import ElementTree as ET
from xml.dom.minidom import parseString


try:
    import uno
    from com.sun.star.beans import PropertyValue
    from com.sun.star.awt import Size
    from com.sun.star.view.PaperFormat import LETTER
    APP_LIBO = True
except ImportError:
    APP_LIBO = False

# ~ import pyqrcode
from dateutil import parser
from lxml import etree

import mako.runtime
from mako.exceptions import TopLevelLookupException
mako.runtime.UNDEFINED = ''

from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice, \
    SeaFileAPI, PrintTicket
from settings import DEBUG, MV, log, template_lookup, COMPANIES, DB_SAT, \
    PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \
    PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES, DIR_FACTURAS

from settings import USAR_TOKEN, API, DECIMALES_TAX
# ~ from .configpac import AUTH


from .utils import get_qr
# ~ v2
import segno
from .pacs.cfdi_cert import SATCertificate

from settings import (
    CFDI_VERSIONS,
    EXT,
    MXN,
    PATHS,
    PRE_DEFAULT,
)


def _call(args):
    return subprocess.check_output(args, shell=True).decode()


def _get_md5(data):
    return hashlib.md5(data.encode()).hexdigest()


def save_temp(data, modo='wb'):
    path = tempfile.mkstemp()[1]
    with open(path, modo) as f:
        f.write(data)
    return path


def save_file(path, data, modo='wb'):
    try:
        with open(path, modo) as f:
            f.write(data)
        return True
    except:
        return False


def _join(*paths):
    return os.path.join(*paths)


def _kill(path):
    try:
        os.remove(path)
    except:
        pass
    return


def get_pass():
    password = getpass.getpass('Introduce la contraseña: ')
    pass2 = getpass.getpass('Confirma la contraseña: ')

    if password != pass2:
        msg = 'Las contraseñas son diferentes'
        return False, msg
    password = password.strip()
    if not password:
        msg = 'La contraseña es necesaria'
        return False, msg

    return True, password


def get_value(arg):
    value = input('Introduce el {}: '.format(arg)).strip()
    if not value:
        msg = 'El {} es requerido'.format(arg)
        log.error(msg)
        return ''
    return value


def _valid_db_companies():
    con = sqlite3.connect(COMPANIES)
    sql = """
        CREATE TABLE IF NOT EXISTS names(
            rfc TEXT NOT NULL COLLATE NOCASE UNIQUE,
            con TEXT NOT NULL
        );
    """
    cursor = con.cursor()
    cursor.executescript(sql)
    cursor.close()
    con.close()
    return


def _get_args(rfc):
    _valid_db_companies()
    con = sqlite3.connect(COMPANIES)
    cursor = con.cursor()
    sql = "SELECT con FROM names WHERE rfc=?"
    cursor.execute(sql, (rfc,))
    values = cursor.fetchone()
    if values is None:
        msg = 'No se encontró el RFC'
        log.error(msg)
        return ''

    cursor.close()
    con.close()
    return values[0]


def get_rfcs():
    _valid_db_companies()
    con = sqlite3.connect(COMPANIES)
    cursor = con.cursor()
    sql = "SELECT * FROM names"
    cursor.execute(sql)
    values = cursor.fetchall()
    cursor.close()
    con.close()
    return values


def get_con(rfc=''):
    if not rfc:
        rfc = get_value('RFC').upper()
        if not rfc:
            return {}

    args = _get_args(rfc.upper())
    if not args:
        return {}
    return loads(args)


def get_sat_key(table, key):
    con = sqlite3.connect(DB_SAT)
    cursor = con.cursor()
    sql = 'SELECT key, name FROM {} WHERE key=?'.format(table)
    cursor.execute(sql, (key,))
    data = cursor.fetchone()
    cursor.close()
    con.close()
    if data is None:
        return {'ok': False, 'text': 'No se encontró la clave'}
    return {'ok': True, 'text': data[1]}


def get_sat_monedas(key):
    con = sqlite3.connect(DB_SAT)
    con.row_factory = sqlite3.Row
    cursor = con.cursor()

    filtro = '%{}%'.format(key)
    sql = "SELECT * FROM monedas WHERE key LIKE ? OR name LIKE ?"

    cursor.execute(sql, [filtro, filtro])
    data = cursor.fetchall()
    cursor.close()
    con.close()
    if data is None:
        return ()

    data = [dict(r) for r in data]
    return tuple(data)


def get_sat_unidades(key):
    con = sqlite3.connect(DB_SAT)
    con.row_factory = sqlite3.Row
    cursor = con.cursor()

    filtro = '%{}%'.format(key)
    sql = "SELECT * FROM unidades WHERE key LIKE ? OR name LIKE ?"

    cursor.execute(sql, [filtro, filtro])
    data = cursor.fetchall()
    cursor.close()
    con.close()
    if data is None:
        return ()

    data = [dict(r) for r in data]
    return tuple(data)


def get_sat_unidadespeso(key):
    con = sqlite3.connect(DB_SAT)
    con.row_factory = sqlite3.Row
    cursor = con.cursor()

    filtro = '%{}%'.format(key)
    sql = "SELECT * FROM unidad_peso WHERE key LIKE ? OR name LIKE ?"

    cursor.execute(sql, [filtro, filtro])
    data = cursor.fetchall()
    cursor.close()
    con.close()
    if data is None:
        return ()

    data = tuple([dict(r) for r in data])

    return data


def get_sat_productos(key):
    con = sqlite3.connect(DB_SAT)
    con.row_factory = sqlite3.Row
    cursor = con.cursor()

    filtro = '%{}%'.format(key)
    sql = "SELECT * FROM productos WHERE key LIKE ? OR name LIKE ?"

    cursor.execute(sql, [filtro, filtro])
    data = cursor.fetchall()
    cursor.close()
    con.close()
    if data is None:
        return ()

    data = [dict(r) for r in data]
    return tuple(data)


def now():
    n = datetime.datetime.now().replace(microsecond=0)
    return n


def today():
    return datetime.date.today()


def get_token():
    return _get_hash(uuid.uuid4().hex)


def get_mimetype(path):
    mt = mimetypes.guess_type(path)[0]
    return mt or 'application/octet-stream'


def is_file(path):
    return os.path.isfile(path)


def get_stream(path):
    return get_file(path), get_size(path)


def get_file(path):
    return open(path, 'rb')


def get_files(path, ext='xml'):
    docs = []
    for folder, _, files in os.walk(path):
        pattern = re.compile('\.{}'.format(ext), re.IGNORECASE)
        docs += [os.path.join(folder, f) for f in files if pattern.search(f)]
    return tuple(docs)


def read_file(path, mode='rb'):
    return open(path, mode).read()


def get_size(path):
    return os.path.getsize(path)


def get_template(name, data={}):
    # ~ print ('NAME', name)
    template = template_lookup.get_template(name)
    return template.render(**data)


def get_custom_styles(name, default='plantilla_factura.json'):
    path = _join(PATH_MEDIA, 'templates', name.lower())
    if is_file(path):
        with open(path) as fh:
            return loads(fh.read())

    path = _join(PATH_TEMPLATES, default)
    if is_file(path):
        with open(path) as fh:
            return loads(fh.read())

    return {}


def get_template_ods(name, default='plantilla_factura.ods'):
    path = _join(PATH_MEDIA, 'templates', name.lower())
    if is_file(path):
        return path

    if 'pagos' in name:
        default='plantilla_pagos.ods'

    path = _join(PATH_TEMPLATES, default)
    if is_file(path):
        return path

    return ''


def dumps(data):
    return json.dumps(data, default=str)


def loads(data):
    return json.loads(data)


def import_json(path):
    return loads(read_file(path, 'r'))


def clean(values):
    for k, v in values.items():
        if isinstance(v, str):
            values[k] = v.strip()
    return values


def parse_con(values):
    data = values.split('|')
    try:
        con = {'type': data[0]}
        if con['type'] == 'sqlite':
            con['name'] = data[1]
        else:
            if data[1]:
                con['host'] = data[1]
            if data[2]:
                con['port'] = data[2]
            con['name'] = data[3]
            con['user'] = data[4]
            con['password'] = data[5]
        return con
    except IndexError:
        return {}


def spaces(value):
    return '\n'.join([' '.join(l.split()) for l in value.split('\n')])


def to_slug(string):
    value = (unicodedata.normalize('NFKD', string)
        .encode('ascii', 'ignore')
        .decode('ascii').lower())
    return value.replace(' ', '_')


def timbra_xml(xml, auth):
    from .pac import Finkok as PAC

    if not DEBUG and not auth:
        msg = 'Sin datos para timbrar'
        result = {'ok': False, 'error': msg}
        return result

    result = {'ok': True, 'error': ''}
    pac = PAC(auth)
    new_xml = pac.timbra_xml(xml)
    if not new_xml:
        result['ok'] = False
        result['error'] = pac.error
        if pac.error.startswith('413'):
            return _ecodex_timbra_xml(xml)
        else:
            return result

    result['xml'] = new_xml
    result['uuid'] = pac.uuid
    result['fecha'] = pac.fecha
    return result


def _get_uuid_fecha(xml):
    doc = parse_xml(xml)
    version = doc.attrib['Version']
    node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(
        PRE[version], PRE['TIMBRE']))
    return node.attrib['UUID'], node.attrib['FechaTimbrado']


# ~ def get_sat(xml):
    # ~ from .pac import get_status_sat
    # ~ return get_status_sat(xml)


class LIBO(object):
    HOST = 'localhost'
    PORT = '8100'
    ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format(
        HOST, PORT)
    CMD = ['soffice',
        '-env:SingleAppInstance=false',
        '-env:UserInstallation=file:///tmp/LIBO_Process8100',
        '--headless', '--norestore', '--nologo', '--accept={}'.format(ARG)]
    CELL_STYLE = {
        'EUR': 'euro',
    }

    def __init__(self):
        self._app = None
        self._start_office()
        self._init_values()

    def _init_values(self):
        self._es_pre = False
        self._ctx = None
        self._sm = None
        self._desktop = None
        self._currency = MXN
        self._total_cantidades = 0
        if self.is_running:
            ctx = uno.getComponentContext()
            service = 'com.sun.star.bridge.UnoUrlResolver'
            resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx)
            self._ctx = resolver.resolve('uno:{}'.format(self.ARG))
            self._sm = self._ctx.ServiceManager
            self._desktop = self._create_instance('com.sun.star.frame.Desktop')
        return

    def _create_instance(self, name, with_context=True):
        if with_context:
            instance = self._sm.createInstanceWithContext(name, self._ctx)
        else:
            instance = self._sm.createInstance(name)
        return instance

    @property
    def is_running(self):
        try:
            s = socket.create_connection((self.HOST, self.PORT), 5.0)
            s.close()
            return True
        except ConnectionRefusedError:
            return False

    def _start_office(self):
        if self.is_running:
            return

        for i in range(3):
            self.app = subprocess.Popen(self.CMD,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            time.sleep(5)
            if self.is_running:
                break
        return

    def _set_properties(self, properties):
        pl = []
        for k, v in properties.items():
            pv = PropertyValue()
            pv.Name = k
            pv.Value = v
            pl.append(pv)
        return tuple(pl)

    def _doc_open(self, path, options):
        options = self._set_properties(options)
        path = self._path_url(path)
        try:
            doc = self._desktop.loadComponentFromURL(path, '_blank', 0, options)
            return doc
        except:
            return None

    def _path_url(self, path):
        if path.startswith('file://'):
            return path
        return uno.systemPathToFileUrl(path)

    def close(self):
        if self.is_running:
            if not self._desktop is None:
                self._desktop.terminate()
            if not self._app is None:
                self._app.terminate()
        return

    def _read(self, path):
        try:
            return open(path, 'rb').read()
        except:
            return b''

    def _clean(self):
        self._sd.SearchRegularExpression = True
        self._sd.setSearchString("\{(\w.+)\}")
        self._search.replaceAll(self._sd)
        return

    def _cancelado(self, cancel):
        if not cancel:
            pd = self._sheet.getDrawPage()
            if pd.getCount():
                pd.remove(pd.getByIndex(0))
        return

    def _set_search(self):
        self._sheet = self._template.getSheets().getByIndex(0)
        try:
            self._search = self._sheet.getPrintAreas()[0]
        except IndexError:
            self._search = self._sheet.getRangeAddress()

        self._search = self._sheet.getCellRangeByPosition(
            self._search.StartColumn,
            self._search.StartRow,
            self._search.EndColumn,
            self._search.EndRow
        )
        self._sd = self._sheet.createSearchDescriptor()
        try:
            self._sd.SearchCaseSensitive = False
        except:
            print ('SD', self._sd)
        return

    def _next_cell(self, cell):
        col = cell.getCellAddress().Column
        row = cell.getCellAddress().Row + 1
        return self._sheet.getCellByPosition(col, row)

    def _copy_cell(self, cell):
        destino = self._next_cell(cell)
        self._sheet.copyRange(destino.getCellAddress(), cell.getRangeAddress())
        return destino

    def _set_cell(self, k='', v=None, cell=None, value=False):
        if k:
            self._sd.setSearchString(k)
            ranges = self._search.findAll(self._sd)
            if ranges:
                ranges = ranges.getRangeAddressesAsString().split(';')
                for r in ranges:
                    for c in r.split(','):
                        cell = self._sheet.getCellRangeByName(c)
                        if v is None:
                            return cell
                        if cell.getImplementationName() == 'ScCellObj':
                            pattern = re.compile(k, re.IGNORECASE)
                            nv = pattern.sub(v, cell.getString())
                            if value:
                                cell.setValue(nv)
                            else:
                                cell.setString(nv)
                return cell
        if cell:
            if cell.getImplementationName() == 'ScCellObj':
                ca = cell.getCellAddress()
                new_cell = self._sheet.getCellByPosition(ca.Column, ca.Row + 1)
                if value:
                    new_cell.setValue(v)
                else:
                    new_cell.setString(v)
                return new_cell

    def _comprobante(self, data):
        for k, v in data.items():
            if k.lower() in ('total', 'descuento', 'subtotal', 'totalgravado', 'totalexento'):
                self._set_cell('{cfdi.%s}' % k, v, value=True)
            else:
                self._set_cell('{cfdi.%s}' % k, v)
        return

    def _informacion_global(self, data):
        for k, v in data.items():
            print(k, v)
            self._set_cell('{cfdi.%s}' % k, v)
        return

    def _emisor(self, data):
        for k, v in data.items():
            self._set_cell('{emisor.%s}' % k, v)
        return

    def _receptor(self, data):
        for k, v in data.items():
            if k.lower() in ('salariobasecotapor', 'salariodiariointegrado'):
                self._set_cell('{receptor.%s}' % k, v, value=True)
            else:
                self._set_cell('{receptor.%s}' % k, v)
        return

    def _copy_row(self, cell):
        row = cell.getCellAddress().Row
        source = self._sheet.getRows().getByIndex(row)
        nc = self._next_cell(cell)
        self._sheet.copyRange(nc.getCellAddress(), source.getRangeAddress())
        return

    def _clean_rows(self, row, count):
        for i in range(count):
            source = self._sheet.getRows().getByIndex(row + i)
            source.clearContents(5)
        return

    def _copy_paste_rows(self, cell, count):
        dispatch = self._create_instance('com.sun.star.frame.DispatchHelper')

        row = cell.getCellAddress().Row
        source = self._sheet.getRows().getByIndex(row)
        self._template.getCurrentController().select(source)
        frame = self._template.getCurrentController().getFrame()
        dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ())

        target = self._sheet.getCellRangeByPosition(0, row + 1, 0, row + count)
        self._template.getCurrentController().select(target)
        dispatch.executeDispatch(frame, '.uno:Paste', '', 0, ())
        return

    def _get_style(self, cell):
        if cell is None:
            return ''

        match = re.match(r"([a-z]+)([0-9]+)", cell.CellStyle, re.I)
        if not match:
            return ''

        currency = self.CELL_STYLE.get(self._currency, 'peso')
        return '{}{}'.format(currency, match.groups()[1])

    def _conceptos(self, data, pakings):
        first = True
        col1 = []
        col2 = []
        col3 = []
        col4 = []
        col5 = []
        col6 = []
        col7 = []
        col8 = []
        count = len(data) - 1
        for i, concepto in enumerate(data):
            key = concepto.get('noidentificacion', '')
            description = concepto['descripcion']
            unidad = concepto['unidad']
            cantidad = concepto['cantidad']
            valor_unitario = concepto['valorunitario']
            importe = concepto['importe']
            descuento = concepto.get('descuento', '0.0')
            if first:
                first = False
                cell_1 = self._set_cell('{noidentificacion}', key)
                cell_2 = self._set_cell('{descripcion}', description)
                cell_3 = self._set_cell('{unidad}', unidad)
                cell_4 = self._set_cell('{cantidad}', cantidad, value=True)
                cell_5 = self._set_cell('{valorunitario}', valor_unitario, value=True)
                cell_6 = self._set_cell('{importe}', importe, value=True)
                cell_7 = self._set_cell('{descuento}', descuento, value=True)
                if pakings:
                    cell_8 = self._set_cell('{empaque}', pakings[i], value=True)
                if len(data) > 1:
                    row = cell_1.getCellAddress().Row + 1
                    self._sheet.getRows().insertByIndex(row, count)
                    self._copy_paste_rows(cell_1, count)
                row = cell_1.getCellAddress().Row
            else:
                col1.append((key,))
                col2.append((description,))
                col3.append((unidad,))
                col4.append((float(cantidad),))
                col5.append((float(valor_unitario),))
                col6.append((float(importe),))
                col7.append((float(descuento),))
                if pakings:
                    col8.append((pakings[i],))
            self._total_cantidades += float(cantidad)

        if not count:
            if not cell_5 is None:
                cell_5.CellStyle = self._get_style(cell_5)
            if not cell_6 is None:
                cell_6.CellStyle = self._get_style(cell_6)
            return

        style_5 = self._get_style(cell_5)
        style_6 = self._get_style(cell_6)
        style_7 = self._get_style(cell_7)
        style_8 = ''
        if pakings:
            style_8 = self._get_style(cell_8)

        col = cell_1.getCellAddress().Column
        target1 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        col = cell_2.getCellAddress().Column
        target2 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        col = cell_3.getCellAddress().Column
        target3 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        col = cell_4.getCellAddress().Column
        target4 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        col = cell_5.getCellAddress().Column
        target5 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        col = cell_6.getCellAddress().Column
        target6 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        target7 = None
        target8 = None
        if not cell_7 is None:
            col = cell_7.getCellAddress().Column
            target7 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)
        if pakings and cell_8:
            col = cell_8.getCellAddress().Column
            target8 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count)

        target1.setFormulaArray(tuple(col1))
        target2.setDataArray(tuple(col2))
        target3.setFormulaArray(tuple(col3))
        target4.setDataArray(tuple(col4))
        target5.setDataArray(tuple(col5))
        target6.setDataArray(tuple(col6))
        if not target7 is None:
            target7.setDataArray(tuple(col7))
        if not target8 is None:
            target8.setDataArray(tuple(col8))

        if style_5:
            cell_5.CellStyle = style_5
            target5.CellStyle = style_5
        if style_6:
            cell_6.CellStyle = style_6
            target6.CellStyle = style_6
        if style_7:
            cell_7.CellStyle = style_7
            target7.CellStyle = style_7
        if style_8:
            cell_8.CellStyle = style_8
            target8.CellStyle = style_8
        return

    def _add_totales(self, data):
        currency = data['moneda']
        value = data['total']
        cell_value = self._set_cell('{total}', value, value=True)
        if cell_value is None:
            return False

        cell_value.CellStyle = currency

        return True

    def _totales(self, data):
        cell_styles = {
            'EUR': 'euro',
        }
        currency = data['moneda']

        self._set_cell('{total_cantidades}', str(self._total_cantidades))

        if self._pagos:
            return

        cell_title = self._set_cell('{subtotal.titulo}', 'SubTotal')
        value = data['subtotal']
        cell_value = self._set_cell('{subtotal}', value, value=True)
        if not cell_value is None:
            cell_value.CellStyle = cell_styles.get(currency, 'peso')

        #~ Si encuentra el campo {total}, se asume que los totales e impuestos
        #~ están declarados de forma independiente cada uno
        if self._add_totales(data):
            return

        #~ Si no se encuentra, copia las celdas hacia abajo de
        #~ {subtotal.titulo} y {subtotal}
        #~ print (data['descuento'])
        if 'descuento' in data:
            self._copy_cell(cell_title)
            self._copy_cell(cell_value)
            cell_title = self._set_cell(v='Descuento', cell=cell_title)
            value = data['descuento']
            cell_value = self._set_cell(v=value, cell=cell_value, value=True)
            cell_value.CellStyle = currency

        for tax in data['traslados']:
            self._copy_cell(cell_title)
            self._copy_cell(cell_value)
            cell_title = self._set_cell(v=tax[0], cell=cell_title)
            cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True)
            cell_value.CellStyle = currency

        for tax in data['retenciones']:
            self._copy_cell(cell_title)
            self._copy_cell(cell_value)
            cell_title = self._set_cell(v=tax[0], cell=cell_title)
            cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True)
            cell_value.CellStyle = currency

        for tax in data['taxlocales']:
            self._copy_cell(cell_title)
            self._copy_cell(cell_value)
            cell_title = self._set_cell(v=tax[0], cell=cell_title)
            cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True)
            cell_value.CellStyle = currency

        self._copy_cell(cell_title)
        self._copy_cell(cell_value)
        cell_title = self._set_cell(v='Total', cell=cell_title)
        value = data['total']
        cell_value = self._set_cell(v=value, cell=cell_value, value=True)
        cell_value.CellStyle = currency
        return

    def _timbre(self, data):
        if self._es_pre or self._is_ticket:
            return

        qr = data.pop('cbb')
        for k, v in data.items():
            self._set_cell('{timbre.%s}' % k, v)
        pd = self._sheet.getDrawPage()
        image = self._template.createInstance('com.sun.star.drawing.GraphicObjectShape')
        gp = self._create_instance('com.sun.star.graphic.GraphicProvider')
        pd.add(image)

        instance = 'com.sun.star.io.SequenceInputStream'
        stream = self._create_instance(instance)
        stream.initialize((uno.ByteSequence(qr.getvalue()),))
        properties = self._set_properties({'InputStream': stream})
        image.Graphic = gp.queryGraphic(properties)

        s = Size()
        s.Width = 4000
        s.Height = 4000
        image.setSize(s)
        image.Anchor = self._set_cell('{timbre.cbb}')

        return

    def _donataria(self, data):
        if not data:
            return

        for k, v in data.items():
            self._set_cell('{donataria.%s}' % k, v)
        return

    def _ine(self, data):
        if not data:
            return

        for k, v in data.items():
            self._set_cell('{ine.%s}' % k, v)
        return

    def _divisas(self, data):
        if data:
            for k, v in data.items():
                self._set_cell(f'{{divisas.{k}}}', v)
        return

    def _leyendas(self, data):
        if not data:
            return

        first = True
        for row in data:
            leyenda = row['textoLeyenda']
            norma = row.get('norma', '')
            disposicion = row.get('disposicionFiscal', '')
            if first:
                first = False
                cell1 = self._set_cell('{textoLeyenda}', leyenda)
                cell2 = self._set_cell('{norma}', norma)
                cell3 = self._set_cell('{disposicionFiscal}', disposicion)
            else:
                row = cell1.CellAddress.Row + 1
                self._sheet.getRows().insertByIndex(row, 1)
                cell1 = self._set_cell(v=leyenda, cell=cell1)
                cell2 = self._set_cell(v=norma, cell=cell2)
                cell3 = self._set_cell(v=disposicion, cell=cell3)
        return

    def _carta_porte(self, data):
        if not data:
            return

        # ~ print(data)
        figuras = data.pop('figuras')
        mercancias = data.pop('mercancias')
        detalle = mercancias.pop('detalle')
        mercancias = mercancias.pop('mercancias')
        autotransporte = data.pop('autotransporte')
        ubicaciones = data.pop('ubicaciones')

        for k, v in data.items():
            self._set_cell(f'{{cp.{k}}}', v)
        for k, v in figuras.items():
            self._set_cell(f'{{cp.{k}}}', v)
        for k, v in autotransporte.items():
            self._set_cell(f'{{cp.{k}}}', v)
        for k, v in mercancias.items():
            self._set_cell(f'{{cp.{k}}}', v)

        first = True
        count = len(ubicaciones) - 1
        for i, ubicacion in enumerate(ubicaciones):
            tipo = ubicacion['TipoUbicacion']
            nombre = ubicacion['NombreRemitenteDestinatario']
            rfc = ubicacion['RFCRemitenteDestinatario']
            nombre_rfc = f"{nombre} ({rfc})"
            fecha = ubicacion['FechaHoraSalidaLlegada']
            domicilio = ubicacion['domicilio']
            if first:
                first = False
                cell_1 = self._set_cell('{cp.TipoUbicacion}', tipo)
                cell_2 = self._set_cell('{cp.NombreRemitenteDestinatario}', nombre)
                cell_3 = self._set_cell('{cp.RFCRemitenteDestinatario}', rfc)
                cell_4 = self._set_cell('{cp.FechaHoraSalidaLlegada}', fecha)
                cell_5 = self._set_cell('{cp.Domicilio}', domicilio)

                row = cell_1.CellAddress.Row + 1
                self._sheet.getRows().insertByIndex(row, count)
                self._copy_paste_rows(cell_1, count)
            else:
                cell_1 = self._set_cell(v=tipo, cell=cell_1)
                cell_2 = self._set_cell(v=nombre, cell=cell_2)
                cell_3 = self._set_cell(v=rfc, cell=cell_3)
                cell_4 = self._set_cell(v=fecha, cell=cell_4)
                cell_5 = self._set_cell(v=domicilio, cell=cell_5)

        first = True
        count = len(detalle) - 1
        for i, mercancia in enumerate(detalle):
            clave = mercancia['BienesTransp']
            descripcion = mercancia['Descripcion']
            unidad = mercancia['ClaveUnidad']
            cantidad = mercancia['Cantidad']
            peso = mercancia['PesoEnKg']
            if first:
                first = False
                cell_1 = self._set_cell('{cp.BienesTransp}', clave)
                cell_2 = self._set_cell('{cp.Descripcion}', descripcion)
                cell_3 = self._set_cell('{cp.ClaveUnidad}', unidad)
                cell_4 = self._set_cell('{cp.Cantidad}', cantidad)
                cell_5 = self._set_cell('{cp.PesoEnKg}', peso)
                if count > 0:
                    row = cell_1.CellAddress.Row + 1
                    self._sheet.getRows().insertByIndex(row, count)
                    self._copy_paste_rows(cell_1, count)
            else:
                cell_1 = self._set_cell(v=clave, cell=cell_1)
                cell_2 = self._set_cell(v=descripcion, cell=cell_2)
                cell_3 = self._set_cell(v=unidad, cell=cell_3)
                cell_4 = self._set_cell(v=cantidad, cell=cell_4)
                cell_5 = self._set_cell(v=peso, cell=cell_5)

        return

    def _comercio_exterior(self, data):
        if not data:
            return

        emisor = data.pop('emisor')
        receptor = data.pop('receptor')
        mercancias = data.pop('mercancias')

        for k, v in data.items():
            self._set_cell(f'{{cce.{k}}}', v)
        for k, v in emisor.items():
            self._set_cell(f'{{cce.emisor.{k}}}', v)
        for k, v in receptor.items():
            self._set_cell(f'{{cce.receptor.{k}}}', v)

        first = True
        count = len(mercancias) - 1
        for i, mercancia in enumerate(mercancias):
            no_identificacion = mercancia['NoIdentificacion']
            fraccion = mercancia['FraccionArancelaria']
            unidad = mercancia['UnidadAduana']
            cantidad = mercancia['CantidadAduana']
            valor_unitario = mercancia['ValorUnitarioAduana']
            valor_dolares = mercancia['ValorDolares']
            if first:
                first = False
                cell_1 = self._set_cell('{cce.mercancia.noidentificacion}', no_identificacion)
                cell_2 = self._set_cell('{cce.mercancia.fraccionarancelaria}', fraccion)
                cell_3 = self._set_cell('{cce.mercancia.unidadaduana}', unidad)
                cell_4 = self._set_cell('{cce.mercancia.cantidadaduana}', cantidad)
                cell_5 = self._set_cell('{cce.mercancia.valorunitarioaduana}', valor_unitario)
                cell_6 = self._set_cell('{cce.mercancia.valordolares}', valor_dolares)
                if count > 0:
                    row = cell_1.CellAddress.Row + 1
                    self._sheet.getRows().insertByIndex(row, count)
                    self._copy_paste_rows(cell_1, count)
            else:
                cell_1 = self._set_cell(v=no_identificacion, cell=cell_1)
                cell_2 = self._set_cell(v=fraccion, cell=cell_2)
                cell_3 = self._set_cell(v=unidad, cell=cell_3)
                cell_4 = self._set_cell(v=cantidad, cell=cell_4)
                cell_5 = self._set_cell(v=valor_unitario, cell=cell_5)
                cell_6 = self._set_cell(v=valor_dolares, cell=cell_6)
        return

    def _nomina(self, data):
        if not data:
            return

        percepciones = data.pop('percepciones', [])
        deducciones = data.pop('deducciones', [])
        otrospagos = data.pop('otrospagos', [])
        incapacidades = data.pop('incapacidades', [])

        for k, v in data.items():
            if k.lower() in ('totalpercepciones', 'totaldeducciones',
                'totalotrospagos', 'subsidiocausado'):
                self._set_cell('{nomina.%s}' % k, v, value=True)
            else:
                self._set_cell('{nomina.%s}' % k, v)

        count = len(percepciones)
        if len(deducciones) > count:
            count = len(deducciones)
        count -= 1

        first = True
        separacion = {}
        for r in percepciones:
            if 'TotalPagado' in r:
                separacion = r
                continue

            tipo = r.get('TipoPercepcion')
            concepto = r.get('Concepto')
            gravado = r.get('ImporteGravado')
            exento = r.get('ImporteExento')
            if first:
                first = False
                cell_1 = self._set_cell('{percepcion.TipoPercepcion}', tipo)
                cell_2 = self._set_cell('{percepcion.Concepto}', concepto)
                cell_3 = self._set_cell('{percepcion.ImporteGravado}', gravado, value=True)
                cell_4 = self._set_cell('{percepcion.ImporteExento}', exento, value=True)
                if count:
                    row = cell_1.getCellAddress().Row + 1
                    self._sheet.getRows().insertByIndex(row, count)
                    self._copy_paste_rows(cell_1, count)
                    self._clean_rows(row, count)
            else:
                cell_1 = self._set_cell(v=tipo, cell=cell_1)
                cell_2 = self._set_cell(v=concepto, cell=cell_2)
                cell_3 = self._set_cell(v=gravado, cell=cell_3, value=True)
                cell_4 = self._set_cell(v=exento, cell=cell_4, value=True)

        first = True
        for r in deducciones:
            tipo = r.get('TipoDeduccion')
            concepto = r.get('Concepto')
            importe = r.get('Importe')
            if first:
                first = False
                cell_1 = self._set_cell('{deduccion.TipoDeduccion}', tipo)
                cell_2 = self._set_cell('{deduccion.Concepto}', concepto)
                cell_3 = self._set_cell('{deduccion.Importe}', importe, value=True)
            else:
                cell_1 = self._set_cell(v=tipo, cell=cell_1)
                cell_2 = self._set_cell(v=concepto, cell=cell_2)
                cell_3 = self._set_cell(v=importe, cell=cell_3, value=True)

        count = len(otrospagos) - 1
        first = True
        for r in otrospagos:
            tipo = r.get('TipoOtroPago')
            concepto = r.get('Concepto')
            importe = r.get('Importe')
            if first:
                first = False
                cell_1 = self._set_cell('{otropago.TipoOtroPago}', tipo)
                cell_2 = self._set_cell('{otropago.Concepto}', concepto)
                cell_3 = self._set_cell('{otropago.Importe}', importe, value=True)
                if count:
                    row = cell_1.getCellAddress().Row + 1
                    self._sheet.getRows().insertByIndex(row, count)
                    self._copy_paste_rows(cell_1, count)
                    self._clean_rows(row, count)
            else:
                cell_1 = self._set_cell(v=tipo, cell=cell_1)
                cell_2 = self._set_cell(v=concepto, cell=cell_2)
                cell_3 = self._set_cell(v=importe, cell=cell_3, value=True)

        count = len(incapacidades) - 1
        first = True
        for r in incapacidades:
            tipo = r.get('TipoIncapacidad')
            days = r.get('DiasIncapacidad')
            importe = r.get('ImporteMonetario')
            if first:
                first = False
                cell_1 = self._set_cell('{incapacidad.TipoIncapacidad}', tipo)
                cell_2 = self._set_cell('{incapacidad.DiasIncapacidad}', days)
                cell_3 = self._set_cell('{incapacidad.ImporteMonetario}', importe, value=True)
                # ~ if count:
                    # ~ row = cell_1.getCellAddress().Row + 1
                    # ~ self._sheet.getRows().insertByIndex(row, count)
                    # ~ self._copy_paste_rows(cell_1, count)
                    # ~ self._clean_rows(row, count)
            # ~ else:
                # ~ cell_1 = self._set_cell(v=tipo, cell=cell_1)
                # ~ cell_2 = self._set_cell(v=concepto, cell=cell_2)
                # ~ cell_3 = self._set_cell(v=importe, cell=cell_3, value=True)

        return

    def _cfdipays(self, data):
        VERSION2 = '2.0'
        version = data['Version']

        related = data.pop('related', [])
        for k, v in data.items():
            if k.lower() in ('monto',):
                self._set_cell('{pago.%s}' % k, v, value=True)
            else:
                self._set_cell('{pago.%s}' % k, v)

        col1 = []
        col2 = []
        col3 = []
        col4 = []
        col5 = []
        col6 = []
        col7 = []
        col8 = []
        col9 = []
        count = len(related)
        for i, doc in enumerate(related):
            uuid = doc['IdDocumento'].upper()
            serie = doc.get('Serie', '')
            folio = doc['Folio']
            metodo_pago = doc['MetodoDePagoDR']
            moneda = doc['MonedaDR']
            parcialidad = doc['NumParcialidad']
            saldo_anterior = doc['ImpSaldoAnt']
            importe_pagado = doc['ImpPagado']
            saldo_insoluto = doc['ImpSaldoInsoluto']
            if i == 0:
                cell_1 = self._set_cell('{doc.uuid}', uuid)
                cell_2 = self._set_cell('{doc.serie}', serie)
                cell_3 = self._set_cell('{doc.folio}', folio)
                if version != VERSION2:
                    cell_4 = self._set_cell('{doc.metodopago}', metodo_pago)
                cell_5 = self._set_cell('{doc.moneda}', moneda)
                cell_6 = self._set_cell('{doc.parcialidad}', parcialidad)
                cell_7 = self._set_cell('{doc.saldoanterior}', saldo_anterior, value=True)
                cell_8 = self._set_cell('{doc.importepagado}', importe_pagado, value=True)
                cell_9 = self._set_cell('{doc.saldoinsoluto}', saldo_insoluto, value=True)
            else:
                col1.append((uuid,))
                col2.append((serie,))
                col3.append((folio,))
                if version != VERSION2:
                    col4.append((metodo_pago,))
                col5.append((moneda,))
                col6.append((parcialidad,))
                col7.append((float(saldo_anterior),))
                col8.append((float(importe_pagado),))
                col9.append((float(saldo_insoluto),))

        if count == 1:
            return

        count -= 1
        row1 = cell_1.getCellAddress().Row + 1
        row2 = row1 + count - 1
        self._sheet.getRows().insertByIndex(row1, count)
        self._copy_paste_rows(cell_1, count)

        # ~ style_7 = self._get_style(cell_7)
        # ~ style_8 = self._get_style(cell_8)
        # ~ style_9 = self._get_style(cell_9)

        col = cell_1.getCellAddress().Column
        target1 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_2.getCellAddress().Column
        target2 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_3.getCellAddress().Column
        target3 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        if version != VERSION2:
            col = cell_4.getCellAddress().Column
            target4 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_5.getCellAddress().Column
        target5 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_6.getCellAddress().Column
        target6 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_7.getCellAddress().Column
        target7 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_8.getCellAddress().Column
        target8 = self._sheet.getCellRangeByPosition(col, row1, col, row2)
        col = cell_9.getCellAddress().Column
        target9 = self._sheet.getCellRangeByPosition(col, row1, col, row2)

        target1.setFormulaArray(tuple(col1))
        target2.setDataArray(tuple(col2))
        target3.setFormulaArray(tuple(col3))
        if version != VERSION2:
            target4.setDataArray(tuple(col4))
        target5.setDataArray(tuple(col5))
        target6.setDataArray(tuple(col6))
        target7.setDataArray(tuple(col7))
        target8.setDataArray(tuple(col8))
        target9.setDataArray(tuple(col9))

        return

    def _render(self, data):
        self._set_search()
        self._es_pre = data.pop('es_pre', False)
        self._is_ticket = data.pop('is_ticket', False)
        self._currency = data['totales']['moneda']
        self._pagos = data.pop('pagos', False)

        pakings = data.pop('pakings', [])

        self._comprobante(data['comprobante'])
        self._informacion_global(data.get('informacion_global', {}))
        self._emisor(data['emisor'])
        self._receptor(data['receptor'])
        self._conceptos(data['conceptos'], pakings)

        if self._pagos:
            self._cfdipays(data['pays'])

        if 'nomina' in data and data['nomina']:
            self._nomina(data['nomina'])
        else:
            self._totales(data['totales'])

        self._donataria(data['donataria'])
        self._ine(data['ine'])
        self._divisas(data.get('divisas', {}))
        self._leyendas(data.get('leyendas', ''))
        self._carta_porte(data.get('carta_porte', {}))
        self._comercio_exterior(data.get('comercio_exterior', {}))

        self._timbre(data['timbre'])

        self._cancelado(data['cancelada'])
        self._others_values(data)
        self._clean()
        return

    def _others_values(self, data):
        el_version = data.get('el.version', '')
        if el_version:
            self._set_cell('{el.version}', el_version)
        return

    def pdf(self, path, data, ods=False):
        options = {'AsTemplate': True, 'Hidden': True}
        log.debug('Abrir plantilla...')
        self._template = self._doc_open(path, options)
        if self._template is None:
            return b''

        self._template.setPrinter(self._set_properties({'PaperFormat': LETTER}))
        self._render(data)

        path_ods = get_path_temp('.ods')
        self._template.storeToURL(self._path_url(path_ods), ())
        if ods:
            data = self._read(path_ods)
            _kill(path_ods)
            return data

        options = {'FilterName': 'calc_pdf_Export'}
        path_pdf = get_path_temp('.pdf')
        self._template.storeToURL(self._path_url(path_pdf), self._set_properties(options))
        try:
            self._template.close(True)
        except:
            pass
        data = self._read(path_pdf)
        _kill(path_ods)
        _kill(path_pdf)
        return data

    def _get_data(self, doc, name=0):
        try:
            sheet = doc.getSheets()[name]
            cursor = sheet.createCursorByRange(sheet['A1'])
            cursor.collapseToCurrentRegion()
        except KeyError:
            msg = 'Hoja no existe'
            return (), msg

        return cursor.getDataArray(), ''

    def products(self, path):
        options = {'AsTemplate': True, 'Hidden': True}
        doc = self._doc_open(path, options)
        if doc is None:
            return (), 'No se pudo abrir la plantilla'

        data, msg = self._get_data(doc)
        doc.close(True)

        if len(data) == 1:
            msg = 'Sin datos para importar'
            return (), msg

        fields = (
            'categoria',
            'clave',
            'clave_sat',
            'descripcion',
            'unidad',
            'valor_unitario',
            'inventario',
            'existencia',
            'codigo_barras',
            'impuestos',
        )
        rows = [dict(zip(fields, r)) for r in data[1:]]
        return rows, ''

    def employees(self, path):
        options = {'AsTemplate': True, 'Hidden': True}
        doc = self._doc_open(path, options)
        if doc is None:
            return ()

        data, msg = self._get_data(doc, 'Empleados')
        doc.close(True)

        if len(data) == 1:
            msg = 'Sin datos para importar'
            return (), msg

        fields = (
            'num_empleado',
            'rfc',
            'curp',
            'nombre',
            'paterno',
            'materno',
            'fecha_ingreso',
            'imss',
            'tipo_contrato',
            'es_sindicalizado',
            'tipo_jornada',
            'tipo_regimen',
            'departamento',
            'puesto',
            'riesgo_puesto',
            'periodicidad_pago',
            'banco',
            'cuenta_bancaria',
            'clabe',
            'salario_base',
            'salario_diario',
            'estado',
            'codigo_postal',
            'notas',
            'correo',
            'regimen_fiscal',
        )
        rows = tuple([dict(zip(fields, r)) for r in data[1:]])
        msg = 'Empleados importados correctamente'
        return rows, msg

    def _get_nomina(self, doc):
        rows, msg = self._get_data(doc, 'Nomina')
        if len(rows) == 2:
            msg = 'Sin datos para importar'
            return {}, msg

        fields = (
            'rfc',
            'tipo_nomina',
            'fecha_pago',
            'fecha_inicial_pago',
            'fecha_final_pago',
            'relacionados',
            'dias_pagados',
        )
        data = tuple([dict(zip(fields, r[1:])) for r in rows[2:]])
        return data, ''

    def _get_percepciones(self, doc, count):
        rows, msg = self._get_data(doc, 'Percepciones')
        if len(rows) == 2:
            msg = 'Sin Percepciones'
            return {}, msg

        if len(rows[0][2:]) % 2:
            msg = 'Las Percepciones deben ir en pares: Gravado y Exento'
            return {}, msg

        data = tuple([r[2:] for r in rows[:count+2]])
        return data, ''

    def _get_deducciones(self, doc, count):
        rows, msg = self._get_data(doc, 'Deducciones')
        if len(rows) == 2:
            msg = 'Sin Deducciones'
            return {}, msg

        data = tuple([r[2:] for r in rows[:count+2]])

        sheet = doc.Sheets['Deducciones']
        notes = sheet.getAnnotations()
        new_titles = {}
        for n in notes:
            col = n.getPosition().Column - 2
            if data[0][col] == '004':
                new_titles[col] = n.getString()

        return data, new_titles, ''

    def _get_otros_pagos(self, doc, count):
        rows, msg = self._get_data(doc, 'OtrosPagos')
        if len(rows) == 2:
            msg = 'Sin Otros Pagos'
            return {}, msg

        data = tuple([r[2:] for r in rows[:count+2]])

        return data, ''

    def _get_separacion(self, doc, count):
        rows, msg = self._get_data(doc, 'Separacion')
        if len(rows) == 2:
            msg = 'Sin Separacion'
            return {}, msg

        data = tuple([r[1:] for r in rows[:count+2]])
        return data, ''

    def _get_horas_extras(self, doc, count):
        rows, msg = self._get_data(doc, 'HorasExtras')
        if len(rows) == 2:
            msg = 'Sin Horas Extras'
            return {}, msg

        if len(rows[1][1:]) % 4:
            msg = 'Las Horas Extras deben ir grupos de 4 columnas'
            return {}, msg

        data = tuple([r[1:] for r in rows[:count+2]])
        return data, ''

    def _get_incapacidades(self, doc, count):
        rows, msg = self._get_data(doc, 'Incapacidades')
        if len(rows) == 2:
            msg = 'Sin Incapacidades'
            return {}, msg

        if len(rows[1][1:]) % 3:
            msg = 'Las Incapacidades deben ir grupos de 3 columnas'
            return {}, msg

        data = tuple([r[1:] for r in rows[:count+2]])
        return data, ''

    def nomina(self, path):
        options = {'AsTemplate': True, 'Hidden': True}
        doc = self._doc_open(path, options)
        if doc is None:
            msg = 'No se pudo abrir la plantilla'
            return {}, msg

        data = {}
        nomina, msg = self._get_nomina(doc)
        if msg:
            doc.close(True)
            return {}, msg

        percepciones, msg = self._get_percepciones(doc, len(nomina))
        if msg:
            doc.close(True)
            return {}, msg

        deducciones, new_titles, msg = self._get_deducciones(doc, len(nomina))
        if msg:
            doc.close(True)
            return {}, msg

        otros_pagos, msg = self._get_otros_pagos(doc, len(nomina))
        if msg:
            doc.close(True)
            return {}, msg

        separacion, msg = self._get_separacion(doc, len(nomina))
        if msg:
            doc.close(True)
            return {}, msg

        horas_extras, msg = self._get_horas_extras(doc, len(nomina))
        if msg:
            doc.close(True)
            return {}, msg

        incapacidades, msg = self._get_incapacidades(doc, len(nomina))
        if msg:
            doc.close(True)
            return {}, msg

        doc.close(True)

        rows = len(nomina) + 2

        if rows != len(percepciones):
            msg = 'Cantidad de filas incorrecta en: Percepciones'
            return {}, msg
        if rows != len(deducciones):
            msg = 'Cantidad de filas incorrecta en: Deducciones'
            return {}, msg
        if rows != len(otros_pagos):
            msg = 'Cantidad de filas incorrecta en: Otros Pagos'
            return {}, msg
        if rows != len(separacion):
            msg = 'Cantidad de filas incorrecta en: Separación'
            return {}, msg
        if rows != len(horas_extras):
            msg = 'Cantidad de filas incorrecta en: Horas Extras'
            return {}, msg
        if rows != len(incapacidades):
            msg = 'Cantidad de filas incorrecta en: Incapacidades'
            return {}, msg

        data['nomina'] = nomina
        data['percepciones'] = percepciones
        data['deducciones'] = deducciones
        data['otros_pagos'] = otros_pagos
        data['separacion'] = separacion
        data['horas_extras'] = horas_extras
        data['incapacidades'] = incapacidades
        data['new_titles'] = new_titles

        return data, ''

    def invoice(self, path):
        options = {'AsTemplate': True, 'Hidden': True}
        doc = self._doc_open(path, options)
        if doc is None:
            return (), 'No se pudo abrir la plantilla'

        data, msg = self._get_data(doc)
        doc.close(True)

        if len(data) == 1:
            msg = 'Sin datos para importar'
            return (), msg

        rows = tuple(data[1:])

        return rows, ''

    def _data_to_dict(self, rows):
        data = {k: v for k, v in rows if v}
        return data

    def _current_region_to_tuple(self, cursor):
        data = []
        cursor.collapseToCurrentRegion()
        rows = cursor.getDataArray()[1:]

        if len(rows) == 1:
            return data

        keys = rows[0]
        data = [dict(zip(keys, values)) for values in rows[1:]]
        return data

    def _get_data_ce(self, doc):
        msg = ''
        data = {}
        try:
            sheet = doc.Sheets[0]
            rango = sheet['A2:B10']
            data = self._data_to_dict(rango.DataArray)
            rango = sheet['A13:B23']
            data['emisor'] = self._data_to_dict(rango.DataArray)
            rango = sheet['A26:B36']
            data['receptor'] = self._data_to_dict(rango.DataArray)
            rango = sheet['A39:B50']
            data['destinatario'] = self._data_to_dict(rango.DataArray)
            cursor = sheet.createCursorByRange(sheet['E12'])
            data['propietarios'] = self._current_region_to_tuple(cursor)
            cursor = sheet.createCursorByRange(sheet['A53'])
            data['mercancias'] = self._current_region_to_tuple(cursor)
        except Exception as e:
            msg = str(e)

        return data, msg

    def get_ce(self, path):
        options = {'AsTemplate': True, 'Hidden': True}
        doc = self._doc_open(path, options)
        if doc is None:
            return (), 'No se pudo abrir la plantilla'

        data, msg = self._get_data_ce(doc)
        doc.close(True)

        if len(data) == 1:
            msg = 'Sin datos para importar'
            return (), msg

        return data, ''


def to_pdf(data, emisor_rfc, ods=False, pdf_from='1'):
    rfc = data['emisor']['rfc']
    if DEBUG:
        rfc = emisor_rfc

    version = data['comprobante']['version']
    default = f'plantilla_factura_{version}.ods'

    if pdf_from == '2':
        return to_pdf_from_json(rfc, version, data)

    if 'nomina' in data and data['nomina']:
        version_nomina = data['nomina']['version']
        default = f'plantilla_nomina_{version}_{version_nomina}.ods'
        version = f'{version}_cn_{version_nomina}'

    if 'carta_porte' in data:
        default = 'plantilla_factura_ccp.ods'
        version = '{}_ccp_{}'.format(version, data['carta_porte']['version'])

    if data.get('pagos', False):
        version_pagos = data['pays']['version']
        default = f'plantilla_pagos_{version}_{version_pagos}.ods'
        version = f'{version}_cp_{version_pagos}'

    if data['donativo']:
        version_donatarias = data['donataria']['version']
        default = f'plantilla_donatarias_{version}_{version_donatarias}.ods'
        version = f'{version}_cd_{version_donatarias}'

    if 'comercio_exterior' in data:
        version_cce = data['comercio_exterior']['version']
        default = f'plantilla_cce_{version}_{version_cce}.ods'
        version = f'{version}_cce_{version_cce}'

    template_name = f'{rfc.lower()}_{version}.ods'
    # ~ print('\nT', template_name, default)

    if APP_LIBO:
        app = LIBO()
        if app.is_running:
            path = get_template_ods(template_name, default)
            if path:
                return app.pdf(path, data, ods)

    return to_pdf_from_json(rfc, version, data)


def to_pdf_from_json(rfc, version, data):
    rfc = rfc.lower()
    name = '{}_{}.json'.format(rfc, version)
    print('name', name)
    custom_styles = get_custom_styles(name)

    path_logo = _join(PATHS['LOGOS'], f"{rfc}.png")
    if exists(path_logo):
        data['emisor']['logo'] = path_logo

    path_logo = _join(PATHS['LOGOS'], f"{rfc}_2.png")
    if exists(path_logo):
        data['emisor']['logo2'] = path_logo

    buffer = io.BytesIO()
    pdf = TemplateInvoice(buffer)
    pdf.custom_styles = custom_styles
    pdf.data = data
    pdf.render()

    return buffer.getvalue()


def format_currency(value, currency, digits=2):
    c = {
        MXN: '$',
        'USD': '$',
        'EUR': '€',
    }
    s = c.get(currency, MXN)
    return f'{s} {float(value):,.{digits}f}'


def to_html(data):
    name = f"{data['rfc']}_{data['version']}.html"
    try:
        template = template_lookup.get_template(name)
    except TopLevelLookupException:
        template = template_lookup.get_template('plantilla_factura.html')
        data['rfc'] = 'invoice'

    # ~ data['cfdi_sello'] = textwrap.fill(data['cfdi_sello'], 50)
    # ~ data['timbre_sellosat'] = textwrap.fill(data['timbre_sellosat'], 110)
    # ~ data['timbre_cadenaoriginal'] = textwrap.fill(data['timbre_cadenaoriginal'], 140)

    # ~ data['cfdi_sello'] = 'X'*100 + '<BR>' + 'X'*100 + '<BR>' + 'X'*100
    # ~ data['timbre_sellosat'] = 'X'*100 + '<BR>' + 'X'*100 + '<BR>' + 'X'*100

    return template.render(**data)


def html_to_pdf(data):
    path_pdf = '/home/mau/test.pdf'
    css = '/home/mau/projects/empresa-libre/source/static/css/invoice.css'

    # ~ font_config = FontConfiguration()
    # ~ html = HTML(string=data)
    # ~ css = CSS(filename=path_css)
    # ~ html.write_pdf(path_pdf, stylesheets=[css], font_config=font_config)
    options = {
        'page-size': 'Letter',
        'margin-top': '0.50in',
        'margin-right': '0.50in',
        'margin-bottom': '0.50in',
        'margin-left': '0.50in',
        'encoding': "UTF-8",
    }

    pdfkit.from_string(data.decode(), path_pdf, options=options, css=css)

    return


def import_employees(rfc):
    msg = 'No se pudo cargar el archivo'
    name = '{}_employees.ods'.format(rfc.lower())
    path = _join(PATH_MEDIA, 'tmp', name)
    if not is_file(path):
        return (), msg

    msg = 'LibreOffice no se pudo iniciar'
    if APP_LIBO:
        app = LIBO()
        if app.is_running:
            return app.employees(path)

    return (), msg


def import_nomina(rfc):
    name = '{}_nomina.ods'.format(rfc.lower())
    path = _join(PATH_MEDIA, 'tmp', name)
    if not is_file(path):
        return ()

    if APP_LIBO:
        app = LIBO()
        if app.is_running:
            return app.nomina(path)

    return ()


def parse_xml(xml):
    try:
        return ET.fromstring(xml)
    except ET.ParseError:
        return None


def to_pretty_xml(xml):
    tree = parseString(xml)
    return tree.toprettyxml(encoding='utf-8').decode('utf-8')


def get_dict(data):
    return CaseInsensitiveDict(data)


def to_letters(value, currency):
    return NumLet(value, currency).letras


# ~ def get_qr(data, p=True):
    # ~ qr = pyqrcode.create(data, mode='binary')
    # ~ if p:
        # ~ path = get_path_temp('.qr')
        # ~ qr.png(path, scale=7)
        # ~ return path

    # ~ buffer = io.BytesIO()
    # ~ qr.png(buffer, scale=8)
    # ~ return base64.b64encode(buffer.getvalue()).decode()


# ~ def get_qr2(data, kind='svg'):
    # ~ buffer = io.BytesIO()
    # ~ segno.make(data).save(buffer, kind=kind, scale=8, border=2)
    # ~ return buffer


def _get_relacionados(doc, version):
    node = doc.find('{}CfdiRelacionados'.format(PRE[version]))
    if node is None:
        return ''

    uuids = ['UUID: {}'.format(n.attrib['UUID']) for n in list(node)]
    return '\n'.join(uuids)


def _comprobante(doc, options):
    data = CaseInsensitiveDict(doc.attrib.copy())
    del data['certificado']

    serie = ''
    if 'serie' in data:
        serie = '{}-'.format(data['serie'])
    data['seriefolio'] = '{}{}'.format(serie, data.get('folio', ''))
    data['totalenletras'] = to_letters(float(data['total']), data['moneda'])

    is_nomina = options.get('is_nomina', False)
    if is_nomina:
        data['formadepago'] = options['formadepago']
        data['periodicidaddepago'] = options['periodicidaddepago']
        data['tiporelacion'] = options.get('tiporelacion', '')
        return data

    if data['version'] in CFDI_VERSIONS:
        tipos = {
            'I': 'ingreso',
            'E': 'egreso',
            'T': 'traslado',
            'P': 'pago',
        }
        data['tipodecomprobante'] = tipos.get(data['tipodecomprobante'])
        data['lugarexpedicion'] = \
            'C.P. de Expedición: {}'.format(data['lugarexpedicion'])
        if 'metododepago' in options:
            data['metododepago'] = options['metododepago']
        if 'formadepago' in options:
            data['formadepago'] = options['formadepago']

        if 'condicionesdepago' in data:
            data['condicionesdepago'] = \
                'Condiciones de pago: {}'.format(data['condicionesdepago'])
        data['moneda'] = options['moneda']
        data['tiporelacion'] = options.get('tiporelacion', '')
        data['relacionados'] = _get_relacionados(doc, data['version'])
    else:
        fields = {
            'formaDePago': 'Forma de Pago: {}\n',
            'metodoDePago': 'Método de pago: {}\n',
            'condicionesDePago': 'Condiciones de Pago: {}\n',
            'NumCtaPago': 'Número de Cuenta de Pago: {}\n',
            'Moneda': 'Moneda: {}\n',
            'TipoCambio': 'Tipo de Cambio: {}',
        }
        datos = ''
        for k, v in fields.items():
            if k in data:
                datos += v.format(data[k])
        data['datos'] = datos

    data['hora'] = data['fecha'].split('T')[1]
    fecha = parser.parse(data['fecha'])
    try:
        locale.setlocale(locale.LC_TIME, "es_MX.UTF-8")
    except:
        pass
    data['fechaformato'] = fecha.strftime('%A, %d de %B de %Y')

    if 'tipocambio' in data:
        data['tipocambio'] = 'Tipo de Cambio: $ {:0.4f}'.format(
            float(data['tipocambio']))
    data['notas'] = options['notas']

    return data


def _emisor(doc, version, values):
    emisor = doc.find('{}Emisor'.format(PRE[version]))
    data = CaseInsensitiveDict(emisor.attrib.copy())
    node = emisor.find('{}DomicilioFiscal'.format(PRE[version]))
    if not node is None:
        data.update(CaseInsensitiveDict(node.attrib.copy()))

    if version == '3.2':
        node = emisor.find('{}RegimenFiscal'.format(PRE[version]))
        if not node is None:
            data['regimenfiscal'] = node.attrib['Regimen']
            data['regimen'] = node.attrib['Regimen']
    else:
        data['regimenfiscal'] = values['regimenfiscal']

    path = _join(PATH_MEDIA, 'logos', '{}.png'.format(data['rfc'].lower()))
    if is_file(path):
        data['logo'] = path

    return data


def _receptor(doc, version, values):
    node = doc.find('{}Receptor'.format(PRE[version]))
    data = CaseInsensitiveDict(node.attrib.copy())
    node = node.find('{}Domicilio'.format(PRE[version]))
    if not node is None:
        data.update(node.attrib.copy())

    if version == '3.2':
        return data

    data['usocfdi'] = values['usocfdi']
    # ~ data.update(values['receptor'])
    return data


def _conceptos(doc, version, options):
    is_nomina = options.get('is_nomina', False)

    data = []
    conceptos = doc.find('{}Conceptos'.format(PRE[version]))
    # ~ for c in conceptos.getchildren():
    for c in list(conceptos):
        values = CaseInsensitiveDict(c.attrib.copy())
        if is_nomina:
            values['noidentificacion'] = values['ClaveProdServ']
            values['unidad'] = values['ClaveUnidad']
            data.append(values)
            continue

        if version in CFDI_VERSIONS:
            if 'noidentificacion' in values:
                values['noidentificacion'] = '{}\n(SAT {})'.format(
                    values['noidentificacion'], values['ClaveProdServ'])
            else:
                values['noidentificacion'] = 'SAT {}'.format(
                    values['ClaveProdServ'])
            if 'unidad' in values:
                values['unidad'] = '({})\n{}'.format(
                    values['ClaveUnidad'], values['unidad'])
            else:
                values['unidad'] = '{}'.format(values['ClaveUnidad'])

        n = c.find('{}CuentaPredial'.format(PRE[version]))
        if n is not None:
            v = CaseInsensitiveDict(n.attrib.copy())
            info = '\nCuenta Predial Número: {}'.format(v['numero'])
            values['descripcion'] += info

        n = c.find('{}InformacionAduanera'.format(PRE[version]))
        if n is not None:
            v = CaseInsensitiveDict(n.attrib.copy())
            info = '\nNúmero Pedimento: {}'.format(v['numeropedimento'])
            values['descripcion'] += info

        n = c.find('{}ComplementoConcepto'.format(PRE[version]))
        if n is not None:
            v = CaseInsensitiveDict(n[0].attrib.copy())
            info = '\nAlumno: {} (CURP: {})\nNivel: {}, Autorización: {}'.format(
                v['nombreAlumno'], v['CURP'], v['nivelEducativo'], v['autRVOE'])
            values['descripcion'] += info

        data.append(values)
    return data


def _totales(doc, cfdi, version):
    data = {}
    data['moneda'] = doc.attrib['Moneda']
    data['subtotal'] = cfdi['subtotal']
    if 'descuento' in cfdi:
        data['descuento'] = cfdi['descuento']
    data['total'] = cfdi['total']

    tn = {
        '001': 'ISR',
        '002': 'IVA',
        '003': 'IEPS',
    }
    traslados = []
    retenciones = []
    taxlocales = []

    imp = doc.find('{}Impuestos'.format(PRE[version]))
    if imp is not None:
        tmp = CaseInsensitiveDict(imp.attrib.copy())
        for k, v in tmp.items():
            data[k] = v

        node = imp.find('{}Traslados'.format(PRE[version]))
        if node is not None:
            # ~ for n in node.getchildren():
            for n in list(node):
                tmp = CaseInsensitiveDict(n.attrib.copy())
                if version in CFDI_VERSIONS:
                    tasa = ''
                    if 'tasaocuota' in tmp:
                        tasa = round(float(tmp['tasaocuota']), DECIMALES)
                    title = 'Traslado {} {}'.format(tn.get(tmp['impuesto']), tasa)
                else:
                    title = 'Traslado {} {}'.format(tmp['impuesto'], tmp['tasa'])
                if 'importe' in tmp:
                    traslados.append((title, float(tmp['importe'])))

        node = imp.find('{}Retenciones'.format(PRE[version]))
        if node is not None:
            # ~ for n in node.getchildren():
            for n in list(node):
                tmp = CaseInsensitiveDict(n.attrib.copy())
                if version in CFDI_VERSIONS:
                    title = 'Retención {} {}'.format(
                        tn.get(tmp['impuesto']), '')
                else:
                    title = 'Retención {} {}'.format(tmp['impuesto'], '')
                retenciones.append((title, float(tmp['importe'])))

    node = doc.find('{}Complemento/{}ImpuestosLocales'.format(
        PRE[version], PRE['LOCALES']))
    if node is not None:
        for otro in list(node):
            if otro.tag == '{}RetencionesLocales'.format(PRE['LOCALES']):
                tipo = 'Retención '
                name = 'ImpLocRetenido'
                tasa = 'TasadeRetencion'
            else:
                tipo = 'Traslado '
                name = 'ImpLocTrasladado'
                tasa = 'TasadeTraslado'
            title = '{} {} {}%'.format(
                tipo, otro.attrib[name], otro.attrib[tasa])
            importe = float(otro.attrib['Importe'])
            taxlocales.append((title, importe))

    data['traslados'] = traslados
    data['retenciones'] = retenciones
    data['taxlocales'] = taxlocales
    return data


def _timbre(doc, version, values, pdf_from='1'):
    CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||'
    if version in CFDI_VERSIONS:
        CADENA = '||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||'
    node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(
        PRE[version], PRE['TIMBRE']))
    data = CaseInsensitiveDict(node.attrib.copy())

    qr_data = {
        'url': 'https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?',
        'uuid': '&id={}'.format(data['uuid']),
        'emisor': '&re={}'.format(values['rfc_emisor']),
        'receptor': '&rr={}'.format(values['rfc_receptor']),
        'total': '&tt={}'.format(values['total']),
        'sello': '&fe={}'.format(data['sellocfd'][-8:]),
    }
    qr_data = '{url}{uuid}{emisor}{receptor}{total}{sello}'.format(**qr_data)

    data['cbb'] = get_qr(qr_data, 'png')

    # ~ if pdf_from == '1':
        # ~ data['cbb'] = get_qr(qr_data, 'png')
    # ~ else:
        # ~ data['cbb'] = get_qr(qr_data)

    data['cadenaoriginal'] = CADENA.format(**data)
    return data


def _donataria(doc, version, fechadof):
    node = doc.find('{}Complemento/{}Donatarias'.format(
        PRE[version], PRE['DONATARIA']))
    if node is None:
        return {}

    data = CaseInsensitiveDict(node.attrib.copy())
    data['fechadof'] = fechadof
    return data


def _ine(doc, version):
    node = doc.find('{}Complemento/{}INE'.format(PRE[version], PRE['INE']))
    if node is None:
        return {}

    values = (
        ('TipoComite', 'Tipo de Comite: {}'),
        ('TipoProceso', 'Tipo de Proceso: {}'),
        ('IdContabilidad', 'ID de Contabilidad: {}'),
    )
    data = CaseInsensitiveDict(node.attrib.copy())
    for k, v in values:
        if k in data:
            data[k] = v.format(data[k])

    try:
        node = node[0]
        attr = CaseInsensitiveDict(node.attrib.copy())
        values = (
            ('ClaveEntidad', 'Clave de la Entidad: {}'),
            ('Ambito', 'Ámbito: {}'),
        )
        for k, v in values:
            if k in attr:
                data[k] = v.format(attr[k])

        node = node[0]
        attr = CaseInsensitiveDict(node.attrib.copy())
        values = (
            ('IdContabilidad', 'ID de Contabilidad: {}'),
        )
        for k, v in values:
            if k in attr:
                data[k] = v.format(attr[k])
    except Exception as e:
        print(e)

    return data


def _nomina(doc, data, values, version_cfdi):
    is_nomina = values.get('is_nomina', False)
    if not is_nomina:
        return {}

    version = values['version']

    node_nomina = doc.find('{}Complemento/{}Nomina'.format(
        PRE[version_cfdi], PRE['NOMINA'][version]))
    if node_nomina is None:
        return {}
    info = CaseInsensitiveDict(node_nomina.attrib.copy())

    node = node_nomina.find('{}Emisor'.format(PRE['NOMINA'][version]))
    if not node is None:
        data['emisor'].update(CaseInsensitiveDict(node.attrib.copy()))

    node = node_nomina.find('{}Receptor'.format(PRE['NOMINA'][version]))
    data['receptor'].update(CaseInsensitiveDict(node.attrib.copy()))

    node = node_nomina.find('{}Percepciones'.format(PRE['NOMINA'][version]))
    if not node is None:
        data['comprobante'].update(CaseInsensitiveDict(node.attrib.copy()))
        info['percepciones'] = []
        for p in list(node):
            info['percepciones'].append(CaseInsensitiveDict(p.attrib.copy()))

    node = node_nomina.find('{}Deducciones'.format(PRE['NOMINA'][version]))
    if not node is None:
        data['comprobante'].update(CaseInsensitiveDict(node.attrib.copy()))
        info['deducciones'] = []
        for d in list(node):
            info['deducciones'].append(CaseInsensitiveDict(d.attrib.copy()))

    node = node_nomina.find('{}OtrosPagos'.format(PRE['NOMINA'][version]))
    if not node is None:
        info['otrospagos'] = []
        for o in list(node):
            info['otrospagos'].append(CaseInsensitiveDict(o.attrib.copy()))
            n = o.find('{}SubsidioAlEmpleo'.format(PRE['NOMINA'][version]))
            if not n is None:
                info.update(CaseInsensitiveDict(n.attrib.copy()))

    node = node_nomina.find('{}Incapacidades'.format(PRE['NOMINA'][version]))
    if not node is None:
        info['incapacidades'] = []
        for i in list(node):
            info['incapacidades'].append(CaseInsensitiveDict(i.attrib.copy()))

    return info


def _get_info_pays_2(node):
    pre_pays = PRE_DEFAULT['PAGOS']['PRE']
    data = CaseInsensitiveDict(node.attrib.copy())

    path = f"{pre_pays}Totales"
    totales = node.find(path)
    data.update(CaseInsensitiveDict(totales.attrib.copy()))

    path = f"{pre_pays}Pago"
    node_pay = node.find(path)
    data.update(CaseInsensitiveDict(node_pay.attrib.copy()))

    related = []
    for n in node_pay:
        attr = CaseInsensitiveDict(n.attrib.copy())
        if attr:
            attr['metododepagodr'] = ''
            related.append(attr)

    data['related'] = related

    return data


def _cfdipays(doc, data, version):
    pre_pays = PRE_DEFAULT['PAGOS']['PRE']
    path = f"{PRE[version]}Complemento/{pre_pays}Pagos"
    node = doc.find(path)
    if node is None:
        pre_pays = PRE['PAGOS']['1.0']
        path = f"{PRE[version]}Complemento/{pre_pays}Pagos"
        node = doc.find(path)

    if node is None:
        log.error('Node pays not found...')
        return {}

    if version == '4.0':
        info = _get_info_pays_2(node)
    else:
        info = CaseInsensitiveDict(node.attrib.copy())
        related = []
        for n1 in node:
            info.update(CaseInsensitiveDict(n1.attrib.copy()))
            for n2 in n1:
                related.append(CaseInsensitiveDict(n2.attrib.copy()))

        info['related'] = related

    data['comprobante']['totalenletras'] = to_letters(
        float(info['monto']), info['monedap'])
    data['comprobante']['moneda'] = info['monedap']

    return info


def get_data_from_xml(invoice, values, pdf_from='1'):
    data = {'cancelada': invoice.cancelada, 'donativo': False}
    if hasattr(invoice, 'donativo'):
        data['donativo'] = invoice.donativo

    doc = parse_xml(invoice.xml)
    data['comprobante'] = _comprobante(doc, values)
    version = data['comprobante']['version']
    data['emisor'] = _emisor(doc, version, values)
    data['receptor'] = _receptor(doc, version, values)
    data['conceptos'] = _conceptos(doc, version, values)
    data['totales'] = _totales(doc, data['comprobante'], version)
    data['donataria'] = _donataria(doc, version, values['fechadof'])
    data['ine'] = _ine(doc, version)

    options = {
        'rfc_emisor': data['emisor']['rfc'],
        'rfc_receptor': data['receptor']['rfc'],
        'total': data['comprobante']['total'],
    }
    data['timbre'] = _timbre(doc, version, options, pdf_from)
    del data['timbre']['version']
    data['comprobante'].update(data['timbre'])

    data['nomina'] = _nomina(doc, data, values, version)
    data['pagos'] = values.get('pagos', False)
    if data['pagos']:
        data['pays'] = _cfdipays(doc, data, version)
    data['pakings'] = values.get('pakings', [])
    data['el.version'] = values['el.version']

    return data


def to_zip(*files):
    zip_buffer = BytesIO()

    with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:
        for data, file_name in files:
            zip_file.writestr(file_name, data)

    return zip_buffer.getvalue()


def make_fields(xml):
    doc = ET.fromstring(xml)
    data = CaseInsensitiveDict(doc.attrib.copy())
    data.pop('certificado')
    data.pop('sello')
    version = data['version']
    receptor = doc.find('{}Receptor'.format(PRE[version]))
    receptor = CaseInsensitiveDict(receptor.attrib.copy())
    data['receptor_nombre'] = receptor['nombre']
    data['receptor_rfc'] = receptor['rfc']
    data = {k.lower(): v for k, v in data.items()}
    return data


def make_info_mail(data, fields):
    try:
        return data.format(**fields).replace('\n', '<br/>')
    except:
        log.error(data)
        log.error(fields)
        return data.replace('\n', '<br/>')


def send_mail(data):
    msg = ''
    server = SendMail(data['server'])
    is_connect = server.is_connect
    if is_connect:
        msg = server.send(data['options'])
    else:
        msg = server.error
    server.close()
    return {'ok': is_connect, 'msg': msg}


def exists(path):
    return os.path.exists(path)


def get_path_info(path):
    path, filename = os.path.split(path)
    name, extension = os.path.splitext(filename)
    return (path, filename, name, extension)


def get_path_temp(s=''):
    return tempfile.mkstemp(s)[1]


def get_date(value, next_day=False):
    d = parser.parse(value)
    if next_day:
        return d + datetime.timedelta(days=1)
    return d


class UpFile(object):


    def __init__(self):
        self._init_values()

    def _init_values(self):

        return


def save_template(rfc, opt, file_obj):
    result = {'status': 'error', 'ok': False}

    name_template = f'{rfc}{opt}'
    path_template = _join(PATH_MEDIA, 'templates', name_template)

    if save_file(path_template, file_obj.file.read()):
        result = {'status': 'server', 'name': file_obj.filename, 'ok': True}

    return result


def upload_file(rfc, opt, file_obj):
    rfc = rfc.lower()
    tmp = file_obj.filename.split('.')
    ext = tmp[-1].lower()

    versions = ('_3.2.ods',
        '_3.3.ods', '_3.3_cd_1.1.ods', '_3.3_cp_1.0.ods', '_3.3_cn_1.2.ods',  '_3.3_ccp_2.0.ods', '_3.3.json',
        '_4.0.ods',
        '_4.0_cn_1.2.ods',
        '_4.0_cp_2.0.ods',
        '_4.0_ccp_2.0.ods',
        '_4.0_ccp_3.0.ods',
        '_4.0_cd_1.1.ods',
        '_4.0_cce_2.0.ods',
        '_4.0.json',
    )
    if opt in versions:
        return save_template(rfc, opt, file_obj)

    EXTENSIONS = {
        'txt_plantilla_factura_32': EXT['ODS'],
        'txt_plantilla_factura_33': EXT['ODS'],
        'txt_plantilla_factura_html': EXT['HTML'],
        'txt_plantilla_factura_css': EXT['CSS'],
        'txt_plantilla_factura_json': EXT['JSON'],
    }

    if opt in EXTENSIONS:
        if ext != EXTENSIONS[opt]:
            msg = (
                f"Extensión de archivo incorrecta, "
                f"selecciona un archivo {EXTENSIONS[opt].upper()}"
            )
            return {'status': 'server', 'name': msg, 'ok': False}

        NAMES = {
            'txt_plantilla_factura_32': f"{rfc}_3.2.ods",
            'txt_plantilla_factura_33': f"{rfc}_3.3.ods",
            'txt_plantilla_factura_html': f"{rfc}_3.3.html",
            'txt_plantilla_factura_css': f"{rfc}.css",
            'txt_plantilla_factura_json': f"{rfc}_3.3.json",
        }
        name = NAMES[opt]
        paths = {
            'txt_plantilla_factura_32': _join(PATHS['USER'], name),
            'txt_plantilla_factura_33': _join(PATHS['USER'], name),
            'txt_plantilla_factura_html': _join(PATHS['USER'], name),
            'txt_plantilla_factura_css': _join(PATHS['CSS'], name),
            'txt_plantilla_factura_json': _join(PATHS['USER'], name),
        }
        if save_file(paths[opt], file_obj.file.read()):
            return {'status': 'server', 'name': file_obj.filename, 'ok': True}
        return {'status': 'error', 'ok': False}

    if opt == 'txt_plantilla_ticket':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_ticket.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'templates', name)
    elif opt == 'txt_plantilla_donataria':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_3.3_donativo.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'templates', name)
    elif opt == 'txt_plantilla_nomina1233':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_1.2_3.3.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'templates', name)
    elif opt == 'txt_plantilla_pagos10':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_pagos_1.0.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'templates', name)
    elif opt == 'bdfl':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'sqlite':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo SQLite'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}.sqlite'.format(rfc.lower())
        path = _join('/tmp', name)
    elif opt == 'products':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_products.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'tmp', name)
    elif opt == 'invoiceods':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_invoice.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'tmp', name)
    elif opt == 'employees':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_employees.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'tmp', name)
    elif opt == 'nomina':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_nomina.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'tmp', name)
    elif opt == 'ceods':
        tmp = file_obj.filename.split('.')
        ext = tmp[-1].lower()
        if ext != 'ods':
            msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
            return {'status': 'server', 'name': msg, 'ok': False}

        name = '{}_ce.ods'.format(rfc.lower())
        path = _join(PATH_MEDIA, 'tmp', name)

    if save_file(path, file_obj.file.read()):
        return {'status': 'server', 'name': file_obj.filename, 'ok': True}

    return {'status': 'error', 'ok': False}


def _get_pem_from_pfx(cert):
    tmp_p12 = save_temp(cert.p12)
    args = "openssl pkcs12 -in '{}' -clcerts -nodes -nocerts " \
        "-passin pass:'{}' | openssl rsa".format(tmp_p12, _get_md5(cert.rfc))
    result = _call(args)
    _kill(tmp_p12)
    return result.encode()


def cancel_xml(auth, uuid, certificado):
    from .pac import Finkok as PAC

    if DEBUG:
        auth = {}
    else:
        if not auth:
            msg = 'Sin datos para cancelar'
            data = {'ok': False, 'error': msg}
            return data, result

    msg = 'Factura cancelada correctamente'
    data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'}}
    pac = PAC(auth)
    result = pac.cancel_xml(certificado.rfc, str(uuid).upper(),
        certificado.cer_pem.encode(), _get_pem_from_pfx(certificado))
    if result:
        codes = {None: '',
            'Could not get UUID Text': 'UUID no encontrado',
            'Invalid Passphrase': 'Contraseña inválida',
        }
        if not result['CodEstatus'] is None:
            data['ok'] = False
            data['msg'] = codes.get(result['CodEstatus'], result['CodEstatus'])
    else:
        data['ok'] = False
        data['msg'] = pac.error

    return data, result


def cancel_signature(uuid, pk12, rfc, auth):
    from .pac import Finkok as PAC

    token =  _get_md5(rfc)
    if USAR_TOKEN:
        token = auth['PASS']
        if AUTH['DEBUG']:
            token = AUTH['PASS']

    template = read_file(TEMPLATE_CANCEL, 'r')
    data = {
        'rfc': rfc,
        'fecha': datetime.datetime.now().isoformat()[:19],
        'uuid': str(uuid).upper(),
    }
    template = template.format(**data)

    data = {
        'xmlsec': PATH_XMLSEC,
        'pk12': save_temp(pk12),
        'pass': token,
        'template': save_temp(template, 'w'),
    }
    args = '"{xmlsec}" --sign --pkcs12 "{pk12}" --pwd {pass} ' \
        '"{template}"'.format(**data)
    xml_sign = _call(args)

    if DEBUG:
        auth = {}
    else:
        if not auth:
            msg = 'Sin datos para cancelar'
            result = {'ok': False, 'error': msg}
            return result

    msg = 'Factura cancelada correctamente'
    data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'}}
    pac = PAC(auth)
    result = pac.cancel_signature(xml_sign)
    if result:
        codes = {None: '',
            'Could not get UUID Text': 'UUID no encontrado'}
        if not result['CodEstatus'] is None:
            data['ok'] = False
            data['msg'] = codes.get(result['CodEstatus'], result['CodEstatus'])
    else:
        data['ok'] = False
        data['msg'] = pac.error

    return data, result


def run_in_thread(fn):
    def run(*k, **kw):
        t = threading.Thread(target=fn, args=k, kwargs=kw)
        t.start()
        return t
    return run


def get_bool(value):
    if not value:
        return False
    if value == '1':
        return True
    return False


def get_float(value, four=False):
    if four:
        return round(float(value), DECIMALES_TAX)
    return round(float(value), DECIMALES)


def crear_rol(user, contra=''):
    if not contra:
        contra = user
    args = 'psql -U postgres -c "CREATE ROLE {} WITH LOGIN ENCRYPTED ' \
        'PASSWORD \'{}\';"'.format(user, contra)
    try:
        result = _call(args)
        if result == 'CREATE ROLE\n':
            return True
    except Exception as e:
        log.info(e)

    return False


def crear_db(nombre):
    args = 'psql -U postgres -c "CREATE DATABASE {0} WITH ' \
        'OWNER {0};"'.format(nombre)
    try:
        result = _call(args)
        print (result)
        if result == 'CREATE DATABASE\n':
            return True
    except Exception as e:
        log.info(e)

    return False


def _backup_db(user):
    dt = datetime.datetime.now().strftime('%y%m%d_%H%M')
    path_bk = _join(PATH_MEDIA, 'tmp', '{}_{}.bk'.format(user, dt))
    args = 'pg_dump -U postgres -Fc {} > "{}"'.format(user, path_bk)
    _call(args)
    return


def delete_db(user, bk=True):
    if bk:
        _backup_db(user)
    args = 'psql -U postgres -c "DROP DATABASE {0};"'.format(user)
    _call(args)
    args = 'psql -U postgres -c "DROP ROLE {0};"'.format(user)
    _call(args)
    return


def _to_seafile(path_db, data):
    if DEBUG:
        return

    _, filename = os.path.split(path_db)
    if SEAFILE_SERVER:
        msg = '\tSincronizando backup general...'
        log.info(msg)
        seafile = SeaFileAPI(
            SEAFILE_SERVER['URL'],
            SEAFILE_SERVER['USER'],
            SEAFILE_SERVER['PASS'])

        if seafile.is_connect:
            msg = '\tSincronizando: {} '.format(filename)
            log.info(msg)
            seafile.update_file(
                path_db, SEAFILE_SERVER['REPO'], '/', SEAFILE_SERVER['PASS'])
            msg = '\tRespaldo general de {} sincronizado'.format(filename)
            log.info(msg)

    msg = '\tSin datos para sincronización particular de {}'.format(filename)
    if len(data) < 2:
        log.info(msg)
        return
    if not data[0] or not data[1] or not data[2]:
        log.info(msg)
        return

    msg = '\tSincronizando backup particular...'
    log.info(msg)
    seafile = SeaFileAPI(SEAFILE_SERVER['URL'], data[0], data[1])
    if seafile.is_connect:
        msg = '\t\tSincronizando: {} '.format(filename)
        log.info(msg)
        seafile.update_file(path_db, data[2], 'Base de datos/', data[1])
        msg = '\t\tRespaldo partícular de {} sincronizado'.format(filename)
        log.info(msg)

    return


@run_in_thread
def _backup_and_sync(rfc, data):
    msg = 'Generando backup de: {}'.format(rfc)
    log.info(msg)

    sql = 'select correo_timbrado, token_timbrado, token_soporte from emisor;'
    path_bk = _join(PATH_MEDIA, 'tmp', '{}.bk'.format(rfc.lower()))
    if data['type'] == 'postgres':
        args = 'pg_dump -U postgres -Fc {} > "{}"'.format(
            data['name'], path_bk)
        sql = 'psql -U postgres -d {} -Atc "{}"'.format(data['name'], sql)
    elif data['type'] == 'sqlite':
        args = 'gzip -c "{}" > "{}"'.format(data['name'], path_bk)
        sql = 'sqlite3 "{}" "{}"'.format(data['name'], sql)
    try:
        result = _call(args)
        msg = '\tBackup generado de {}'.format(rfc)
        log.info(msg)
        result = _call(sql).strip().split('|')
        _to_seafile(path_bk, result)
    except Exception as e:
        log.info(e)

    return


@run_in_thread
def _backup_companies():
    if DEBUG:
        return

    _, filename = os.path.split(COMPANIES)
    if SEAFILE_SERVER:
        msg = '\tSincronizando backup RFCs...'
        log.info(msg)
        seafile = SeaFileAPI(
            SEAFILE_SERVER['URL'],
            SEAFILE_SERVER['USER'],
            SEAFILE_SERVER['PASS'])

        if seafile.is_connect:
            msg = '\tSincronizando: {} '.format(filename)
            log.info(msg)
            seafile.update_file(
                COMPANIES, SEAFILE_SERVER['REPO'], '/', SEAFILE_SERVER['PASS'])
            msg = '\tRespaldo general de {} sincronizado'.format(filename)
            log.info(msg)
    return


def backup_dbs():
    con = sqlite3.connect(COMPANIES)
    cursor = con.cursor()
    sql = "SELECT * FROM names"
    cursor.execute(sql)
    rows = cursor.fetchall()
    if rows is None:
        return
    cursor.close()
    con.close()

    for rfc, data in rows:
        args = loads(data)
        _backup_and_sync(rfc, args)

    _backup_companies()
    return


def _validar_directorios(path_bk, target):
    path = Path(_join(path_bk, target))
    path.mkdir(parents=True, exist_ok=True)
    return str(path)


def local_copy(files):
    if not MV:
        return

    path_bk = _join(str(Path.home()), DIR_FACTURAS)
    if not os.path.isdir(path_bk):
        msg = 'No existe la carpeta: facturas'
        log.error(msg)
        return

    # ~ args = 'df -P {} | tail -1 | cut -d" " -f 1'.format(path_bk)
    # ~ try:
        # ~ result = _call(args)
        # ~ log.info(result)
    # ~ except:
        # ~ pass
        # ~ if result != 'empresalibre\n':
            # ~ log.info(result)
            # ~ msg = 'Asegurate de que exista la carpeta para sincronizar'
            # ~ log.error(msg)
            # ~ return
    # ~ except subprocess.CalledProcessError:
        # ~ msg = 'No se pudo obtener la ruta para sincronizar'
        # ~ log.error(msg)
        # ~ return

    try:
        for obj, name, target in files:
            path = _validar_directorios(path_bk, target)
            path_file = _join(path, name)
            m = 'wb'
            if name.endswith('xml'):
                m = 'w'
            save_file(path_file, obj, m)
    except Exception as e:
        log.error(e)

    return


def sync_files(files, auth={}):
    if not MV:
        return

    path_bk = _join(str(Path.home()), DIR_FACTURAS)
    if not os.path.isdir(path_bk):
        msg = 'No existe la carpeta: facturas'
        log.error(msg)
        return

    for obj, name, target in files:
        path = _validar_directorios(path_bk, target)
        path_file = _join(path, name)
        m = 'wb'
        if name.endswith('xml'):
            m = 'w'
        save_file(path_file, obj, m)
    return


def sync_cfdi(files):
    local_copy(files)

    if DEBUG:
        return

    # ~ if not auth['REPO'] or not SEAFILE_SERVER:
        # ~ return

    # ~ seafile = SeaFileAPI(SEAFILE_SERVER['URL'], auth['USER'], auth['PASS'])
    # ~ if seafile.is_connect:
        # ~ for f in files:
            # ~ seafile.update_file(
                # ~ f, auth['REPO'], 'Facturas/{}/'.format(f[2]), auth['PASS'])
    return


class ImportCFDI(object):

    def __init__(self, xml):
        self._doc = xml
        self._pre = ''

    def _relacionados(self):
        data = {}
        node = self._doc.find('{}CfdiRelacionados'.format(self._pre))
        if not node is None:
            data = CaseInsensitiveDict(node.attrib.copy())
        return data

    def _emisor(self):
        emisor = self._doc.find('{}Emisor'.format(self._pre))
        data = CaseInsensitiveDict(emisor.attrib.copy())
        node = emisor.find('{}RegimenFiscal'.format(self._pre))
        if not node is None:
            data['regimen_fiscal'] = node.attrib['Regimen']
        return data

    def _receptor(self):
        node = self._doc.find('{}Receptor'.format(self._pre))
        data = CaseInsensitiveDict(node.attrib.copy())
        node = node.find('{}Domicilio'.format(self._pre))
        if not node is None:
            data.update(node.attrib.copy())
        return data

    def _conceptos(self):
        data = []
        conceptos = self._doc.find('{}Conceptos'.format(self._pre))
        for c in list(conceptos):
            values = CaseInsensitiveDict(c.attrib.copy())
            data.append(values)
        return data

    def _impuestos(self):
        data = {}
        node = self._doc.find('{}Impuestos'.format(self._pre))
        if not node is None:
            data = CaseInsensitiveDict(node.attrib.copy())
        return data

    def _timbre(self):
        node = self._doc.find('{}Complemento/{}TimbreFiscalDigital'.format(
            self._pre, PRE['TIMBRE']))
        data = CaseInsensitiveDict(node.attrib.copy())
        data.pop('SelloCFD', None)
        data.pop('SelloSAT', None)
        data.pop('Version', None)
        return data

    def get_data(self):
        invoice = CaseInsensitiveDict(self._doc.attrib.copy())
        invoice.pop('certificado', '')
        invoice.pop('sello', '')
        self._pre = PRE[invoice['version']]

        relacionados = self._relacionados()
        emisor = self._emisor()
        receptor = self._receptor()
        conceptos = self._conceptos()
        impuestos = self._impuestos()
        timbre = self._timbre()

        invoice.update(relacionados)
        invoice.update(emisor)
        invoice.update(receptor)
        invoice.update(impuestos)
        invoice.update(timbre)

        data = {
            'invoice': invoice,
            'emisor': emisor,
            'receptor': receptor,
            'conceptos': conceptos,
        }

        return data


def print_ticket(data, info):
    p = PrintTicket(info)
    return p.printer(data)


def import_products(rfc):
    name = '{}_products.ods'.format(rfc.lower())
    path = _join(PATH_MEDIA, 'tmp', name)
    if not is_file(path):
        return (), 'No se encontró la plantilla'

    if APP_LIBO:
        app = LIBO()
        if app.is_running:
            return app.products(path)

    return (), 'No se encontro LibreOffice'


def import_invoice(rfc):
    name = '{}_invoice.ods'.format(rfc.lower())
    path = _join(PATH_MEDIA, 'tmp', name)
    if not is_file(path):
        return (), 'No se encontró la plantilla'

    if APP_LIBO:
        app = LIBO()
        if app.is_running:
            return app.invoice(path)

    return (), 'No se encontro LibreOffice'


def import_ceods(rfc):
    name = '{}_ce.ods'.format(rfc.lower())
    path = _join(PATH_MEDIA, 'tmp', name)
    if not is_file(path):
        return (), 'No se encontró la plantilla'

    if APP_LIBO:
        app = LIBO()
        if app.is_running:
            return app.get_ce(path)

    return (), 'No se encontro LibreOffice'


def calc_to_date(value):
    return datetime.date.fromordinal(int(value) + 693594)


def get_days(start, end):
    return (end - start).days + 1


def log_file(name, msg='', kill=False):
    path = _join(PATH_MEDIA, 'tmp', '{}.log'.format(name))

    if kill:
        _kill(path)
        return

    with open(path, 'a') as fh:
        line = '{} : {}\n'.format(str(now()), msg)
        fh.write(line)
    return


def get_log(name):
    data = ''
    name = '{}.log'.format(name)
    path = _join(PATH_MEDIA, 'tmp', name)
    if is_file(path):
        data = open(path).read()
    return data, name


def get_timbres(auth):
    from .pac import Finkok as PAC

    if DEBUG:
        return '-d'

    pac = PAC(auth)
    timbres = pac.client_get_timbres(auth['RFC'])
    if pac.error:
        return '-e'

    return timbres


def truncate(value):
    return trunc(value * 100) / 100


def validate_path_bk():
    path_bk = _join(str(Path.home()), DIR_FACTURAS)
    if not os.path.isdir(path_bk):
        msg = 'No existe la carpeta'
        return {'ok': False, 'msg': msg}

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


def respaldar_db(values, path_bk):
    user = values[0].lower()
    db = loads(values[1])['name']
    path = _join(path_bk, '{}.bk'.format(user))
    args = 'pg_dump -U postgres -Fc {} > "{}"'.format(db, path)
    _call(args)
    return


def validate_rfc(value):
    msg = ''
    if len(value) < 12:
        msg = 'Longitud inválida del RFC'
        return msg
    l = 4
    if len(value)==12:
        l = 3
    s = value[0:l]
    r = re.match('[A-ZÑ&]{%s}' % l, s)
    msg = 'Caracteres inválidos al {} del RFC'
    if not r:
        return msg.format('inicio')
    s = value[-3:]
    r = re.match('[A-Z0-9]{3}', s)
    if not r:
        return msg.format('final')
    s = value[l:l+6]
    r = re.match('[0-9]{6}', s)
    msg = 'Fecha inválida en el RFC'
    if not r:
        return msg
    try:
        datetime.datetime.strptime(s, '%y%m%d')
        return ''
    except:
        return msg


def parse_xml2(xml_str):
    return etree.fromstring(xml_str.encode('utf-8'))


def get_idccp():
    uuid_v4 = uuid.uuid4()
    custom_uuid_str = f"CCC{uuid_v4.hex[3:8].upper()}-{uuid_v4.hex[8:12].upper()}-{uuid_v4.hex[12:16].upper()}-{uuid_v4.hex[16:20].upper()}-{uuid_v4.hex[20:32].upper()}"
    return custom_uuid_str

