#!/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 . 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 _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._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 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}' template_name = f'{rfc.lower()}_{version}.ods' # ~ print('T', 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 + '
' + 'X'*100 + '
' + 'X'*100 # ~ data['timbre_sellosat'] = 'X'*100 + '
' + 'X'*100 + '
' + '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', '
') except: log.error(data) log.error(fields) return data.replace('\n', '
') 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_cd_1.1.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) 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 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'))