Generar PDF

This commit is contained in:
Mauricio Baeza 2017-10-15 02:30:55 -05:00
parent 2f79bb290a
commit 981fdba5f4
8 changed files with 655 additions and 19 deletions

View File

@ -1,12 +1,225 @@
#!/usr/bin/env python3
import falcon
from models.main import get_cp
#~ import falcon
#~ from models.main import get_cp
class AppPostalCode(object):
#~ class AppPostalCode(object):
def on_get(self, req, resp):
values = req.params
req.context['result'] = get_cp(values['cp'])
resp.status = falcon.HTTP_200
#~ def on_get(self, req, resp):
#~ values = req.params
#~ req.context['result'] = get_cp(values['cp'])
#~ resp.status = falcon.HTTP_200
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
import re
import collections
from collections import OrderedDict
class CaseInsensitiveDict(collections.MutableMapping):
"""A case-insensitive ``dict``-like object.
Implements all methods and operations of
``collections.MutableMapping`` as well as dict's ``copy``. Also
provides ``lower_items``.
All keys are expected to be strings. The structure remembers the
case of the last key to be set, and ``iter(instance)``,
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
will contain case-sensitive keys. However, querying and contains
testing is case insensitive::
cid = CaseInsensitiveDict()
cid['Accept'] = 'application/json'
cid['aCCEPT'] == 'application/json' # True
list(cid) == ['Accept'] # True
For example, ``headers['content-encoding']`` will return the
value of a ``'Content-Encoding'`` response header, regardless
of how the header name was originally stored.
If the constructor, ``.update``, or equality comparison
operations are given keys that have equal ``.lower()``s, the
behavior is undefined.
"""
def __init__(self, data=None, **kwargs):
self._store = OrderedDict()
if data is None:
data = {}
self.update(data, **kwargs)
def __setitem__(self, key, value):
# Use the lowercased key for lookups, but store the actual
# key alongside the value.
self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
def __delitem__(self, key):
del self._store[key.lower()]
def __iter__(self):
return (casedkey for casedkey, mappedvalue in self._store.values())
def __len__(self):
return len(self._store)
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
return (
(lowerkey, keyval[1])
for (lowerkey, keyval)
in self._store.items()
)
def __eq__(self, other):
if isinstance(other, collections.Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
# Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
# Copy is required
def copy(self):
return CaseInsensitiveDict(self._store.values())
def __repr__(self):
return str(dict(self.items()))
class NumLet(object):
def __init__(self, value, moneda, **args):
self._letras = self._letters(value, moneda)
@property
def letras(self):
return self._letras.upper()
#~ def _letters(self, numero, moneda='peso', texto_inicial='-(',
#~ texto_final='/100 m.n.)-', fraccion_letras=False, fraccion=''):
def _letters(self, numero, moneda='peso'):
texto_inicial = ''
texto_final = '/100 m.n.)-'
fraccion_letras = False
fraccion = ''
enletras = texto_inicial
numero = abs(numero)
numtmp = '%015d' % numero
if numero < 1:
enletras += 'cero ' + self._plural(moneda) + ' '
else:
enletras += self._numlet(numero)
if numero == 1 or numero < 2:
enletras += moneda + ' '
elif int(''.join(numtmp[3:])) == 0 or int(''.join(numtmp[9:])) == 0:
enletras += 'de ' + self._plural(moneda) + ' '
else:
enletras += self._plural(moneda) + ' '
decimal = '%0.2f' % numero
decimal = decimal.split('.')[1]
#~ decimal = int((numero-int(numero))*100)
if fraccion_letras:
if decimal == 0:
enletras += 'con cero ' + self._plural(fraccion)
elif decimal == 1:
enletras += 'con un ' + fraccion
else:
enletras += 'con ' + self._numlet(int(decimal)) + self.plural(fraccion)
else:
enletras += decimal
enletras += texto_final
return enletras
def _numlet(self, numero):
numtmp = '%015d' % numero
co1=0
letras = ''
leyenda = ''
for co1 in range(0,5):
inicio = co1*3
cen = int(numtmp[inicio:inicio+1][0])
dec = int(numtmp[inicio+1:inicio+2][0])
uni = int(numtmp[inicio+2:inicio+3][0])
letra3 = self.centena(uni, dec, cen)
letra2 = self.decena(uni, dec)
letra1 = self.unidad(uni, dec)
if co1 == 0:
if (cen+dec+uni) == 1:
leyenda = 'billon '
elif (cen+dec+uni) > 1:
leyenda = 'billones '
elif co1 == 1:
if (cen+dec+uni) >= 1 and int(''.join(numtmp[6:9])) == 0:
leyenda = "mil millones "
elif (cen+dec+uni) >= 1:
leyenda = "mil "
elif co1 == 2:
if (cen+dec) == 0 and uni == 1:
leyenda = 'millon '
elif cen > 0 or dec > 0 or uni > 1:
leyenda = 'millones '
elif co1 == 3:
if (cen+dec+uni) >= 1:
leyenda = 'mil '
elif co1 == 4:
if (cen+dec+uni) >= 1:
leyenda = ''
letras += letra3 + letra2 + letra1 + leyenda
letra1 = ''
letra2 = ''
letra3 = ''
leyenda = ''
return letras
def centena(self, uni, dec, cen):
letras = ''
numeros = ["","","doscientos ","trescientos ","cuatrocientos ","quinientos ","seiscientos ","setecientos ","ochocientos ","novecientos "]
if cen == 1:
if (dec+uni) == 0:
letras = 'cien '
else:
letras = 'ciento '
elif cen >= 2 and cen <= 9:
letras = numeros[cen]
return letras
def decena(self, uni, dec):
letras = ''
numeros = ["diez ","once ","doce ","trece ","catorce ","quince ","dieci","dieci","dieci","dieci"]
decenas = ["","","","treinta ","cuarenta ","cincuenta ","sesenta ","setenta ","ochenta ","noventa "]
if dec == 1:
letras = numeros[uni]
elif dec == 2:
if uni == 0:
letras = 'veinte '
elif uni > 0:
letras = 'veinti'
elif dec >= 3 and dec <= 9:
letras = decenas[dec]
if uni > 0 and dec > 2:
letras = letras+'y '
return letras
def unidad(self, uni, dec):
letras = ''
numeros = ["","un ","dos ","tres ","cuatro ","cinco ","seis ","siete ","ocho ","nueve "]
if dec != 1:
if uni > 0 and uni <= 5:
letras = numeros[uni]
if uni >= 6 and uni <= 9:
letras = numeros[uni]
return letras
def _plural(self, palabra):
if re.search('[aeiou]$', palabra):
return re.sub('$', 's', palabra)
else:
return palabra + 'es'

View File

@ -206,8 +206,9 @@ class AppDocumentos(object):
#~ self._not_json = True
def on_get(self, req, resp, type_doc, id_doc):
session = req.env['beaker.session']
req.context['result'], file_name, content_type = \
self._db.get_doc(type_doc, id_doc)
self._db.get_doc(type_doc, id_doc, session['rfc'])
resp.append_header('Content-Disposition',
'attachment; filename={}'.format(file_name))
resp.content_type = content_type

View File

@ -8,15 +8,22 @@ import mimetypes
import os
import re
import sqlite3
import socket
import subprocess
import tempfile
import time
import unicodedata
import uuid
from xml.etree import ElementTree as ET
import uno
from com.sun.star.beans import PropertyValue
from dateutil import parser
from .helper import CaseInsensitiveDict, NumLet
from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE
#~ def _get_hash(password):
@ -165,6 +172,18 @@ def get_template(name, data={}):
return template.render(**data)
def get_path_template(name, default='plantilla_factura.ods'):
path = _join(PATH_TEMPLATES, name)
if is_file(path):
return path
path = _join(PATH_TEMPLATES, default)
if is_file(path):
return path
return ''
def dumps(data):
return json.dumps(data, default=str)
@ -207,7 +226,7 @@ def to_slug(string):
value = (unicodedata.normalize('NFKD', string)
.encode('ascii', 'ignore')
.decode('ascii').lower())
return value
return value.replace(' ', '_')
class Certificado(object):
@ -399,3 +418,284 @@ def timbra_xml(xml):
result['uuid'] = pac.uuid
result['fecha'] = pac.fecha
return result
class LIBO(object):
HOST = 'localhost'
PORT = '8100'
ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format(
HOST, PORT)
def __init__(self):
self._app = None
self._start_office()
self._init_values()
def _init_values(self):
self._ctx = None
self._sm = None
self._desktop = None
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
c = 1
while c < 4:
c += 1
self.app = subprocess.Popen([
'soffice', '--headless', '--accept={}'.format(self.ARG)],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(5)
if self.is_running:
return
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)
self._search = self._sheet.getPrintAreas()[0]
self._search = self._sheet.getCellRangeByPosition(
self._search.StartColumn,
self._search.StartRow,
self._search.EndColumn,
self._search.EndRow
)
self._sd = self._sheet.createSearchDescriptor()
self._sd.SearchCaseSensitive = False
return
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 in ('total', 'descuento', 'subtotal'):
self._set_cell('{cfdi.%s}' % k, v, value=True)
else:
self._set_cell('{cfdi.%s}' % k, v)
return
def _timbre(self, data):
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')
#~ image.GraphicURL = data['path_cbb']
#~ pd.add(image)
#~ s = Size()
#~ s.Width = 4500
#~ s.Height = 4500
#~ image.setSize(s)
#~ image.Anchor = self._set_cell('{timbre.cbb}')
return
def _render(self, data):
self._set_search()
self._comprobante(data['comprobante'])
#~ self._emisor(data['emisor'])
#~ self._receptor(data['receptor'])
#~ self._conceptos(data['conceptos'])
self._timbre(data['timbre'])
self._cancelado(False)
#~ self._clean()
return
def pdf(self, path, data):
options = {'AsTemplate': True, 'Hidden': True}
self._template = self._doc_open(path, options)
if self._template is None:
return b''
self._render(data)
options = {'FilterName': 'calc_pdf_Export'}
path = tempfile.mkstemp()[1]
self._template.storeToURL(self._path_url(path), self._set_properties(options))
self._template.close(True)
return self._read(path)
def to_pdf(path, data):
app = LIBO()
if not app.is_running:
return b''
return app.pdf(path, data)
def parse_xml(xml):
return ET.fromstring(xml)
def get_dict(data):
return CaseInsensitiveDict(data)
def to_letters(value, moneda):
monedas = {
'MXN': 'peso',
}
return NumLet(value, monedas[moneda]).letras
def get_qr(data):
scale = 10
path = tempfile.mkstemp()[1]
code = QRCode(data, mode='binary')
code.png(path, scale)
return path
def _comprobante(values):
data = CaseInsensitiveDict(values)
del data['certificado']
#~ print (data)
data['totalenletras'] = to_letters(float(data['total']), data['moneda'])
if data['version'] == '3.3':
tipos = {
'I': 'ingreso',
'E': 'egreso',
'T': 'traslado',
}
data['tipodecomprobante'] = tipos.get(data['tipodecomprobante'])
data['lugarexpedicion'] = 'C.P. Expedición: {}'.format(data['lugarexpedicion'])
return data
def _timbre(doc, version, values):
CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||'
if version == '3.3':
CADENA = '||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||'
node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(
PRE[version], PRE['TIMBRE']))
data = CaseInsensitiveDict(node.attrib.copy())
total_s = '%017.06f' % float(values['total'])
qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % (
values['rfc_emisor'],
values['rfc_receptor'],
total_s,
node.attrib['UUID'])
#~ data['path_cbb'] = get_qr(qr_data)
data['cadenaoriginal'] = CADENA.format(**node.attrib)
return data
def get_data(invoice, rfc):
name = '{}_factura.ods'.format(rfc.lower())
path = get_path_template(name)
values = {}
data = {'cancelada': invoice.cancelada}
doc = parse_xml(invoice.xml)
data['comprobante'] = _comprobante(doc.attrib.copy())
version = data['comprobante']['version']
values['rfc_emisor'] = '123'
values['rfc_receptor'] = '456'
values['total'] = data['comprobante']['total']
data['timbre'] = _timbre(doc, version, values)
return path, data

View File

@ -115,9 +115,12 @@ class StorageEngine(object):
def add_folios(self, values):
return main.Folios.add(values)
def get_doc(self, type_doc, id):
def get_doc(self, type_doc, id, rfc):
if type_doc == 'xml':
data, file_name = main.Facturas.get_xml(id)
content_type = 'application.xml'
content_type = 'application/xml'
if type_doc == 'pdf':
data, file_name = main.Facturas.get_pdf(id, rfc)
content_type = 'application/pdf'
return data, file_name, content_type

View File

@ -14,7 +14,7 @@ if __name__ == '__main__':
from controllers import util
from settings import log, VERSION, PATH_CP, COMPANIES
from settings import log, VERSION, PATH_CP, COMPANIES, PRE
FORMAT = '{0:.2f}'
@ -928,6 +928,7 @@ class Facturas(BaseModel):
regimen_fiscal = TextField(default='')
notas = TextField(default='')
pagada = BooleanField(default=False)
cancelada = BooleanField(default=False)
error = TextField(default='')
class Meta:
@ -939,6 +940,103 @@ class Facturas(BaseModel):
name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
return obj.xml, name
def _get_data_cfdi_to_pdf(self, xml, cancel, version):
pre_nomina = PRE['NOMINA'][version]
data['comprobante']['letters'] = NumerosLetras().letters(
float(data['comprobante']['total'])).upper()
data['year'] = data['comprobante']['fecha'][0:4]
data['month'] = data['comprobante']['fecha'][5:7]
node = doc.find('{}Emisor'.format(pre))
data['emisor'] = node.attrib.copy()
rfc_emisor = data['emisor']['rfc']
node = node.find('{}DomicilioFiscal'.format(pre))
if not node is None:
data['emisor'].update(node.attrib.copy())
node = doc.find('{}Receptor'.format(pre))
data['receptor'] = node.attrib.copy()
rfc_receptor = data['receptor']['rfc']
node = node.find('{}Domicilio'.format(pre))
if not node is None:
data['receptor'].update(node.attrib.copy())
data['conceptos'] = []
conceptos = doc.find('{}Conceptos'.format(pre))
for c in conceptos.getchildren():
data['conceptos'].append(c.attrib.copy())
node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(pre, PRE['TIMBRE']))
data['timbre'] = node.attrib.copy()
total_s = '%017.06f' % float(doc.attrib['total'])
qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % (
rfc_emisor, rfc_receptor, total_s, node.attrib['UUID'])
data['timbre']['path_cbb'] = get_qr(node.attrib['UUID'], qr_data)
data['timbre']['cadenaoriginal'] = CADENA.format(**node.attrib)
data['nomina'] = {}
node = doc.find('{}Complemento/{}Nomina'.format(pre, pre_nomina))
if not node is None:
data['nomina']['nomina'] = node.attrib.copy()
subnode = node.find('{}Emisor'.format(pre_nomina))
if not subnode is None:
data['emisor'].update(subnode.attrib.copy())
subnode = node.find('{}Receptor'.format(pre_nomina))
data['receptor'].update(subnode.attrib.copy())
subnode = node.find('{}Percepciones'.format(pre_nomina))
data['nomina']['percepciones'] = subnode.attrib.copy()
detalle = []
for n in subnode.getchildren():
if 'SeparacionIndemnizacion' in n.tag:
continue
detalle.append(n.attrib.copy())
data['nomina']['percepciones']['detalle'] = detalle
data['nomina']['deducciones'] = None
subnode = node.find('{}Deducciones'.format(pre_nomina))
if not subnode is None:
data['nomina']['deducciones'] = subnode.attrib.copy()
detalle = []
for n in subnode.getchildren():
detalle.append(n.attrib.copy())
data['nomina']['deducciones']['detalle'] = detalle
data['nomina']['incapacidades'] = None
subnode = node.find('{}Incapacidades'.format(pre_nomina))
if not subnode is None:
detalle = []
for n in subnode.getchildren():
detalle.append(n.attrib.copy())
data['nomina']['incapacidades'] = detalle
data['nomina']['otrospagos'] = None
subnode = node.find('{}OtrosPagos'.format(pre_nomina))
if not subnode is None:
data['nomina']['otrospagos'] = subnode.attrib.copy()
detalle = []
for n in subnode.getchildren():
detalle.append(n.attrib.copy())
ns = n.find('{}SubsidioAlEmpleo'.format(pre_nomina))
if not ns is None:
data['nomina']['otrospagos']['SubsidioCausado'] = ns.attrib['SubsidioCausado']
data['nomina']['otrospagos']['detalle'] = detalle
return data
@classmethod
def get_pdf(cls, id, rfc):
obj = Facturas.get(Facturas.id==id)
name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc)
if obj.uuid is None:
return b'', name
path, data = util.get_data(obj, rfc)
doc = util.to_pdf(path, data)
return doc, name
@classmethod
def get_(cls, values):
rows = tuple(Facturas

View File

@ -59,3 +59,17 @@ PATH_OPENSSL = 'openssl'
if 'win' in sys.platform:
PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe')
PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe')
PRE = {
'2.0': '{http://www.sat.gob.mx/cfd/2}',
'2.2': '{http://www.sat.gob.mx/cfd/2}',
'3.0': '{http://www.sat.gob.mx/cfd/3}',
'3.2': '{http://www.sat.gob.mx/cfd/3}',
'3.3': '{http://www.sat.gob.mx/cfd/3}',
'TIMBRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}',
'NOMINA': {
'1.1': '{http://www.sat.gob.mx/nomina}',
'1.2': '{http://www.sat.gob.mx/nomina12}',
}
}

View File

@ -11,8 +11,8 @@ function get_series(){
pre = values[0]
$$('lst_serie').getList().parse(values)
$$('lst_serie').setValue(pre.id)
if(pre.usar_con){
$$('lst_tipo_comprobante').setValue(pre.usar_con)
if(pre.usarcon){
$$('lst_tipo_comprobante').setValue(pre.usarcon)
$$('lst_tipo_comprobante').config.readonly = true
$$('lst_tipo_comprobante').refresh()
}
@ -644,8 +644,15 @@ function cmd_invoice_timbrar_click(){
function grid_invoices_click(id, e, node){
var row = this.getItem(id)
if(id.column == 'xml'){
location = '/doc/xml/' + row.id
}else if(id.column == 'pdf'){
location = '/doc/pdf/' + row.id
}else if(id.column == 'zip'){
location = '/doc/zip/' + row.id
}else if(id.column == 'email'){
show('Correo')
}
}

View File

@ -142,10 +142,10 @@ var suggest_products = {
header: true,
columns: [
{id: 'id', hidden: true},
{id: 'clave', adjust: 'data'},
{id: 'descripcion', adjust: 'data'},
{id: 'unidad', adjust: 'data'},
{id: 'valor_unitario', adjust: 'data',
{id: 'clave', header: 'Clave', adjust: 'data'},
{id: 'descripcion', header: 'Descripción', adjust: 'data'},
{id: 'unidad', header: 'Unidad', adjust: 'data'},
{id: 'valor_unitario', header: 'Valor Unitario', adjust: 'data',
format: webix.i18n.priceFormat}
],
dataFeed:function(text){