From 981fdba5f43a63846c88abde3b831be22612eb75 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 15 Oct 2017 02:30:55 -0500 Subject: [PATCH] Generar PDF --- source/app/controllers/helper.py | 227 +++++++++++++++++- source/app/controllers/main.py | 3 +- source/app/controllers/util.py | 304 +++++++++++++++++++++++- source/app/models/db.py | 7 +- source/app/models/main.py | 100 +++++++- source/app/settings.py | 14 ++ source/static/js/controller/invoices.js | 11 +- source/static/js/ui/invoices.js | 8 +- 8 files changed, 655 insertions(+), 19 deletions(-) diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 159a6b6..6020d66 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -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' diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index ac55355..0c1ebfb 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -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 diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 156de65..0c6c519 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -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 diff --git a/source/app/models/db.py b/source/app/models/db.py index fe07363..406880f 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -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 diff --git a/source/app/models/main.py b/source/app/models/main.py index 586b8c4..0e05d34 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -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 diff --git a/source/app/settings.py b/source/app/settings.py index 88b87ff..60c08b0 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -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}', + } +} diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index eaf553d..af77be7 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -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') } } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 950baf1..1bd8b8a 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -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){