From 55190c5faa72cffaafa8b3c8da0ba3f97e8f3576 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 10 Oct 2017 18:49:05 -0500 Subject: [PATCH] Facturar, generar y timbrar --- .gitignore | 2 + source/app/controllers/cfdi_xml.py | 308 ++++++++++++ source/app/controllers/pac.py | 609 ++++++++++++++++++++++++ source/app/controllers/util.py | 69 ++- source/app/models/main.py | 86 ++-- source/app/settings.py | 9 + source/static/img/file-email.png | Bin 0 -> 3268 bytes source/static/img/file-pdf.png | Bin 0 -> 3601 bytes source/static/img/file-xml.png | Bin 1143 -> 4814 bytes source/static/img/file-zip.png | Bin 0 -> 5620 bytes source/static/js/controller/invoices.js | 52 +- source/static/js/controller/main.js | 9 +- source/static/js/ui/invoices.js | 43 +- source/xslt/cadena.xslt | 345 ++++++++++++++ source/xslt/comercioexterior11.xslt | 181 +++++++ source/xslt/leyendasFisc.xslt | 28 ++ source/xslt/nomina12.xslt | 412 ++++++++++++++++ source/xslt/utilerias.xslt | 22 + 18 files changed, 2110 insertions(+), 65 deletions(-) create mode 100644 source/app/controllers/cfdi_xml.py create mode 100644 source/app/controllers/pac.py create mode 100644 source/static/img/file-email.png create mode 100644 source/static/img/file-pdf.png create mode 100644 source/static/img/file-zip.png create mode 100644 source/xslt/cadena.xslt create mode 100644 source/xslt/comercioexterior11.xslt create mode 100644 source/xslt/leyendasFisc.xslt create mode 100644 source/xslt/nomina12.xslt create mode 100644 source/xslt/utilerias.xslt diff --git a/.gitignore b/.gitignore index 3f3eb5e..a434b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,6 @@ credenciales.conf *.sqlite *.sql rfc.db +configpac.py + diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py new file mode 100644 index 0000000..6cef536 --- /dev/null +++ b/source/app/controllers/cfdi_xml.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python + +import datetime +from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString + +from logbook import Logger + +#~ from settings import DEBUG + + +log = Logger('XML') +CFDI_ACTUAL = 'cfdi33' +NOMINA_ACTUAL = 'nomina12' + +SAT = { + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'cfdi32': { + 'version': '3.2', + 'prefix': 'cfdi', + 'xmlns': 'http://www.sat.gob.mx/cfd/3', + 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd', + }, + 'cfdi33': { + 'version': '3.3', + 'prefix': 'cfdi', + 'xmlns': 'http://www.sat.gob.mx/cfd/3', + 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd', + }, + 'nomina11': { + 'version': '1.1', + 'prefix': 'nomina', + 'xmlns': 'http://www.sat.gob.mx/nomina', + 'schema': 'http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd', + }, + 'nomina12': { + 'version': '1.2', + 'prefix': 'nomina', + 'xmlns': 'http://www.sat.gob.mx/nomina12', + 'schema': 'http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd', + }, +} + + +class CFDI(object): + + def __init__(self, version=CFDI_ACTUAL): + self._sat_cfdi = SAT[version] + self._xsi = SAT['xsi'] + self._pre = self._sat_cfdi['prefix'] + self._cfdi = None + self.error = '' + + def _now(self): + return datetime.datetime.now().isoformat()[:19] + + def get_xml(self, datos): + if not self._validate(datos): + return '' + + self._comprobante(datos['comprobante']) + self._emisor(datos['emisor']) + self._receptor(datos['receptor']) + self._conceptos(datos['conceptos']) + self._impuestos(datos['impuestos']) + if 'nomina' in datos: + self._nomina(datos['nomina']) + if 'complementos' in datos: + self._complementos(datos['complementos']) + return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) + + def add_sello(self, sello): + self._cfdi.attrib['Sello'] = sello + return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8')) + + def _to_pretty_xml(self, source): + tree = parseString(source) + xml = tree.toprettyxml(encoding='utf-8').decode('utf-8') + return xml + + def _validate(self, datos): + if 'nomina' in datos: + return self._validate_nomina(datos) + return True + + def _validate_nomina(self, datos): + comprobante = datos['comprobante'] + + validators = ( + ('MetodoDePago', 'NA'), + ('TipoCambio', '1'), + ('Moneda', 'MXN'), + ('TipoDeComprobante', 'egreso'), + ) + for f, v in validators: + if f in comprobante: + if v != comprobante[f]: + msg = 'El atributo: {}, debe ser: {}'.format(f, v) + self.error = msg + return False + return True + + def _comprobante(self, datos): + attributes = {} + attributes['xmlns:{}'.format(self._pre)] = self._sat_cfdi['xmlns'] + attributes['xmlns:xsi'] = self._xsi + attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + attributes.update(datos) + + #~ if DEBUG: + #~ attributes['Fecha'] = self._now() + #~ attributes['NoCertificado'] = CERT_NUM + + if not 'Version' in attributes: + attributes['Version'] = self._sat_cfdi['version'] + if not 'Fecha' in attributes: + attributes['Fecha'] = self._now() + + self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes) + return + + def _emisor(self, datos): + #~ if DEBUG: + #~ datos['Rfc'] = RFC_TEST + + node_name = '{}:Emisor'.format(self._pre) + emisor = ET.SubElement(self._cfdi, node_name, datos) + return + + def _receptor(self, datos): + node_name = '{}:Receptor'.format(self._pre) + emisor = ET.SubElement(self._cfdi, node_name, datos) + return + + def _conceptos(self, datos): + conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre)) + for row in datos: + complemento = {} + if 'complemento' in row: + complemento = row.pop('complemento') + + taxes = {} + if 'impuestos' in row: + taxes = row.pop('impuestos') + node_name = '{}:Concepto'.format(self._pre) + concepto = ET.SubElement(conceptos, node_name, row) + + if taxes: + node_name = '{}:Impuestos'.format(self._pre) + impuestos = ET.SubElement(concepto, node_name) + if 'traslados' in taxes and taxes['traslados']: + node_name = '{}:Traslados'.format(self._pre) + traslados = ET.SubElement(impuestos, node_name) + for traslado in taxes['traslados']: + ET.SubElement( + traslados, '{}:Traslado'.format(self._pre), traslado) + if 'retenciones' in taxes and taxes['retenciones']: + node_name = '{}:Retenciones'.format(self._pre) + retenciones = ET.SubElement(impuestos, node_name) + for retencion in taxes['retenciones']: + ET.SubElement( + retenciones, '{}:Retencion'.format(self._pre), retencion) + + if 'InformacionAduanera' in row: + for field in fields: + if field in row['InformacionAduanera']: + attributes[field] = row['InformacionAduanera'][field] + if attributes: + node_name = '{}:InformacionAduanera'.format(self._pre) + ET.SubElement(concepto, node_name, attributes) + + if 'CuentaPredial' in row: + attributes = {'numero': row['CuentaPredial']} + node_name = '{}:CuentaPredial'.format(self._pre) + ET.SubElement(concepto, node_name, attributes) + + if 'autRVOE' in row: + fields = ( + 'version', + 'nombreAlumno', + 'CURP', + 'nivelEducativo', + 'autRVOE', + ) + for field in fields: + if field in row['autRVOE']: + attributes[field] = row['autRVOE'][field] + node_name = '{}:ComplementoConcepto'.format(self._pre) + complemento = ET.SubElement(concepto, node_name) + ET.SubElement(complemento, 'iedu:instEducativas', attributes) + return + + def _impuestos(self, datos): + if not datos: + node_name = '{}:Impuestos'.format(self._pre) + ET.SubElement(self._cfdi, node_name) + return + + attributes = {} + fields = ('TotalImpuestosTrasladados', 'TotalImpuestosRetenidos') + for field in fields: + if field in datos: + attributes[field] = datos[field] + node_name = '{}:Impuestos'.format(self._pre) + impuestos = ET.SubElement(self._cfdi, node_name, attributes) + + if 'retenciones' in datos and datos['retenciones']: + retenciones = ET.SubElement(impuestos, '{}:Retenciones'.format(self._pre)) + for row in datos['retenciones']: + ET.SubElement(retenciones, '{}:Retencion'.format(self._pre), row) + + if 'traslados' in datos and datos['traslados']: + traslados = ET.SubElement(impuestos, '{}:Traslados'.format(self._pre)) + for row in datos['traslados']: + ET.SubElement(traslados, '{}:Traslado'.format(self._pre), row) + return + + def _nomina(self, datos): + sat_nomina = SAT[NOMINA_ACTUAL] + pre = sat_nomina['prefix'] + complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre)) + + emisor = datos.pop('Emisor', None) + receptor = datos.pop('Receptor', None) + percepciones = datos.pop('Percepciones', None) + deducciones = datos.pop('Deducciones', None) + + attributes = {} + attributes['xmlns:{}'.format(pre)] = sat_nomina['xmlns'] + attributes['xsi:schemaLocation'] = sat_nomina['schema'] + attributes.update(datos) + + if not 'Version' in attributes: + attributes['Version'] = sat_nomina['version'] + + nomina = ET.SubElement(complemento, '{}:Nomina'.format(pre), attributes) + if emisor: + ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor) + if receptor: + ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor) + if percepciones: + detalle = percepciones.pop('detalle', None) + percepciones = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones) + for row in detalle: + ET.SubElement(percepciones, '{}:Percepcion'.format(pre), row) + if deducciones: + detalle = deducciones.pop('detalle', None) + deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones) + for row in detalle: + ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row) + return + + def _complementos(self, datos): + complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre)) + if 'ce' in datos: + pre = 'cce11' + datos = datos.pop('ce') + emisor = datos.pop('emisor') + propietario = datos.pop('propietario') + receptor = datos.pop('receptor') + destinatario = datos.pop('destinatario') + conceptos = datos.pop('conceptos') + + attributes = {} + attributes['xmlns:{}'.format(pre)] = \ + 'http://www.sat.gob.mx/ComercioExterior11' + attributes['xsi:schemaLocation'] = \ + 'http://www.sat.gob.mx/ComercioExterior11 ' \ + 'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd' + attributes.update(datos) + ce = ET.SubElement( + complemento, '{}:ComercioExterior'.format(pre), attributes) + + attributes = {} + if 'Curp' in emisor: + attributes = {'Curp': emisor.pop('Curp')} + node = ET.SubElement(ce, '{}:Emisor'.format(pre), attributes) + ET.SubElement(node, '{}:Domicilio'.format(pre), emisor) + + if propietario: + ET.SubElement(ce, '{}:Propietario'.format(pre), propietario) + + attributes = {} + if 'NumRegIdTrib' in receptor: + attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')} + node = ET.SubElement(ce, '{}:Receptor'.format(pre), attributes) + ET.SubElement(node, '{}:Domicilio'.format(pre), receptor) + + attributes = {} + if 'NumRegIdTrib' in destinatario: + attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')} + if 'Nombre' in destinatario: + attributes.update({'Nombre': destinatario.pop('Nombre')}) + node = ET.SubElement(ce, '{}:Destinatario'.format(pre), attributes) + ET.SubElement(node, '{}:Domicilio'.format(pre), destinatario) + + node = ET.SubElement(ce, '{}:Mercancias'.format(pre)) + fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie') + for row in conceptos: + detalle = {} + for f in fields: + if f in row: + detalle[f] = row.pop(f) + concepto = ET.SubElement(node, '{}:Mercancia'.format(pre), row) + if detalle: + ET.SubElement( + concepto, '{}:DescripcionesEspecificas'.format(pre), detalle) + return diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py new file mode 100644 index 0000000..49e3cc5 --- /dev/null +++ b/source/app/controllers/pac.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python + +#~ import re +#~ from xml.etree import ElementTree as ET +#~ from requests import Request, Session, exceptions +import datetime +import hashlib +import os +import requests +import time +from lxml import etree +from xml.dom.minidom import parseString +from xml.sax.saxutils import escape, unescape +from uuid import UUID + +from logbook import Logger +from zeep import Client +from zeep.plugins import HistoryPlugin +from zeep.cache import SqliteCache +from zeep.transports import Transport +from zeep.exceptions import Fault, TransportError + +from .configpac import DEBUG, TIMEOUT, AUTH, URL + + +log = Logger('PAC') +#~ node = client.create_message(client.service, SERVICE, **args) +#~ print(etree.tostring(node, pretty_print=True).decode()) + + +class Ecodex(object): + + def __init__(self): + self.codes = URL['codes'] + self.error = '' + self.message = '' + self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT) + self._plugins = None + self._history = None + if DEBUG: + self._history = HistoryPlugin() + self._plugins = [self._history] + + def _get_token(self, rfc): + client = Client(URL['seguridad'], + transport=self._transport, plugins=self._plugins) + try: + result = client.service.ObtenerToken(rfc, self._get_epoch()) + except Fault as e: + self.error = str(e) + log.error(self.error) + return '' + + s = '{}|{}'.format(AUTH['ID'], result.Token) + return hashlib.sha1(s.encode()).hexdigest() + + def _get_token_rest(self, rfc): + data = { + 'rfc': rfc, + 'grant_type': 'authorization_token', + } + headers = {'Content-type': 'application/x-www-form-urlencoded'} + result = requests.post(URL['token'], data=data, headers=headers) + data = result.json() + s = '{}|{}'.format(AUTH['ID'], data['service_token']) + return hashlib.sha1(s.encode()).hexdigest(), data['access_token'] + + def _validate_xml(self, xml): + NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'} + if os.path.isfile(xml): + tree = etree.parse(xml).getroot() + else: + tree = etree.fromstring(xml.encode()) + + fecha = tree.get('Fecha') + rfc = tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI) + data = { + 'ComprobanteXML': etree.tostring(tree).decode(), + 'RFC': rfc, + 'Token': self._get_token(rfc), + 'TransaccionID': self._get_epoch(fecha), + } + return data + + def _get_by_hash(self, sh, rfc): + token, access_token = self._get_token_rest(rfc) + url = URL['hash'].format(sh) + headers = { + 'Authorization': 'Bearer {}'.format(access_token), + 'X-Auth-Token': token, + } + result = requests.get(url, headers=headers) + if result.status_code == 200: + print (result.json()) + return + + def timbra_xml(self, xml): + data = self._validate_xml(xml) + client = Client(URL['timbra'], + transport=self._transport, plugins=self._plugins) + try: + result = client.service.TimbraXML(**data) + except Fault as e: + error = str(e) + if self.codes['HASH'] in error: + sh = error.split(' ')[3] + return self._get_by_hash(sh[:40], data['RFC']) + self.error = error + return '' + + tree = parseString(result.ComprobanteXML.DatosXML) + xml = tree.toprettyxml(encoding='utf-8').decode('utf-8') + return xml + + def _get_epoch(self, date=None): + if isinstance(date, str): + f = '%Y-%m-%dT%H:%M:%S' + e = int(time.mktime(time.strptime(date, f))) + else: + date = datetime.datetime.now() + e = int(time.mktime(date.timetuple())) + return e + + def estatus_cuenta(self, rfc): + #~ Codigos: + #~ 100 = Cuenta encontrada + #~ 101 = RFC no dado de alta en el sistema ECODEX + token = self._get_token(rfc) + if not token: + return {} + + data = { + 'RFC': rfc, + 'Token': token, + 'TransaccionID': self._get_epoch() + } + client = Client(URL['clients'], + transport=self._transport, plugins=self._plugins) + try: + result = client.service.EstatusCuenta(**data) + except Fault as e: + log.error(str(e)) + return + #~ print (result) + return result.Estatus + + +class Finkok(object): + + def __init__(self): + self.codes = URL['codes'] + self.error = '' + self.message = '' + self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT) + self._plugins = None + self._history = None + self.uuid = '' + self.fecha = None + if DEBUG: + self._history = HistoryPlugin() + self._plugins = [self._history] + + def _debug(self): + if not DEBUG: + return + print('SEND', self._history.last_sent) + print('RESULT', self._history.last_received) + return + + def _check_result(self, method, result): + #~ print ('CODE', result.CodEstatus) + #~ print ('INCIDENCIAS', result.Incidencias) + self.message = '' + MSG = { + 'OK': 'Comprobante timbrado satisfactoriamente', + '307': 'Comprobante timbrado previamente', + } + status = result.CodEstatus + if status is None and result.Incidencias: + for i in result.Incidencias['Incidencia']: + self.error += 'Error: {}\n{}'.format( + i['CodigoError'], i['MensajeIncidencia']) + return '' + + if method == 'timbra' and status in (MSG['OK'], MSG['307']): + #~ print ('UUID', result.UUID) + #~ print ('FECHA', result.Fecha) + if status == MSG['307']: + self.message = MSG['307'] + tree = parseString(result.xml) + response = tree.toprettyxml(encoding='utf-8').decode('utf-8') + self.uuid = result.UUID + self.fecha = result.Fecha + + return response + + def _load_file(self, path): + try: + with open(path, 'rb') as f: + data = f.read() + except Exception as e: + self.error = str(e) + return + return data + + def _validate_xml(self, file_xml): + if os.path.isfile(file_xml): + try: + with open(file_xml, 'rb') as f: + xml = f.read() + except Exception as e: + self.error = str(e) + return False, '' + else: + xml = file_xml.encode('utf-8') + return True, xml + + def _validate_uuid(self, uuid): + try: + UUID(uuid) + return True + except ValueError: + self.error = 'UUID no válido: {}'.format(uuid) + return False + + def timbra_xml(self, file_xml): + self.error = '' + method = 'timbra' + ok, xml = self._validate_xml(file_xml) + if not ok: + return '' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'xml': xml, + } + if URL['quick_stamp']: + try: + result = client.service.quick_stamp(**args) + except Fault as e: + self.error = str(e) + return + else: + try: + result = client.service.stamp(**args) + except Fault as e: + self.error = str(e) + return + + return self._check_result(method, result) + + def _get_xml(self, uuid): + if not self._validate_uuid(uuid): + return '' + + method = 'util' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'uuid': uuid, + 'taxpayer_id': self.rfc, + 'invoice_type': 'I', + } + try: + result = client.service.get_xml(**args) + except Fault as e: + self.error = str(e) + return '' + except TransportError as e: + self.error = str(e) + return '' + + if result.error: + self.error = result.error + return '' + + tree = parseString(result.xml) + xml = tree.toprettyxml(encoding='utf-8').decode('utf-8') + return xml + + def recupera_xml(self, file_xml='', uuid=''): + self.error = '' + if uuid: + return self._get_xml(uuid) + + method = 'timbra' + ok, xml = self._validate_xml(file_xml) + if not ok: + return '' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + try: + result = client.service.stamped(xml, AUTH['USER'], AUTH['PASS']) + except Fault as e: + self.error = str(e) + return '' + + return self._check_result(method, result) + + def estatus_xml(self, uuid): + method = 'timbra' + if not self._validate_uuid(uuid): + return '' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + try: + result = client.service.query_pending(AUTH['USER'], AUTH['PASS'], uuid) + #~ print (result.date) + #~ tree = parseString(unescape(result.xml)) + #~ response = tree.toprettyxml(encoding='utf-8').decode('utf-8') + return result.status + except Fault as e: + self.error = str(e) + return '' + + def cancel_xml(self, rfc, uuids, path_cer, path_key): + for u in uuids: + if not self._validate_uuid(u): + return '' + + cer = self._load_file(path_cer) + key = self._load_file(path_key) + method = 'cancel' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + uuid_type = client.get_type('ns1:UUIDS') + sa = client.get_type('ns0:stringArray') + + args = { + 'UUIDS': uuid_type(uuids=sa(string=uuids)), + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'taxpayer_id': rfc, + 'cer': cer, + 'key': key, + 'store_pending': True, + } + try: + result = client.service.cancel(**args) + except Fault as e: + self.error = str(e) + return '' + + if result.CodEstatus and self.codes['205'] in result.CodEstatus: + self.error = result.CodEstatus + return '' + + return result + + def cancel_signature(self, file_xml): + method = 'cancel' + if os.path.isfile(file_xml): + root = etree.parse(file_xml).getroot() + else: + root = etree.fromstring(file_xml) + + xml = etree.tostring(root) + + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'xml': xml, + 'store_pending': True, + } + + result = client.service.cancel_signature(**args) + return result + + def get_acuse(self, rfc, uuids, type_acuse='C'): + for u in uuids: + if not self._validate_uuid(u): + return '' + + method = 'cancel' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'taxpayer_id': rfc, + 'uuid': '', + 'type': type_acuse, + } + try: + result = [] + for u in uuids: + args['uuid'] = u + r = client.service.get_receipt(**args) + result.append(r) + except Fault as e: + self.error = str(e) + return '' + + return result + + def estatus_cancel(self, uuids): + for u in uuids: + if not self._validate_uuid(u): + return '' + + method = 'cancel' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'uuid': '', + } + try: + result = [] + for u in uuids: + args['uuid'] = u + r = client.service.query_pending_cancellation(**args) + result.append(r) + except Fault as e: + self.error = str(e) + return '' + + return result + + def add_token(self, rfc, email): + """ + Se requiere cuenta de reseller para usar este método + """ + method = 'util' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'name': rfc, + 'token_username': email, + 'taxpayer_id': rfc, + 'status': True, + } + result = client.service.add_token(**args) + return result + + def get_date(self): + method = 'util' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + try: + result = client.service.datetime(AUTH['USER'], AUTH['PASS']) + except Fault as e: + self.error = str(e) + return '' + + if result.error: + self.error = result.error + return + + return result.datetime + + def add_client(self, rfc, type_user=False): + """ + Se requiere cuenta de reseller para usar este método + type_user: False == 'P' == Prepago or True == 'O' == On demand + """ + tu = {False: 'P', True: 'O'} + method = 'client' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + args = { + 'reseller_username': AUTH['USER'], + 'reseller_password': AUTH['PASS'], + 'taxpayer_id': rfc, + 'type_user': tu[type_user], + 'added': datetime.datetime.now().isoformat()[:19], + } + try: + result = client.service.add(**args) + except Fault as e: + self.error = str(e) + return '' + + return result + + def edit_client(self, rfc, status=True): + """ + Se requiere cuenta de reseller para usar este método + status = 'A' or 'S' + """ + sv = {False: 'S', True: 'A'} + method = 'client' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + args = { + 'reseller_username': AUTH['USER'], + 'reseller_password': AUTH['PASS'], + 'taxpayer_id': rfc, + 'status': sv[status], + } + try: + result = client.service.edit(**args) + except Fault as e: + self.error = str(e) + return '' + + return result + + def get_client(self, rfc): + """ + Se requiere cuenta de reseller para usar este método + """ + method = 'client' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + args = { + 'reseller_username': AUTH['USER'], + 'reseller_password': AUTH['PASS'], + 'taxpayer_id': rfc, + } + + try: + result = client.service.get(**args) + except Fault as e: + self.error = str(e) + return '' + except TransportError as e: + self.error = str(e) + return '' + + return result + + def assign_client(self, rfc, credit): + """ + Se requiere cuenta de reseller para usar este método + """ + method = 'client' + client = Client( + URL[method], transport=self._transport, plugins=self._plugins) + args = { + 'username': AUTH['USER'], + 'password': AUTH['PASS'], + 'taxpayer_id': rfc, + 'credit': credit, + } + try: + result = client.service.assign(**args) + except Fault as e: + self.error = str(e) + return '' + + return result + + +def _get_data_sat(path): + BF = 'string(//*[local-name()="{}"]/@{})' + NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'} + + try: + if os.path.isfile(path): + tree = etree.parse(path).getroot() + else: + tree = etree.fromstring(path) + + data = {} + emisor = escape( + tree.xpath('string(//cfdi:Emisor/@rfc)', namespaces=NS_CFDI) or + tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI) + ) + receptor = escape( + tree.xpath('string(//cfdi:Receptor/@rfc)', namespaces=NS_CFDI) or + tree.xpath('string(//cfdi:Receptor/@Rfc)', namespaces=NS_CFDI) + ) + data['total'] = tree.get('total') or tree.get('Total') + data['emisor'] = emisor + data['receptor'] = receptor + data['uuid'] = tree.xpath(BF.format('TimbreFiscalDigital', 'UUID')) + except Exception as e: + print (e) + return {} + + return '?re={emisor}&rr={receptor}&tt={total}&id={uuid}'.format(**data) + + +def get_status_sat(xml): + data = _get_data_sat(xml) + if not data: + return + + URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?wsdl' + client = Client(URL, transport=Transport(cache=SqliteCache())) + try: + result = client.service.Consulta(expresionImpresa=data) + except Exception as e: + return 'Error: {}'.format(str(e)) + + return result.Estado + + +def main(): + return + + +if __name__ == '__main__': + main() diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 4a71ac0..156de65 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -15,7 +15,8 @@ import uuid from dateutil import parser -from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT +from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ + PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL #~ def _get_hash(password): @@ -30,6 +31,10 @@ 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: @@ -37,6 +42,18 @@ def _save_temp(data, modo='wb'): return path +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: ') @@ -312,7 +329,7 @@ class Certificado(object): args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:{} | ' \ 'openssl rsa -des3 -passout pass:{}'.format( - self._path_key, password, hashlib.md5(rfc.encode()).hexdigest()) + self._path_key, password, _get_md5(rfc)) key_enc = _call(args) data['key'] = self._key @@ -327,7 +344,7 @@ class Certificado(object): return {} data = self._get_info_cer(rfc) - llave = self._get_info_key(password, rfc) + llave = self._get_info_key(password, data['rfc']) if not llave: return {} @@ -336,3 +353,49 @@ class Certificado(object): self._kill(self._path_key) self._kill(self._path_cer) return data + + +def make_xml(data, certificado): + from .cfdi_xml import CFDI + + if DEBUG: + data['emisor']['Rfc'] = certificado.rfc + data['emisor']['RegimenFiscal'] = '603' + + cfdi = CFDI() + xml = cfdi.get_xml(data) + + data = { + 'xsltproc': PATH_XSLTPROC, + 'xslt': _join(PATH_XSLT, 'cadena.xslt'), + 'xml': _save_temp(xml, 'w'), + 'openssl': PATH_OPENSSL, + 'key': _save_temp(certificado.key_enc, 'w'), + 'pass': _get_md5(certificado.rfc) + } + args = '"{xsltproc}" "{xslt}" "{xml}" | ' \ + '"{openssl}" dgst -sha256 -sign "{key}" -passin pass:{pass} | ' \ + '"{openssl}" enc -base64 -A'.format(**data) + sello = _call(args) + + _kill(data['xml']) + _kill(data['key']) + + return cfdi.add_sello(sello) + + +def timbra_xml(xml): + from .pac import Finkok as PAC + + result = {'ok': True, 'error': ''} + pac = PAC() + xml = pac.timbra_xml(xml) + if not xml: + result['ok'] = False + result['error'] = pac.error + return result + + result['xml'] = xml + result['uuid'] = pac.uuid + result['fecha'] = pac.fecha + return result diff --git a/source/app/models/main.py b/source/app/models/main.py index 65a8f5e..c42b8d9 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -16,6 +16,8 @@ if __name__ == '__main__': from controllers import util from settings import log, VERSION, PATH_CP +FORMAT = '{0:.2f}' + database_proxy = Proxy() class BaseModel(Model): @@ -947,6 +949,7 @@ class Facturas(BaseModel): obj = Facturas.get(Facturas.id==id) if obj.uuid: return False + q = FacturasDetalle.delete().where(FacturasDetalle.factura==obj) q.execute() q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj) @@ -1108,21 +1111,21 @@ class Facturas(BaseModel): receptor = { 'Rfc': invoice.cliente.rfc, - 'Nombre': invoice.cliente.name, + 'Nombre': invoice.cliente.nombre, 'UsoCFDI': invoice.uso_cfdi, } conceptos = [] - rows = Details.select().where(Details.invoice==invoice) + rows = FacturasDetalle.select().where(FacturasDetalle.factura==invoice) for row in rows: concepto = { - 'ClaveProdServ': row.product.key_sat, - 'NoIdentificacion': row.product.key, - 'Cantidad': FORMAT.format(row.cant), - 'ClaveUnidad': row.product.unit.key, - 'Unidad': row.product.unit.name, - 'Descripcion': row.product.description, - 'ValorUnitario': FORMAT.format(row.price), + 'ClaveProdServ': row.producto.clave_sat, + 'NoIdentificacion': row.producto.clave, + 'Cantidad': FORMAT.format(row.cantidad), + 'ClaveUnidad': row.producto.unidad.key, + 'Unidad': row.producto.unidad.name, + 'Descripcion': row.producto.descripcion, + 'ValorUnitario': FORMAT.format(row.valor_unitario), 'Importe': FORMAT.format(row.importe), } @@ -1130,25 +1133,21 @@ class Facturas(BaseModel): traslados = [] retenciones = [] - impuestos = (ProductsTaxes - .select() - .where(ProductsTaxes.product==row.product)) - - for impuesto in impuestos: - if impuesto.tax.tipo == 'E': + for impuesto in row.producto.impuestos: + if impuesto.tipo == 'E': continue - import_tax = round(impuesto.tax.tasa * row.importe, 2) + import_tax = round(impuesto.tasa * row.importe, 2) tipo_factor = 'Tasa' - if impuesto.tax.factor != 'T': + if impuesto.factor != 'T': tipo_factor = 'Cuota' tax = { "Base": FORMAT.format(row.importe), - "Impuesto": impuesto.tax.key, + "Impuesto": impuesto.key, "TipoFactor": tipo_factor, - "TasaOCuota": str(impuesto.tax.tasa), + "TasaOCuota": str(impuesto.tasa), "Importe": FORMAT.format(import_tax), } - if impuesto.tax.tipo == 'T': + if impuesto.tipo == 'T': traslados.append(tax) else: retenciones.append(tax) @@ -1168,24 +1167,24 @@ class Facturas(BaseModel): impuestos['TotalImpuestosRetenidos'] = \ FORMAT.format(invoice.total_retenciones) - taxes = (InvoicesTaxes + taxes = (FacturasImpuestos .select() - .where(InvoicesTaxes.invoice==invoice)) + .where(FacturasImpuestos.factura==invoice)) for tax in taxes: tipo_factor = 'Tasa' - if tax.tax.factor != 'T': + if tax.impuesto.factor != 'T': tipo_factor = 'Cuota' - if tax.tax.tipo == 'T': + if tax.impuesto.tipo == 'T': traslado = { - "Impuesto": tax.tax.key, + "Impuesto": tax.impuesto.key, "TipoFactor": tipo_factor, - "TasaOCuota": str(tax.tax.tasa), + "TasaOCuota": str(tax.impuesto.tasa), "Importe": FORMAT.format(tax.importe), } traslados.append(traslado) else: retencion = { - "Impuesto": tax.tax.key, + "Impuesto": tax.impuesto.key, "Importe": FORMAT.format(tax.importe), } retenciones.append(retencion) @@ -1209,21 +1208,24 @@ class Facturas(BaseModel): obj.estatus = 'Generada' obj.save() - #~ error = False - #~ result = util.timbra_xml(obj.xml) - #~ if result['ok']: - #~ obj.xml = result['xml'] - #~ obj.uuid = result['uuid'] - #~ obj.fecha_timbrado = result['fecha'] - #~ obj.estatus = 'Timbrada' - #~ obj.save() - #~ else: - #~ error = True - #~ msg = result['error'] - #~ obj.estatus = 'Error' - #~ obj.error = msg - #~ obj.save() - return + error = False + msg = 'Factura timbrada correctamente' + result = util.timbra_xml(obj.xml) + if result['ok']: + obj.xml = result['xml'] + obj.uuid = result['uuid'] + obj.fecha_timbrado = result['fecha'] + obj.estatus = 'Timbrada' + obj.save() + row = {'uuid': obj.uuid, 'estatus': 'Timbrada'} + else: + error = True + msg = result['error'] + obj.estatus = 'Error' + obj.error = msg + row = {'estatus': 'Error'} + obj.save() + return {'ok': result['ok'], 'msg': msg, 'row': row} class FacturasDetalle(BaseModel): diff --git a/source/app/settings.py b/source/app/settings.py index 5c1d76d..88b87ff 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -20,6 +20,9 @@ PATH_CP = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'cp.db')) COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) DB_SAT = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'sat.db')) +PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt')) +PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin')) + template_lookup = TemplateLookup(directories=[PATH_TEMPLATES], input_encoding='utf-8', output_encoding='utf-8') @@ -50,3 +53,9 @@ else: log = Logger(LOG_NAME) + +PATH_XSLTPROC = 'xsltproc' +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') diff --git a/source/static/img/file-email.png b/source/static/img/file-email.png new file mode 100644 index 0000000000000000000000000000000000000000..b655a0b7bca4c726aa6f82d264799136f92e1122 GIT binary patch literal 3268 zcmeH}`9BkmAIG=3=9(iiHx);|xo=l90O`U!T9@`*}Yeuh;vR_b;#KdrY z9-*PBrF}t1S5M!-@RE_S$z|jf)2n9J%+VH>R@OEcTP)7b-of#@le3Gf+YNUQ&p*6< zeDQvSTmAuoLBV(KhK7Yl-1{>!Dmo@MF8+Q(Vp1|OCG|mCdd9=dM~|Omk;vIOxli-* z3kr+=DyC3NO3P^F74&D7&#S6mysW8xRaf8ey0NMGZOgmXw)T$BE=KqJzd!W!_Vo`8 zejH*Bv;G+w9UGsR{4_N^!~Q(`o~5_r8UO&kWo~-e4k}sI9_uj}D#x?7;A`CXE+f!e{v3yHY?G5 z^XG_#fagvlg!@}HWAUga%-vT9RS0c)9U)m%D>vyG^dq+O4H5QGjU)O#X0MH zPqOKGGT1Tm#r3mEQzF@mIk%u)W%8Ng-#b1CwinV1fyG5pB*D)NQ*ZXMfl{5vZ)pF95*yrX|88z|O z?c|zdYXetJG{`MH6PQv%_eDTjz}u zcbB~8*^JoD6cnb&O}j7vRDRnzyW8Y}d@aMGYImyvth@)ax)`)*-pj;;gKZ>+X}_vW zcb|XqbxN!ZP%W815WM|Omg*NNlQ=ztXiGH+O0q>;K%$s_&Y*k=yndLVYK<0QkyFIiV_OOl|Hxi2ot>tK-GeJwmVITWg%YYN~-`A47fjT#|Hg zda1*(9IXybSQgBc`l`{0GtctdyC{YJWJ@^TQ5QEcK94}X_yd-9Xk`Jk%)}XZnV46W zWqlEaF!#Sx4`N|+W&lC^Etxd~W-k7hQOPeu>U4fT6p*Q8q|yY~sPes!?k=IR8N zwZ9k?64*qj(plS`t3bk}Ain!5x-8n05K{ouM~ec7KF@hHKUB+P@|vz~{{6}#stz}8iKOYduooY;tSHH_8<~CP3MYT~ zh*ihx33Cve>hgK>UExWMrw&h%>k<7_ZK(C@;UB?cmZjKXK-(o2lX9W5cr8@Bz$=Dk zr91Y?rb;?)w1TCbpp%N#2dz-7PLG99w}sl;pGFJ>@{98*+hiQi=oC+F#yLj6%sUPSBN}U zVY8wEbAz?;Io^VOkHg84Z`YND>5>*;mq3mNu(urLopvYL0fd#}=MMvbi>MFj9t?L#Mql zU(Ry7r(r#?&BD(5$~f9n#I>Im#0lv(BGB7R;Yye=Yy?O3yV5lynK#&89>jy5IsC4F z8G=7R$_|$cB&rf{K!K8MzSbQT%lLdFQgVknCEZyUu!CEMw4%hi+xkK1)`Tfe!r2l) zNa;3Wf8I8NTRr^?NvcDcPMU~t9ZH$N?^;n15Osa5K5w%G-TuORdnLQ+q?utcpALo| z+Qnn|&HafxyWh+10AgBHyiY$0PGLq}>o(I4O&-*QPbaULQ(yAO2bA|`JvuHVU#oA$ z6mKii8BP^ut4-|7A&Hs9S?3FC>5Gq^taOPcqw8sDOqwSc%W9NeXR%odi}_QFS0O{%B}Nsu%Z#&bTxNOI9F0;*HH7qP;x}(jK$&o7w`qf1%IS?XBxH;vGzum|YQx@3p?E zu0Y&{98)LpIBg@|`dbHRUfBlS%fjo+ct@S`tayy!fpG<8_}MQoeBNJQHjmXF*Q|WS z!4YA?KyWBRC3B#~iwCyhrO-#B2EN|%w0v2A+cayl#b>i7Kb|~qI`1=ZxG)Gei)G$TDvgF-C?)7$9MI1tTBg3V?aCVs`t(#m?`YyhD>K@*bOrl zD&eeAmFLRad~OD1fO&OZqx6eYLLOTJ9TPMn@lS|t^jeF^iom}9tiiVQUN`X`Po{*u nD_lSOXuMnL1b1B~YUNO9Ad#uZxl0)P_v182S({cPJ?{SxFLI^V literal 0 HcmV?d00001 diff --git a/source/static/img/file-pdf.png b/source/static/img/file-pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..e8fbe0ed662ca17f40c6cd8d97d14d86051178dd GIT binary patch literal 3601 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 GQU3w1p*gbv literal 0 HcmV?d00001 diff --git a/source/static/img/file-xml.png b/source/static/img/file-xml.png index 74dae07d49f129526e23ce0e9d08e96031b333cd..1fa648e3f387700bec234a2c5abce7dcc86b52cc 100644 GIT binary patch literal 4814 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

$EpuOD7X>=evT@9) z;%;sXyXEf(KJi1EhjhQH4GA%0tnn*ma-zVf&2uW=$6pKxWtH+Y6|hNRV07G(np9&( z0jO#2Pf$oA1~1V1@=;cKo{6iLN6m_|P1#iLb{BoQgfi-vabZG0Q_mskMG@+_2KtNf zg-4lcVPXqm{DG|Gf7ZeOK2j0DHYdL6)1RtVQ`@dgnW`-I@VNS_1~yC@hL1jwM`#y`@v(Gm0^b@O$lG{)sq8kHnUFN-*5-VQwjea0;4`J$mW}MDNhPoGQ^K#?3~kxVXXE^*+N1{0EsuX7*$58?c={AEF%)q5o84?BP(w(k)S^O9>M#QozJR zd5tJE>EHo+8DN-a_sGkrK_cAhUiCmjrC}U$>A6YkpMbEpmlruGc2On8E;@6hzAw2+ zmVpnXX<0Vr%x~p=A|3OH%SA85IlQL$PIvzEowNgETf{jg-_OAHwc6C^>$LN0#)Kyw zm2pKiu^bZR49-Q@k?c0zRHL$OgE>&>M66S{BO}avkl(Vz&Oop|WzTA?W%-s2PP19v zlIB|SCXo8MzzUl?*vsJ8g;;Tje{D^^3B5bFZz|rAS&mz>@Lu`;%nSj7m%Uta; zWM1hGk<^qe^sSHraVotFQE2R8KFQ2E3tRuENAlyjAwyo_OrM<{D6N7J3`-QCc#ba*0L+zi(zn%gWe!6Gn;|0L;2e9L^4faaw~=DM Of=msquGj0MF#iWQB35$% literal 0 HcmV?d00001 diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 5ee58a7..a4c2b98 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -111,7 +111,7 @@ function cmd_edit_invoice_click(id, e, node){ function delete_invoice(id){ webix.ajax().del('/invoices', {id: id}, function(text, xml, xhr){ if(xhr.status == 200){ - $$('grid_invoices').remove(id) + gi.remove(id) msg_sucess('Factura eliminada correctamente') }else{ msg_error('No se pudo eliminar') @@ -120,16 +120,15 @@ function delete_invoice(id){ } - function cmd_delete_invoice_click(id, e, node){ - var row = $$('grid_invoices').getSelectedItem() + var row = gi.getSelectedItem() if (row == undefined){ msg_error('Selecciona una factura') return } - if(!row['uuid']==null){ + if(row.uuid){ msg_error('Solo se pueden eliminar facturas sin timbrar') return } @@ -221,9 +220,9 @@ function validate_invoice(values){ function update_grid_invoices(values){ if(values.new){ - $$('grid_invoices').add(values.row) + gi.add(values.row) }else{ - $$("grid_invoices").updateItem(values.row['id'], values.row) + gi.updateItem(values.row['id'], values.row) } } @@ -232,6 +231,7 @@ function send_timbrar(id){ var values = data.json() if(values.ok){ msg_sucess(values.msg) + gi.updateItem(id, values.row) }else{ webix.alert({ title: 'Error al Timbrar', @@ -256,11 +256,12 @@ function save_invoice(data){ success:function(text, data, XmlHttpRequest){ values = data.json(); if(values.ok){ + msg_sucess('Factura guardada correctamente. Enviando a timbrar') update_grid_invoices(values) send_timbrar(values.row['id']) result = true }else{ - webix.message({type:'error', text:values.msg}) + msg_error(values.msg) } } }) @@ -602,3 +603,40 @@ function grid_details_header_click(id){ } }) } + + +function cmd_refacturar_click(){ + show('Refacturar') +} + + +function cmd_invoice_timbrar_click(){ + if(gi.count() == 0){ + return + } + + var row = gi.getSelectedItem() + if (row == undefined){ + msg_error('Selecciona una factura') + return + } + + if(row.uuid){ + msg_error('La factura ya esta timbrada') + return + } + + msg = '¿Estás seguro de enviar a timbrar esta factura?' + webix.confirm({ + title: 'Timbrar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + send_timbrar(row.id) + } + } + }) +} diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index c44d63b..b9e64ba 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -1,3 +1,4 @@ +var gi = null var controllers = { @@ -29,9 +30,9 @@ var controllers = { $$("chk_automatica").attachEvent("onChange", chk_automatica_change) $$("valor_unitario").attachEvent("onChange", valor_unitario_change) //~ Invoices - $$("cmd_new_invoice").attachEvent("onItemClick", cmd_new_invoice_click) - $$("cmd_edit_invoice").attachEvent("onItemClick", cmd_edit_invoice_click) - $$("cmd_delete_invoice").attachEvent("onItemClick", cmd_delete_invoice_click) + $$('cmd_new_invoice').attachEvent("onItemClick", cmd_new_invoice_click) + $$('cmd_refacturar').attachEvent("onItemClick", cmd_refacturar_click) + $$('cmd_delete_invoice').attachEvent("onItemClick", cmd_delete_invoice_click) $$('cmd_timbrar').attachEvent('onItemClick', cmd_timbrar_click) $$('cmd_close_invoice').attachEvent('onItemClick', cmd_close_invoice_click) $$('search_client_id').attachEvent('onKeyPress', search_client_id_key_press) @@ -42,6 +43,7 @@ var controllers = { $$('grid_details').attachEvent('onHeaderClick', grid_details_header_click) $$('grid_details').attachEvent('onBeforeEditStart', grid_details_before_edit_start) $$('grid_details').attachEvent('onBeforeEditStop', grid_details_before_edit_stop) + $$('cmd_invoice_timbrar').attachEvent('onItemClick', cmd_invoice_timbrar_click) } } @@ -136,6 +138,7 @@ function multi_change(prevID, nextID){ if(active == 'invoices_home'){ get_invoices() } + gi = $$('grid_invoices') return } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 528e13f..6d93c27 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -3,28 +3,46 @@ var toolbar_invoices = [ {view: "button", id: "cmd_new_invoice", label: "Nueva", type: "iconButton", autowidth: true, icon: "plus"}, - {view: "button", id: "cmd_edit_invoice", label: "Editar", type: "iconButton", + {view: "button", id: "cmd_refacturar", label: "Refacturar", type: "iconButton", autowidth: true, icon: "pencil"}, + {}, {view: "button", id: "cmd_delete_invoice", label: "Eliminar", type: "iconButton", autowidth: true, icon: "minus"}, ] +var toolbar_invoices_util = [ + {view: 'button', id: 'cmd_invoice_timbrar', label: 'Timbrar', + type: 'iconButton', autowidth: true, icon: 'ticket'}, +] + + function doc_xml(obj){ var node = "" return node } +function doc_pdf(obj){ + var node = "" + return node +} + + +function get_icon(tipo){ + var node = "" + return node +} + + var grid_invoices_cols = [ {id: "id", header:"ID", hidden:true}, {id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data", sort:"string"}, {id: "folio", header: ["Folio", {content: "numberFilter"}], adjust: "data", sort:"int", css: "cell_right"}, - {id: 'xml', header: '', adjust: 'data', template: doc_xml}, {id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data", - sort:"string"}, + sort:"string", hidden:true}, {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort:"string"}, {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], adjust: 'header', sort: 'string'}, @@ -34,13 +52,17 @@ var grid_invoices_cols = [ sort: 'int', format: webix.i18n.priceFormat, css: 'right'}, {id: "cliente", header: ["Razón Social", {content: "selectFilter"}], fillspace:true, sort:"string"}, + {id: 'xml', header: 'XML', adjust: 'data', template: get_icon('xml')}, + {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')}, + {id: 'zip', header: 'ZIP', adjust: 'data', template: get_icon('zip')}, + {id: 'email', header: '', adjust: 'data', template: get_icon('email')} ] var grid_invoices = { - view: "datatable", - id: "grid_invoices", - select: "row", + view: 'datatable', + id: 'grid_invoices', + select: 'row', adjust: true, footer: true, resizeColumn: true, @@ -296,15 +318,16 @@ var form_invoice = { var multi_invoices = { - id: "multi_invoices", + id: 'multi_invoices', view: 'multiview', animate: true, cells:[ - {id: "invoices_home", rows:[ - {view: "toolbar", elements: toolbar_invoices}, + {id: 'invoices_home', rows:[ + {view: 'toolbar', elements: toolbar_invoices}, + {view: 'toolbar', elements: toolbar_invoices_util}, grid_invoices, ]}, - {id: "invoices_new", rows:[form_invoice, {}]} + {id: 'invoices_new', rows:[form_invoice, {}]} ] } diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt new file mode 100644 index 0000000..2b81c70 --- /dev/null +++ b/source/xslt/cadena.xslt @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + ||| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/comercioexterior11.xslt b/source/xslt/comercioexterior11.xslt new file mode 100644 index 0000000..fd71841 --- /dev/null +++ b/source/xslt/comercioexterior11.xslt @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/leyendasFisc.xslt b/source/xslt/leyendasFisc.xslt new file mode 100644 index 0000000..e0587a2 --- /dev/null +++ b/source/xslt/leyendasFisc.xslt @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/xslt/nomina12.xslt b/source/xslt/nomina12.xslt new file mode 100644 index 0000000..2570170 --- /dev/null +++ b/source/xslt/nomina12.xslt @@ -0,0 +1,412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/xslt/utilerias.xslt b/source/xslt/utilerias.xslt new file mode 100644 index 0000000..d5dd14e --- /dev/null +++ b/source/xslt/utilerias.xslt @@ -0,0 +1,22 @@ + + + + + + | + + + + + + + + | + + + + + + + +